From e928c611077e8b72423e57a5e1b168006f19e88a Mon Sep 17 00:00:00 2001 From: mow Date: Sun, 8 Mar 2026 09:01:20 +0100 Subject: [PATCH] refactor: rename commands to SessionCommand/DocumentCommand split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename SessionKind → CollectionKind - Replace monolithic DocumentCommand with SessionCommand (lifecycle + collection) and DocumentCommand (reserved for future document operations) - Separate session creation from population (New → Select → AddDirectory) - Collection commands operate on active session, no hidden side effects - RemoveDocument/CopyDocument use explicit indices (no more Option) --- core/src/document/command.rs | 51 ++++++ core/src/document/manager.rs | 300 ++++++++++++++--------------------- core/src/document/mod.rs | 1 + core/src/document/session.rs | 8 +- 4 files changed, 171 insertions(+), 189 deletions(-) create mode 100644 core/src/document/command.rs diff --git a/core/src/document/command.rs b/core/src/document/command.rs new file mode 100644 index 0000000..01ce88e --- /dev/null +++ b/core/src/document/command.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/document/command.rs +// +// Command enums for session management and document operations. + +use crate::document::session::CollectionKind; +use std::path::PathBuf; + +/// Commands operating on sessions and their document collections. +/// All collection-mutating commands operate on the active session +/// unless they explicitly name a session. +#[derive(Debug, Clone)] +pub enum SessionCommand { + // -- Lifecycle -- + /// Create a new empty session of the given kind. + New { name: String, kind: CollectionKind }, + /// Open sessions from an existing .ron file. + Open { path: PathBuf }, + /// Switch the active session by name. + Select { name: String }, + + // -- Collection management -- + /// Scan a directory and replace the active session's items. + AddDirectory { dir: PathBuf }, + /// Add a single document to the active session. + AddDocument { path: PathBuf }, + /// Copy a document from one session to another. + CopyDocument { + source: String, + target: String, + source_index: usize, + target_index: usize, + }, + /// Remove a document by index from the active session. + RemoveDocument { index: usize }, + + // -- Navigation -- + /// Navigate to the next document in the active session. + NextDocument, + /// Navigate to the previous document in the active session. + PreviousDocument, + /// Select a document by index in the active session. + SelectDocument { index: usize }, +} + +/// Commands operating on the active document itself. +/// Reserved for future use (rotate, flip, zoom, export, …). +#[derive(Debug, Clone)] +pub enum DocumentCommand { + // Future: Rotate, Flip, Zoom, Export, ... +} diff --git a/core/src/document/manager.rs b/core/src/document/manager.rs index cbc83c9..c17ac5d 100644 --- a/core/src/document/manager.rs +++ b/core/src/document/manager.rs @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/document/manager.rs // -// Document Manager – single entry point via DocumentCommand. +// Document Manager – single entry point via SessionCommand. +use crate::document::command::SessionCommand; use crate::document::loader::{load_document, scan_directory_into}; -use crate::document::session::{SessionData, SessionKind}; +use crate::document::session::{CollectionKind, SessionData}; use crate::document::store; use crate::document::DocumentContent; use crate::error::Error; use log::warn; -use std::path::{Path, PathBuf}; +use std::path::Path; #[allow(dead_code)] #[derive(Debug)] @@ -20,44 +21,6 @@ pub enum DocumentState { 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), - /// Copy an item from one session to another. - CopyItemToSession { - source_session: String, - target_session: String, - source_item_index: usize, - target_item_index: Option, - }, - /// Delete an item from a session. - DeleteItemFromSession { - session: String, - item_index: Option, - }, -} - pub struct DocumentManager { sessions: Vec, pub active_session_name: Option, @@ -79,61 +42,47 @@ impl DocumentManager { Self::default() } - /// The single public entry point for all state changes. - pub fn handle(&mut self, command: DocumentCommand) { + /// The single public entry point for all session/collection state changes. + pub fn handle(&mut self, command: SessionCommand) { match command { - DocumentCommand::CreateSession { name, kind, path } => { - if self.session(&name).is_none() { - self.sessions - .push(SessionData::new(&name, kind, path, Vec::new())); - } + // -- Lifecycle -- + SessionCommand::New { name, kind } => { + self.create_session(&name, kind); } - - DocumentCommand::ScanDirectory { session, dir } => { - if let Some(s) = self.session_mut(&session) { - scan_directory_into(s, &dir); - } + SessionCommand::Open { path } => { + self.open_session_file(&path); } - - DocumentCommand::AddFileToSession { session, path } => { - self.active_session_name = Some(session); - self.load_file(&path); - } - - DocumentCommand::SetActive(name) => { + SessionCommand::Select { name } => { if self.session(&name).is_some() { self.active_session_name = Some(name); } } - DocumentCommand::OpenSession(path) => { - self.load_all_sessions(&path); + // -- Collection management -- + SessionCommand::AddDirectory { dir } => { + if let Some(session) = self.active_session_mut() { + scan_directory_into(session, &dir); + } } - - DocumentCommand::NavigateNext => self.next(), - DocumentCommand::NavigatePrevious => self.previous(), - DocumentCommand::SelectItem(index) => self.select_item(index), - - DocumentCommand::CopyItemToSession { - source_session, - target_session, - source_item_index, - target_item_index, + SessionCommand::AddDocument { path } => { + self.add_document(&path); + } + SessionCommand::CopyDocument { + source, + target, + source_index, + target_index, } => { - self.copy_item_to_session( - &source_session, - &target_session, - source_item_index, - target_item_index, - ); + self.copy_document(&source, &target, source_index, target_index); + } + SessionCommand::RemoveDocument { index } => { + self.remove_document(index); } - DocumentCommand::DeleteItemFromSession { - session, - item_index, - } => { - self.delete_item_from_session(&session, item_index); - } + // -- Navigation -- + SessionCommand::NextDocument => self.navigate_next(), + SessionCommand::PreviousDocument => self.navigate_previous(), + SessionCommand::SelectDocument { index } => self.select_document(index), } } @@ -142,32 +91,41 @@ impl DocumentManager { self.sessions.iter().find(|s| s.name == name) } - // Mutable access to a session by name. + /// Read-only access to the active session. + pub fn active_session(&self) -> Option<&SessionData> { + let name = self.active_session_name.as_deref()?; + self.sessions.iter().find(|s| s.name == name) + } + + // -- Private helpers -- + 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) { + fn create_session(&mut self, name: &str, kind: CollectionKind) { + if self.session(name).is_none() { + self.sessions + .push(SessionData::new(name, kind, None, Vec::new())); + } + } + + fn open_session_file(&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 => { + CollectionKind::DirectoryBrowser => { if let Some(dir) = session.path.clone() { scan_directory_into(session, &dir); } } - SessionKind::DocumentCollection => { + CollectionKind::DocumentCollection => { session.items.retain(|item| { if item.path.exists() { true @@ -180,8 +138,6 @@ impl DocumentManager { 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), @@ -191,10 +147,9 @@ impl DocumentManager { } } - // Activate the DirectoryBrowser as the default entry point. let browser_name = loaded .iter() - .find(|s| s.kind == SessionKind::DirectoryBrowser) + .find(|s| s.kind == CollectionKind::DirectoryBrowser) .map(|s| s.name.clone()); self.sessions = loaded; @@ -207,40 +162,10 @@ impl DocumentManager { } } - 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) { + fn add_document(&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); @@ -254,49 +179,73 @@ impl DocumentManager { } } - fn copy_item_to_session( + fn select_document(&mut self, index: usize) { + if let Some(session) = self.active_session_mut() { + if index < session.items.len() { + session.current_index = Some(index); + } + } + } + + fn navigate_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 navigate_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 copy_document( &mut self, - source_session: &str, - target_session: &str, - source_item_index: usize, - target_item_index: Option, + source_name: &str, + target_name: &str, + source_index: usize, + target_index: usize, ) { - let source = match self.session(source_session) { - Some(s) => s, + let item = match self.session(source_name) { + Some(s) => match s.items.get(source_index) { + Some(item) => item.clone(), + None => return, + }, None => { self.state = DocumentState::Error(Error::Session(format!( - "Session '{}' not found", - source_session + "Source session '{}' not found", + source_name ))); return; } }; - let target = match self.session(target_session) { + let target = match self.session(target_name) { Some(s) => s, None => { self.state = DocumentState::Error(Error::Session(format!( - "Session '{}' not found", - target_session + "Target session '{}' not found", + target_name ))); return; } }; - if target.kind != SessionKind::DocumentCollection { + if target.kind != CollectionKind::DocumentCollection { self.state = DocumentState::Error(Error::Session( "Target must be a DocumentCollection".to_string(), )); return; } - let item = match source.items.get(source_item_index) { - Some(item) => item.clone(), - None => { - return; - } - }; - if target .items .iter() @@ -306,55 +255,36 @@ impl DocumentManager { return; } - let insert_index = match target_item_index { - Some(idx) if idx <= target.items.len() => idx, - _ => target.items.len(), - }; - - if let Some(target) = self.session_mut(target_session) { - target.items.insert(insert_index, item); + if let Some(target) = self.session_mut(target_name) { + let idx = target_index.min(target.items.len()); + target.items.insert(idx, item); } } - fn delete_item_from_session(&mut self, session: &str, item_index: Option) { - let target = match self.session(session) { + fn remove_document(&mut self, index: usize) { + let session = match self.active_session_mut() { Some(s) => s, - None => { - self.state = DocumentState::Error(Error::Session(format!( - "Session '{}' not found", - session - ))); - return; - } + None => return, }; - if target.kind != SessionKind::DocumentCollection { + if session.kind != CollectionKind::DocumentCollection { self.state = DocumentState::Error(Error::Session( - "Session must be a DocumentCollection".to_string(), + "Can only remove from a DocumentCollection".to_string(), )); return; } - let idx = match item_index { - Some(i) => i, - None => match target.current_index { - Some(i) => i, - None => return, - }, - }; + if index >= session.items.len() { + return; + } - if let Some(target) = self.session_mut(session) { - if idx < target.items.len() { - target.items.remove(idx); - - let new_len = target.items.len(); - if new_len == 0 { - target.current_index = None; - } else if let Some(current) = target.current_index { - if current >= new_len { - target.current_index = Some(new_len - 1); - } - } + session.items.remove(index); + let new_len = session.items.len(); + if new_len == 0 { + session.current_index = None; + } else if let Some(current) = session.current_index { + if current >= new_len { + session.current_index = Some(new_len - 1); } } } diff --git a/core/src/document/mod.rs b/core/src/document/mod.rs index 44ad87d..5954763 100644 --- a/core/src/document/mod.rs +++ b/core/src/document/mod.rs @@ -3,6 +3,7 @@ // // Document abstraction and format definitions. +pub mod command; pub mod core; pub mod format; pub mod loader; diff --git a/core/src/document/session.rs b/core/src/document/session.rs index ebec144..01e49c5 100644 --- a/core/src/document/session.rs +++ b/core/src/document/session.rs @@ -4,7 +4,7 @@ // A session is the unit of work in Noctua. It groups a set of document items // under a name and tracks which item is currently active. // -// There are two kinds of session: +// There are two kinds of collection: // // DirectoryBrowser // Represents a directory on disk. Items are populated by scanning the @@ -25,7 +25,7 @@ use std::path::PathBuf; /// Determines how a session's items are managed. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum SessionKind { +pub enum CollectionKind { /// Items are derived from a directory scan; `path` holds the source directory. DirectoryBrowser, /// Items are a curated list of documents or pages; `path` is None. @@ -52,7 +52,7 @@ impl SessionItem { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionData { pub name: String, - pub kind: SessionKind, + pub kind: CollectionKind, /// Source directory for `DirectoryBrowser` sessions; `None` for `DocumentCollection`. #[serde(default)] pub path: Option, @@ -64,7 +64,7 @@ pub struct SessionData { impl SessionData { pub fn new( name: &str, - kind: SessionKind, + kind: CollectionKind, path: Option, items: Vec, ) -> Self {