// SPDX-License-Identifier: GPL-3.0-or-later // src/document/manager.rs // // Document Manager – single entry point via DocumentCommand. use crate::document::loader::{load_document, scan_directory_into}; use crate::document::session::{SessionData, SessionKind}; use crate::document::store; use crate::document::DocumentContent; use crate::error::Error; use log::warn; use std::path::{Path, PathBuf}; #[allow(dead_code)] #[derive(Debug)] pub enum DocumentState { Empty, Loading, Loaded(DocumentContent), Error(Error), } /// All actions the UI can request from the manager. /// The manager executes; the UI decides. #[derive(Debug)] pub enum DocumentCommand { /// Create a new session. Does nothing if a session with that name already exists. CreateSession { name: String, kind: SessionKind, path: Option, }, /// Scan a directory into an existing session, replacing its current items. ScanDirectory { session: String, dir: PathBuf }, /// Load a file and make it the active item in the given session. AddFileToSession { session: String, path: PathBuf }, /// Set the active session by name. SetActive(String), /// Open a session file (.ron): loads all sessions and activates the DirectoryBrowser. OpenSession(PathBuf), /// Navigate to the next item in the active session. NavigateNext, /// Navigate to the previous item in the active session. NavigatePrevious, /// Select a specific item by index in the active session. SelectItem(usize), } pub struct DocumentManager { sessions: Vec, pub active_session_name: Option, pub state: DocumentState, } impl Default for DocumentManager { fn default() -> Self { Self { sessions: Vec::new(), active_session_name: None, state: DocumentState::Empty, } } } impl DocumentManager { pub fn new() -> Self { Self::default() } /// The single public entry point for all state changes. pub fn handle(&mut self, command: DocumentCommand) { match command { DocumentCommand::CreateSession { name, kind, path } => { if self.session(&name).is_none() { self.sessions .push(SessionData::new(&name, kind, path, Vec::new())); } } DocumentCommand::ScanDirectory { session, dir } => { if let Some(s) = self.session_mut(&session) { scan_directory_into(s, &dir); } } DocumentCommand::AddFileToSession { session, path } => { self.active_session_name = Some(session); self.load_file(&path); } DocumentCommand::SetActive(name) => { if self.session(&name).is_some() { self.active_session_name = Some(name); } } DocumentCommand::OpenSession(path) => { self.load_all_sessions(&path); } DocumentCommand::NavigateNext => self.next(), DocumentCommand::NavigatePrevious => self.previous(), DocumentCommand::SelectItem(index) => self.select_item(index), } } /// Read-only access to a session by name. pub fn session(&self, name: &str) -> Option<&SessionData> { self.sessions.iter().find(|s| s.name == name) } // Mutable access to a session by name. fn session_mut(&mut self, name: &str) -> Option<&mut SessionData> { self.sessions.iter_mut().find(|s| s.name == name) } // Mutable access to the currently active session. // The name is cloned to satisfy the borrow checker – it is a cheap String copy. fn active_session_mut(&mut self) -> Option<&mut SessionData> { let name = self.active_session_name.clone()?; self.sessions.iter_mut().find(|s| s.name == name) } // Loads all sessions from a RON file and activates the DirectoryBrowser. fn load_all_sessions(&mut self, path: &Path) { match store::load_sessions(path) { Ok(mut loaded) => { // Re-scan every DirectoryBrowser's source directory on load. // Validate every DocumentCollection: remove items whose files no longer exist. for session in loaded.iter_mut() { match session.kind { SessionKind::DirectoryBrowser => { if let Some(dir) = session.path.clone() { scan_directory_into(session, &dir); } } SessionKind::DocumentCollection => { session.items.retain(|item| { if item.path.exists() { true } else { warn!( "Session '{}': {}", session.name, Error::NotFound(item.path.display().to_string()) ); false } }); // Reset index if it pointed to a now-removed item. session.current_index = match session.current_index { Some(i) if i < session.items.len() => Some(i), _ if !session.items.is_empty() => Some(0), _ => None, }; } } } // Activate the DirectoryBrowser as the default entry point. let browser_name = loaded .iter() .find(|s| s.kind == SessionKind::DirectoryBrowser) .map(|s| s.name.clone()); self.sessions = loaded; self.active_session_name = browser_name; self.state = DocumentState::Empty; } Err(e) => { self.state = DocumentState::Error(e); } } } fn select_item(&mut self, index: usize) { if let Some(session) = self.active_session_mut() { if index < session.items.len() { session.current_index = Some(index); } } } fn next(&mut self) { if let Some(session) = self.active_session_mut() { if let Some(idx) = session.current_index { if idx + 1 < session.items.len() { session.current_index = Some(idx + 1); } } } } fn previous(&mut self) { if let Some(session) = self.active_session_mut() { if let Some(idx) = session.current_index { if idx > 0 { session.current_index = Some(idx - 1); } } } } fn load_file(&mut self, path: &Path) { self.state = DocumentState::Loading; match load_document(path) { Ok(content) => { // Align current_index to the loaded file's position in the session. if let Some(session) = self.active_session_mut() { if let Some(idx) = session.items.iter().position(|item| item.path == path) { session.current_index = Some(idx); } } self.state = DocumentState::Loaded(content); } Err(e) => { self.state = DocumentState::Error(e); } } } }