refactor(core): reorganize module structure

This commit is contained in:
mow 2026-03-08 17:41:05 +01:00
parent 59ab96be98
commit a6c1863f57
22 changed files with 231 additions and 164 deletions

View file

@ -4,9 +4,9 @@
// CLI test harness for the noctua_core document foundation.
use clap::{ArgGroup, Parser};
use noctua_core::document::command::SessionCommand;
use noctua_core::document::manager::{DocumentManager, DocumentState};
use noctua_core::document::session::CollectionKind;
use noctua_core::document::session::command::SessionCommand;
use noctua_core::document::session::data::CollectionKind;
use std::path::PathBuf;
// ── ANSI helpers ──
@ -139,7 +139,7 @@ fn main() {
exec(
&mut mgr,
&format!("New session '{session_name}'"),
&format!("New browser session '{session_name}'"),
SessionCommand::New {
name: session_name.clone(),
kind: CollectionKind::DirectoryBrowser,
@ -154,7 +154,7 @@ fn main() {
);
exec(
&mut mgr,
&format!("Scan directory {}", dir.display()),
&format!("Add directory {}", dir.display()),
SessionCommand::AddDirectory { dir },
);
exec(
@ -186,7 +186,7 @@ fn main() {
);
exec(
&mut mgr,
&format!("Scan directory {}", path.display()),
&format!("Add directory {}", path.display()),
SessionCommand::AddDirectory { dir: path },
);
}

View file

@ -1,51 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/command.rs
//
// Command enums for session management and document operations.
// Commands that operate on the active document itself.
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, …).
/// Commands operating on the active document (zoom, export, annotate, …).
///
/// The `DocumentManager` dispatches these after resolving which document is
/// active the UI never needs to know the underlying format.
#[derive(Debug, Clone)]
pub enum DocumentCommand {
// Future: Rotate, Flip, Zoom, Export, ...
// Reserved for future use: Rotate, Flip, Zoom, Export, Annotate, …
}

View file

@ -1,38 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/format.rs
//
// Single source of truth for document format detection.
use std::path::Path;
/// The three document formats Noctua understands.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocumentFormat {
/// Raster images anything libcosmic's image crate can decode.
Raster,
/// Vector graphics (SVG).
Vector,
/// Portable documents (PDF).
Portable,
}
/// Detects the document format from the file extension.
///
/// Returns `None` for unknown extensions so directory scans can skip
/// unsupported files early. At load time the underlying library
/// (ImageReader, usvg, pdfium) is the final authority.
pub fn detect_format(path: &Path) -> Option<DocumentFormat> {
match path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.as_deref()
{
Some("pdf") => Some(DocumentFormat::Portable),
Some("svg") => Some(DocumentFormat::Vector),
Some("png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "tiff" | "tif") => {
Some(DocumentFormat::Raster)
}
_ => None,
}
}

View file

@ -1,21 +1,56 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/loader.rs
//
// Document loading: format dispatch and directory scanning.
// Document loading: format detection, format dispatch, and directory scanning.
use crate::document::format::{detect_format, DocumentFormat};
use crate::document::portable::PortableModel;
use crate::document::raster::RasterModel;
use crate::document::session::{SessionData, SessionItem};
use crate::document::vector::VectorModel;
use crate::document::{core::PageLayout, DocumentContent};
use crate::document::model::{PortableModel, RasterModel, VectorModel};
use crate::document::session::data::{SessionData, SessionItem};
use crate::document::types::PageLayout;
use crate::document::DocumentContent;
use crate::error::{Error, Result};
use std::path::Path;
/// The three document formats Noctua understands.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DocumentFormat {
/// Raster images anything the image crate can decode.
Raster,
/// Vector graphics (SVG).
Vector,
/// Portable documents (PDF).
Portable,
}
/// Detects the document format from the file extension.
///
/// Returns `None` for unknown extensions so directory scans can skip
/// unsupported files early. At load time the underlying library
/// (`ImageReader`, `usvg`, `pdfium`) is the final authority.
fn detect_format(path: &Path) -> Option<DocumentFormat> {
match path
.extension()
.and_then(|e| e.to_str())
.map(str::to_lowercase)
.as_deref()
{
Some("pdf") => Some(DocumentFormat::Portable),
Some("svg") => Some(DocumentFormat::Vector),
Some("png" | "jpg" | "jpeg" | "webp" | "gif" | "bmp" | "tiff" | "tif") => {
Some(DocumentFormat::Raster)
}
_ => None,
}
}
/// Loads a document from a path and returns its content.
pub fn load_document(path: &Path) -> Result<DocumentContent> {
///
/// # Errors
///
/// Returns [`Error::NotFound`] if the path does not exist.
/// Returns [`Error::Unsupported`] if the file extension is not recognised.
pub(crate) fn load_document(path: &Path) -> Result<DocumentContent> {
if !path.exists() {
return Err(Error::NotFound(format!("{:?}", path)));
return Err(Error::NotFound(format!("{}", path.display())));
}
match detect_format(path) {
@ -51,10 +86,10 @@ pub fn load_document(path: &Path) -> Result<DocumentContent> {
}
/// Scans a directory for supported files and populates the session.
pub fn scan_directory_into(session: &mut SessionData, dir: &Path) {
pub(crate) fn scan_directory_into(session: &mut SessionData, dir: &Path) {
if let Ok(entries) = std::fs::read_dir(dir) {
let mut paths: Vec<_> = entries
.filter_map(|e| e.ok())
.filter_map(std::result::Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file() && detect_format(p).is_some())
.collect();

View file

@ -1,25 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/mod.rs
//
// Document abstraction and format definitions.
// Document abstraction the public surface of the document subsystem.
pub mod command;
pub mod core;
pub mod format;
pub mod loader;
pub mod manager;
pub mod portable;
pub mod raster;
pub mod model;
pub mod session;
pub mod store;
pub mod vector;
pub mod types;
use self::portable::PortableModel;
use self::raster::RasterModel;
use self::vector::VectorModel;
use self::model::{PortableModel, RasterModel, VectorModel};
/// Represents the physical content of a loaded document.
#[derive(Debug, Clone)]
/// The decoded content of a loaded document.
///
/// This enum is internal to `noctua_core`. The UI only ever sees a `RawImage`
/// produced by the render subsystem it never matches on this type.
#[derive(Debug)]
pub enum DocumentContent {
Raster(RasterModel),
Vector(VectorModel),

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/model/mod.rs
//
// The three concrete document models Noctua understands.
pub mod portable;
pub mod raster;
pub mod vector;
pub use portable::PortableModel;
pub use raster::RasterModel;
pub use vector::VectorModel;

View file

@ -1,9 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/portable.rs
// src/document/model/portable.rs
//
// Portable document format model (PDF).
use crate::document::core::PageLayout;
use crate::document::types::PageLayout;
use std::path::PathBuf;
#[derive(Clone, Debug)]

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/raster.rs
// src/document/model/raster.rs
//
// Raster image document model (JPEG, PNG, etc.).

View file

@ -1,9 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/vector.rs
// src/document/model/vector.rs
//
// Vector image document model (SVG).
use crate::document::core::ViewBox;
use crate::document::types::ViewBox;
use std::path::PathBuf;
#[derive(Clone, Debug)]

View file

@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/session/command.rs
//
// 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.
#[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 },
}

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/session.rs
// 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.
@ -42,6 +42,7 @@ pub struct SessionItem {
}
impl SessionItem {
#[must_use]
pub fn new(path: PathBuf, page_index: usize) -> Self {
Self { path, page_index }
}
@ -52,8 +53,8 @@ impl SessionItem {
pub struct SessionData {
pub name: String,
pub kind: CollectionKind,
/// For DirectoryBrowser: the source directory to scan.
/// For DocumentCollection: None (items are managed individually).
/// For `DirectoryBrowser`: the source directory to scan.
/// For `DocumentCollection`: None (items are managed individually).
/// The .ron session file path is tracked separately.
#[serde(default)]
pub path: Option<PathBuf>,
@ -63,6 +64,7 @@ pub struct SessionData {
}
impl SessionData {
#[must_use]
pub fn new(
name: &str,
kind: CollectionKind,

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/session/mod.rs
//
// Session subsystem: data model, commands, and persistence.
pub mod command;
pub mod data;
pub mod store;
pub use command::SessionCommand;
pub use data::{CollectionKind, SessionData, SessionItem};

View file

@ -1,5 +1,5 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/store.rs
// src/document/session/store.rs
//
// RON-based persistence for session data: load and save session lists.
//
@ -27,23 +27,37 @@
// ),
// ]
use crate::document::session::SessionData;
use crate::document::session::data::SessionData;
use crate::error::{Error, Result};
use std::path::Path;
/// Loads all sessions from a RON file.
///
/// # Errors
///
/// Returns [`Error::Io`] if the file cannot be read.
/// Returns [`Error::Session`] if the file cannot be parsed.
pub fn load_sessions(path: &Path) -> Result<Vec<SessionData>> {
let content = std::fs::read_to_string(path).map_err(Error::Io)?;
ron::from_str(&content)
.map_err(|e| Error::Session(format!("Failed to parse session file {:?}: {}", path, e)))
ron::from_str(&content).map_err(|e| {
Error::Session(format!(
"Failed to parse session file {}: {e}",
path.display()
))
})
}
/// Saves all sessions to a RON file.
///
/// # Errors
///
/// 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)))?;
.map_err(|e| Error::Session(format!("Failed to serialize sessions: {e}")))?;
std::fs::write(path, content).map_err(Error::Io)
}

View file

@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/types/image.rs
//
// Raw pixel buffer passed from the render subsystem to the UI.
use std::sync::Arc;
/// A decoded, format-independent pixel buffer ready for display.
///
/// The UI receives this type from the `DocumentManager` and never needs to
/// know which document format (PDF, SVG, raster) produced it.
#[derive(Clone, Debug)]
pub struct RawImage {
pub data: Arc<Vec<u8>>,
pub width: u32,
pub height: u32,
}

View file

@ -1,17 +1,13 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/core.rs
// src/document/types/layout.rs
//
// Core domain models for document representation.
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct RawImage {
pub data: Arc<Vec<u8>>,
pub width: u32,
pub height: u32,
}
// PageLayout describes the vertical arrangement of pages in a document.
/// The computed layout of all pages in a multi-page document.
///
/// Stores absolute y-offsets for each page so the render subsystem can
/// determine which pages are visible at a given scroll position without
/// iterating the full list on every frame.
#[derive(Clone, Debug)]
pub struct PageLayout {
pub page_starts: Vec<f64>,
@ -21,15 +17,8 @@ pub struct PageLayout {
pub max_width: f64,
}
#[derive(Clone, Debug)]
pub struct ViewBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl PageLayout {
#[must_use]
pub fn new(page_sizes: Vec<(f64, f64)>, gap: f64) -> Self {
let mut page_starts = Vec::with_capacity(page_sizes.len());
let mut y = 0.0_f64;
@ -43,7 +32,7 @@ impl PageLayout {
}
}
let total_height = if !page_sizes.is_empty() { y - gap } else { 0.0 };
let total_height = if page_sizes.is_empty() { 0.0 } else { y - gap };
Self {
page_starts,

View file

@ -0,0 +1,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/types/mod.rs
//
// Shared domain primitives used across the document and render subsystems.
pub mod image;
pub mod layout;
pub mod viewbox;
pub use image::RawImage;
pub use layout::PageLayout;
pub use viewbox::ViewBox;

View file

@ -0,0 +1,16 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/document/types/viewbox.rs
//
// ViewBox describes the visible coordinate space of a vector document.
/// The coordinate space declared by an SVG `viewBox` attribute.
///
/// Used by the render subsystem to map document coordinates to screen pixels
/// without exposing SVG internals to the UI.
#[derive(Clone, Debug)]
pub struct ViewBox {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}

View file

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/error/kinds.rs
// src/error.rs
//
// Central error types for Noctua.
// Central error and result types for noctua_core.
use std::fmt;
@ -24,11 +24,11 @@ pub enum Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::NotFound(msg) => write!(f, "Not found: {}", msg),
Error::Unsupported(msg) => write!(f, "Unsupported: {}", msg),
Error::Render(msg) => write!(f, "Render error: {}", msg),
Error::Session(msg) => write!(f, "Session error: {}", msg),
Error::Io(err) => write!(f, "I/O error: {}", err),
Error::NotFound(msg) => write!(f, "Not found: {msg}"),
Error::Unsupported(msg) => write!(f, "Unsupported: {msg}"),
Error::Render(msg) => write!(f, "Render error: {msg}"),
Error::Session(msg) => write!(f, "Session error: {msg}"),
Error::Io(err) => write!(f, "I/O error: {err}"),
}
}
}

View file

@ -1,8 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/error/mod.rs
//
// Central error module re-exports the shared Error and Result types.
mod kinds;
pub use kinds::{Error, Result};

View file

@ -3,6 +3,12 @@
//
// Public API of the noctua_core library.
pub mod document;
pub mod error;
pub mod document;
pub mod render;
pub use document::command::DocumentCommand;
pub use document::manager::DocumentManager;
pub use document::session::SessionCommand;
pub use document::types::RawImage;

View file

@ -4,5 +4,4 @@
// Rendering subsystem: converts document content into displayable output.
pub mod converter;
pub mod page;
pub mod thumbnail;

View file

@ -1,4 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/render/page.rs
//
// Page layout and rendering primitives (scaffold not yet implemented).