refactor(session): introduce cursor model and full command set
This commit is contained in:
parent
767315a65e
commit
9ec407d9aa
5 changed files with 340 additions and 204 deletions
|
|
@ -98,10 +98,4 @@ pub(crate) fn scan_directory_into(session: &mut SessionData, dir: &Path) {
|
|||
|
||||
session.items = paths.into_iter().map(|p| SessionItem::new(p, 0)).collect();
|
||||
}
|
||||
|
||||
session.current_index = if session.items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(0)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// src/document/manager.rs
|
||||
//
|
||||
// Document Manager – single entry point via SessionCommand.
|
||||
// Document manager: session state, cursors, clipboard, and command dispatch.
|
||||
|
||||
use crate::document::loader::{load_document, scan_directory_into};
|
||||
use crate::document::session::command::SessionCommand;
|
||||
use crate::document::session::data::{CollectionKind, SessionData};
|
||||
use crate::document::session::data::{CollectionKind, SessionData, SessionItem};
|
||||
use crate::document::session::store;
|
||||
use crate::document::DocumentContent;
|
||||
use crate::error::Error;
|
||||
use log::warn;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// The load state of the currently viewed document.
|
||||
#[derive(Debug)]
|
||||
pub enum DocumentState {
|
||||
Empty,
|
||||
|
|
@ -21,9 +21,20 @@ pub enum DocumentState {
|
|||
Error(Error),
|
||||
}
|
||||
|
||||
/// Central state for all sessions, cursors, and the document clipboard.
|
||||
///
|
||||
/// Two cursors are maintained:
|
||||
/// - `active_session_index` – the currently active session
|
||||
/// - `active_document_index` – the currently active document within that session
|
||||
///
|
||||
/// All `SessionCommand` variants that do not carry an explicit target operate
|
||||
/// on the active cursor. `SelectSession` and `SelectDocument` set them.
|
||||
pub struct DocumentManager {
|
||||
sessions: Vec<SessionData>,
|
||||
pub active_session_name: Option<String>,
|
||||
active_session_index: Option<usize>,
|
||||
active_document_index: Option<usize>,
|
||||
clipboard: Option<SessionItem>,
|
||||
session_file_path: Option<PathBuf>,
|
||||
pub state: DocumentState,
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +42,10 @@ impl Default for DocumentManager {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
sessions: Vec::new(),
|
||||
active_session_name: None,
|
||||
active_session_index: None,
|
||||
active_document_index: None,
|
||||
clipboard: None,
|
||||
session_file_path: None,
|
||||
state: DocumentState::Empty,
|
||||
}
|
||||
}
|
||||
|
|
@ -43,79 +57,92 @@ impl DocumentManager {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
/// The single public entry point for all session/collection state changes.
|
||||
/// The single public entry point for all session and document state changes.
|
||||
pub fn handle(&mut self, command: SessionCommand) {
|
||||
match command {
|
||||
// -- Lifecycle --
|
||||
SessionCommand::New { name, kind } => {
|
||||
self.create_session(&name, kind);
|
||||
}
|
||||
SessionCommand::Open { path } => {
|
||||
self.open_session_file(&path);
|
||||
}
|
||||
SessionCommand::Select { name } => {
|
||||
if self.session(&name).is_some() {
|
||||
self.active_session_name = Some(name);
|
||||
}
|
||||
}
|
||||
// Session lifecycle
|
||||
SessionCommand::NewSession { name } => self.new_session(&name),
|
||||
SessionCommand::OpenSession { path } => self.open_session_file(&path),
|
||||
SessionCommand::CloseSession | SessionCommand::RemoveSession => self.close_session(),
|
||||
SessionCommand::SaveSession => self.save_session(),
|
||||
SessionCommand::SelectSession { name } => self.select_session(&name),
|
||||
SessionCommand::DuplicateSession { new_name } => self.duplicate_session(&new_name),
|
||||
SessionCommand::RenameSession { name } => self.rename_session(&name),
|
||||
|
||||
// -- Collection management --
|
||||
SessionCommand::AddDirectory { dir } => {
|
||||
// Collection management
|
||||
SessionCommand::AddDirectoryToSession { dir } => {
|
||||
if let Some(session) = self.active_session_mut() {
|
||||
scan_directory_into(session, &dir);
|
||||
}
|
||||
self.reset_document_cursor();
|
||||
}
|
||||
SessionCommand::AddDocument { path } => {
|
||||
self.add_document(&path);
|
||||
}
|
||||
SessionCommand::CopyDocument {
|
||||
source,
|
||||
target,
|
||||
source_index,
|
||||
target_index,
|
||||
} => {
|
||||
self.copy_document(&source, &target, source_index, target_index);
|
||||
}
|
||||
SessionCommand::RemoveDocument { index } => {
|
||||
self.remove_document(index);
|
||||
}
|
||||
SessionCommand::AddDocumentToSession { path } => self.add_document(&path),
|
||||
|
||||
// -- Navigation --
|
||||
// Document management
|
||||
SessionCommand::SelectDocument { index } => self.select_document(index),
|
||||
SessionCommand::CopyDocument => self.copy_document(),
|
||||
SessionCommand::PasteDocument => self.paste_document(),
|
||||
SessionCommand::RemoveDocument => self.remove_document(),
|
||||
SessionCommand::NextDocument => self.navigate_next(),
|
||||
SessionCommand::PreviousDocument => self.navigate_previous(),
|
||||
SessionCommand::SelectDocument { index } => self.select_document(index),
|
||||
SessionCommand::MoveDocumentUp => self.move_document_up(),
|
||||
SessionCommand::MoveDocumentDown => self.move_document_down(),
|
||||
SessionCommand::MoveDocumentTo { index } => self.move_document_to(index),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only access to a session by name.
|
||||
#[must_use]
|
||||
pub fn session(&self, name: &str) -> Option<&SessionData> {
|
||||
self.sessions.iter().find(|s| s.name == name)
|
||||
}
|
||||
|
||||
/// Read-only access to the active session.
|
||||
#[must_use]
|
||||
pub fn active_session(&self) -> Option<&SessionData> {
|
||||
let name = self.active_session_name.as_deref()?;
|
||||
self.active_session_index.map(|i| &self.sessions[i])
|
||||
}
|
||||
|
||||
/// The index of the currently active document within the active session.
|
||||
#[must_use]
|
||||
pub fn active_document_index(&self) -> Option<usize> {
|
||||
self.active_document_index
|
||||
}
|
||||
|
||||
/// Read-only access to all sessions.
|
||||
#[must_use]
|
||||
pub fn sessions(&self) -> &[SessionData] {
|
||||
&self.sessions
|
||||
}
|
||||
|
||||
/// Read-only access to a session by name.
|
||||
#[must_use]
|
||||
pub fn session_by_name(&self, name: &str) -> Option<&SessionData> {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
self.active_session_index.map(|i| &mut self.sessions[i])
|
||||
}
|
||||
|
||||
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 index_by_name(&self, name: &str) -> Option<usize> {
|
||||
self.sessions.iter().position(|s| s.name == name)
|
||||
}
|
||||
|
||||
/// Sets `active_document_index` to `Some(0)` if the active session has items, else `None`.
|
||||
fn reset_document_cursor(&mut self) {
|
||||
self.active_document_index = match self.active_session() {
|
||||
Some(s) if !s.items.is_empty() => Some(0),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
fn new_session(&mut self, name: &str) {
|
||||
if self.index_by_name(name).is_some() {
|
||||
return;
|
||||
}
|
||||
self.sessions.push(SessionData::new(
|
||||
name,
|
||||
CollectionKind::DocumentCollection,
|
||||
None,
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
fn open_session_file(&mut self, path: &Path) {
|
||||
|
|
@ -141,23 +168,19 @@ impl DocumentManager {
|
|||
false
|
||||
}
|
||||
});
|
||||
session.current_index = match session.current_index {
|
||||
Some(i) if i < session.items.len() => Some(i),
|
||||
_ if !session.items.is_empty() => Some(0),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let browser_name = loaded
|
||||
let browser_index = loaded
|
||||
.iter()
|
||||
.find(|s| s.kind == CollectionKind::DirectoryBrowser)
|
||||
.map(|s| s.name.clone());
|
||||
.position(|s| s.kind == CollectionKind::DirectoryBrowser);
|
||||
|
||||
self.sessions = loaded;
|
||||
self.active_session_name = browser_name;
|
||||
self.active_session_index = browser_index;
|
||||
self.session_file_path = Some(path.to_path_buf());
|
||||
self.state = DocumentState::Empty;
|
||||
self.reset_document_cursor();
|
||||
}
|
||||
Err(e) => {
|
||||
self.state = DocumentState::Error(e);
|
||||
|
|
@ -165,15 +188,89 @@ impl DocumentManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn close_session(&mut self) {
|
||||
let Some(idx) = self.active_session_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The DirectoryBrowser session is managed by the UI and cannot be closed.
|
||||
if self.sessions[idx].kind == CollectionKind::DirectoryBrowser {
|
||||
return;
|
||||
}
|
||||
|
||||
self.sessions.remove(idx);
|
||||
self.active_document_index = None;
|
||||
|
||||
self.active_session_index = if self.sessions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(idx.saturating_sub(1))
|
||||
};
|
||||
}
|
||||
|
||||
fn save_session(&mut self) {
|
||||
let Some(path) = self.session_file_path.clone() else {
|
||||
return;
|
||||
};
|
||||
if let Err(e) = store::save_sessions(&path, &self.sessions) {
|
||||
self.state = DocumentState::Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_session(&mut self, name: &str) {
|
||||
if let Some(idx) = self.index_by_name(name) {
|
||||
self.active_session_index = Some(idx);
|
||||
self.reset_document_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
fn duplicate_session(&mut self, new_name: &str) {
|
||||
if self.index_by_name(new_name).is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(idx) = self.active_session_index else {
|
||||
return;
|
||||
};
|
||||
let items = self.sessions[idx].items.clone();
|
||||
self.sessions.push(SessionData::new(
|
||||
new_name,
|
||||
CollectionKind::DocumentCollection,
|
||||
None,
|
||||
items,
|
||||
));
|
||||
}
|
||||
|
||||
fn rename_session(&mut self, name: &str) {
|
||||
let Some(idx) = self.active_session_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The DirectoryBrowser session cannot be renamed.
|
||||
if self.sessions[idx].kind == CollectionKind::DirectoryBrowser {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.index_by_name(name).is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.sessions[idx].name = name.to_string();
|
||||
}
|
||||
|
||||
fn add_document(&mut self, path: &Path) {
|
||||
self.state = DocumentState::Loading;
|
||||
match load_document(path) {
|
||||
Ok(content) => {
|
||||
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);
|
||||
if session.kind == CollectionKind::DocumentCollection
|
||||
&& !session.items.iter().any(|i| i.path == path)
|
||||
{
|
||||
session.items.push(SessionItem::new(path.to_path_buf(), 0));
|
||||
}
|
||||
}
|
||||
self.active_document_index = self
|
||||
.active_session()
|
||||
.and_then(|s| s.items.iter().position(|i| i.path == path));
|
||||
self.state = DocumentState::Loaded(content);
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -183,111 +280,145 @@ impl DocumentManager {
|
|||
}
|
||||
|
||||
fn select_document(&mut self, index: usize) {
|
||||
if let Some(session) = self.active_session_mut() {
|
||||
if let Some(session) = self.active_session() {
|
||||
if index < session.items.len() {
|
||||
session.current_index = Some(index);
|
||||
self.active_document_index = Some(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_document(&mut self) {
|
||||
let Some(idx) = self.active_document_index else {
|
||||
return;
|
||||
};
|
||||
self.clipboard = self
|
||||
.active_session()
|
||||
.and_then(|s| s.items.get(idx))
|
||||
.cloned();
|
||||
}
|
||||
|
||||
fn paste_document(&mut self) {
|
||||
let Some(item) = self.clipboard.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !matches!(
|
||||
self.active_session().map(|s| &s.kind),
|
||||
Some(CollectionKind::DocumentCollection)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let new_index = {
|
||||
let Some(session) = self.active_session_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if session
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| i.path == item.path && i.page_index == item.page_index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
session.items.push(item);
|
||||
session.items.len() - 1
|
||||
};
|
||||
|
||||
self.active_document_index = Some(new_index);
|
||||
}
|
||||
|
||||
fn remove_document(&mut self) {
|
||||
if !matches!(
|
||||
self.active_session().map(|s| &s.kind),
|
||||
Some(CollectionKind::DocumentCollection)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(idx) = self.active_document_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_index = {
|
||||
let Some(session) = self.active_session_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if idx >= session.items.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
session.items.remove(idx);
|
||||
let new_len = session.items.len();
|
||||
if new_len == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(idx.min(new_len - 1))
|
||||
}
|
||||
};
|
||||
|
||||
self.active_document_index = new_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);
|
||||
}
|
||||
let len = self.active_session().map_or(0, |s| s.items.len());
|
||||
if let Some(idx) = self.active_document_index {
|
||||
if idx + 1 < len {
|
||||
self.active_document_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);
|
||||
}
|
||||
if let Some(idx) = self.active_document_index {
|
||||
if idx > 0 {
|
||||
self.active_document_index = Some(idx - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_document(
|
||||
&mut self,
|
||||
source_name: &str,
|
||||
target_name: &str,
|
||||
source_index: usize,
|
||||
target_index: usize,
|
||||
) {
|
||||
let item = if let Some(s) = self.session(source_name) {
|
||||
match s.items.get(source_index) {
|
||||
Some(item) => item.clone(),
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
self.state = DocumentState::Error(Error::Session(format!(
|
||||
"Source session '{source_name}' not found"
|
||||
)));
|
||||
fn move_document_up(&mut self) {
|
||||
let Some(idx) = self.active_document_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(target) = self.session(target_name) else {
|
||||
self.state = DocumentState::Error(Error::Session(format!(
|
||||
"Target session '{target_name}' not found"
|
||||
)));
|
||||
return;
|
||||
};
|
||||
|
||||
if target.kind != CollectionKind::DocumentCollection {
|
||||
self.state = DocumentState::Error(Error::Session(
|
||||
"Target must be a DocumentCollection".to_string(),
|
||||
));
|
||||
if idx == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if target
|
||||
.items
|
||||
.iter()
|
||||
.any(|i| i.path == item.path && i.page_index == item.page_index)
|
||||
{
|
||||
self.state = DocumentState::Error(Error::Session("Item already exists".to_string()));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(target) = self.session_mut(target_name) {
|
||||
let idx = target_index.min(target.items.len());
|
||||
target.items.insert(idx, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_document(&mut self, index: usize) {
|
||||
// Check kind before taking a mutable borrow on the session, so that
|
||||
// self.state can be set without conflicting borrows.
|
||||
match self.active_session().map(|s| s.kind.clone()) {
|
||||
None => return,
|
||||
Some(CollectionKind::DirectoryBrowser) => {
|
||||
self.state = DocumentState::Error(Error::Session(
|
||||
"Can only remove from a DocumentCollection".to_string(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
Some(CollectionKind::DocumentCollection) => {}
|
||||
}
|
||||
|
||||
let Some(session) = self.active_session_mut() else {
|
||||
return;
|
||||
};
|
||||
session.items.swap(idx, idx - 1);
|
||||
self.active_document_index = Some(idx - 1);
|
||||
}
|
||||
|
||||
if index >= session.items.len() {
|
||||
fn move_document_down(&mut self) {
|
||||
let Some(idx) = self.active_document_index else {
|
||||
return;
|
||||
};
|
||||
let Some(session) = self.active_session_mut() else {
|
||||
return;
|
||||
};
|
||||
if idx + 1 >= session.items.len() {
|
||||
return;
|
||||
}
|
||||
session.items.swap(idx, idx + 1);
|
||||
self.active_document_index = Some(idx + 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);
|
||||
}
|
||||
fn move_document_to(&mut self, target: usize) {
|
||||
let Some(idx) = self.active_document_index else {
|
||||
return;
|
||||
};
|
||||
let Some(session) = self.active_session_mut() else {
|
||||
return;
|
||||
};
|
||||
if idx == target || target >= session.items.len() {
|
||||
return;
|
||||
}
|
||||
let item = session.items.remove(idx);
|
||||
session.items.insert(target, item);
|
||||
self.active_document_index = Some(target);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,43 +3,66 @@
|
|||
//
|
||||
// Commands that operate on sessions and their document collections.
|
||||
|
||||
use crate::document::session::data::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.
|
||||
/// Two implicit cursors exist in the manager:
|
||||
/// - `active_session_index` – the currently active session
|
||||
/// - `active_document_index` – the currently active document within that session
|
||||
///
|
||||
/// `SelectSession` and `SelectDocument` set these cursors. All commands that
|
||||
/// do not carry an explicit target operate on the active cursor.
|
||||
///
|
||||
/// Only `DocumentCollection` sessions are user-managed. The single
|
||||
/// `DirectoryBrowser` session is created by the UI and cannot be closed,
|
||||
/// renamed, or removed.
|
||||
#[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 },
|
||||
// Session lifecycle
|
||||
/// Create a new empty `DocumentCollection` session.
|
||||
NewSession { name: String },
|
||||
/// Load sessions from an existing .ron file.
|
||||
OpenSession { path: PathBuf },
|
||||
/// Close and remove the active session from the store.
|
||||
CloseSession,
|
||||
/// Persist all sessions to the .ron file.
|
||||
SaveSession,
|
||||
/// Set the active session cursor by name.
|
||||
SelectSession { name: String },
|
||||
/// Duplicate the active session as a new `DocumentCollection`.
|
||||
///
|
||||
/// When the source is a `DirectoryBrowser` this produces a snapshot:
|
||||
/// the current items are copied into a new `DocumentCollection`.
|
||||
DuplicateSession { new_name: String },
|
||||
/// Remove the active `DocumentCollection` session without saving.
|
||||
RemoveSession,
|
||||
/// Rename the active `DocumentCollection` session.
|
||||
RenameSession { 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 },
|
||||
// Collection management within the active session
|
||||
/// Scan a directory and populate the active `DirectoryBrowser` session.
|
||||
AddDirectoryToSession { dir: PathBuf },
|
||||
/// Add a single document to the active `DocumentCollection` session.
|
||||
AddDocumentToSession { path: PathBuf },
|
||||
|
||||
// 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.
|
||||
// Document management within the active session
|
||||
/// Set the active document cursor by index.
|
||||
SelectDocument { index: usize },
|
||||
/// Copy the active document into the clipboard.
|
||||
CopyDocument,
|
||||
/// Paste the clipboard item at the end of the active session.
|
||||
PasteDocument,
|
||||
/// Remove the active document from the active session.
|
||||
RemoveDocument,
|
||||
/// Move the active document cursor to the next item.
|
||||
NextDocument,
|
||||
/// Move the active document cursor to the previous item.
|
||||
PreviousDocument,
|
||||
/// Move the active document one position towards the front.
|
||||
MoveDocumentUp,
|
||||
/// Move the active document one position towards the back.
|
||||
MoveDocumentDown,
|
||||
/// Move the active document to an explicit position.
|
||||
MoveDocumentTo { index: usize },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,19 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// src/document/session/data.rs
|
||||
//
|
||||
// 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.
|
||||
// Pure data structures for sessions and their document items.
|
||||
//
|
||||
// There are two kinds of collection:
|
||||
//
|
||||
// DirectoryBrowser
|
||||
// Represents a directory on disk. Items are populated by scanning the
|
||||
// directory at open time and re-scanned each time the session file is
|
||||
// loaded – the persisted item list is intentionally ignored on load.
|
||||
// The source directory is stored in `path`.
|
||||
//
|
||||
// DocumentCollection
|
||||
// A curated, ordered list of documents (or individual pages within them).
|
||||
// Items are persisted exactly as stored. Missing files are detected and
|
||||
// removed with a warning when the session file is loaded.
|
||||
//
|
||||
// The on-disk representation of a session list is managed by `store.rs`.
|
||||
// Operational state (active session cursor, active document cursor, clipboard)
|
||||
// lives in the manager, not here. This module contains only what is persisted.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Determines how a session's items are managed.
|
||||
///
|
||||
/// A `DirectoryBrowser` session is created once by the UI and cannot be closed
|
||||
/// or removed by the user. It can be duplicated via `DuplicateSession`, which
|
||||
/// produces a new `DocumentCollection` containing a snapshot of the current items.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CollectionKind {
|
||||
/// Items are derived from a directory scan; `path` holds the source directory.
|
||||
|
|
@ -48,19 +39,19 @@ impl SessionItem {
|
|||
}
|
||||
}
|
||||
|
||||
/// The full state of a session.
|
||||
/// The persistable state of a single session.
|
||||
///
|
||||
/// Operational state (active document index, clipboard) lives in the manager.
|
||||
/// This struct is serialized as-is to the RON session file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionData {
|
||||
pub name: String,
|
||||
pub kind: CollectionKind,
|
||||
/// For `DirectoryBrowser`: the source directory to scan.
|
||||
/// For `DocumentCollection`: None (items are managed individually).
|
||||
/// The .ron session file path is tracked separately.
|
||||
/// For `DocumentCollection`: `None` (items are managed individually).
|
||||
#[serde(default)]
|
||||
pub path: Option<PathBuf>,
|
||||
pub items: Vec<SessionItem>,
|
||||
/// Index of the currently active item, or `None` if the session is empty.
|
||||
pub current_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl SessionData {
|
||||
|
|
@ -71,13 +62,11 @@ impl SessionData {
|
|||
path: Option<PathBuf>,
|
||||
items: Vec<SessionItem>,
|
||||
) -> Self {
|
||||
let current_index = if items.is_empty() { None } else { Some(0) };
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
kind,
|
||||
path,
|
||||
items,
|
||||
current_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ pub fn load_sessions(path: &Path) -> Result<Vec<SessionData>> {
|
|||
///
|
||||
/// Returns [`Error::Session`] if the sessions cannot be serialized.
|
||||
/// Returns [`Error::Io`] if the file cannot be written.
|
||||
#[allow(dead_code)]
|
||||
pub fn save_sessions(path: &Path, sessions: &[SessionData]) -> Result<()> {
|
||||
let content = ron::ser::to_string_pretty(sessions, ron::ser::PrettyConfig::default())
|
||||
.map_err(|e| Error::Session(format!("Failed to serialize sessions: {e}")))?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue