diff --git a/Cargo.toml b/Cargo.toml index 0a74905..d771507 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,73 @@ # SPDX-License-Identifier: GPL-3.0-or-later # Cargo.toml # -# Workspace manifest for the Noctua project. +# Noctua COSMIC desktop application -[workspace] -members = ["core", "ui"] -resolver = "2" +[package] +name = "noctua" +version = "0.1.0" +edition = "2021" +description = "Noctua – a document viewer for the COSMIC™ desktop" +repository = "https://codeberg.org/wfx/noctua" +authors = ["Wolfgang Morawetz "] +license = "GPL-3.0-or-later" + +[features] +default = ["image", "vector", "portable"] +image = ["dep:image", "dep:kamadak-exif"] +vector = ["dep:resvg"] +portable = ["dep:pdfium-render"] +full = ["image", "vector", "portable"] + +[dependencies] +# Serialization +serde = { version = "1", features = ["derive"] } +ron = "0.8" + +# Feature-gated document backends +image = { version = "0.25", optional = true } +kamadak-exif = { version = "0.5", optional = true } +pdfium-render = { version = "0.8", optional = true } +resvg = { version = "0.45", optional = true } + +# Logging +log = "0.4" +env_logger = "0.11" + +# i18n / localization +i18n-embed = { version = "0.16", features = [ + "fluent-system", + "desktop-requester", +] } +i18n-embed-fl = "0.10" + +# Resource embedding +rust-embed = "8" + +# Open files/URLs with the system default application +open = "5" + +# Thumbnail cache (freedesktop.org spec) +dirs = "5.0" +md5 = "0.7" +png = "0.17" + +# CLI argument parsing +clap = { version = "4", features = ["derive"] } + +# Async runtime (required by libcosmic) +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } + +[dependencies.libcosmic] +git = "https://github.com/pop-os/libcosmic.git" +features = [ + "a11y", + "about", + "dbus-config", + "multi-window", + "single-instance", + "tokio", + "winit", + "wayland", + "wgpu", +] diff --git a/core/Cargo.toml b/core/Cargo.toml deleted file mode 100644 index d582fe8..0000000 --- a/core/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# core/Cargo.toml -# -# Core library crate – no UI dependencies. - -[package] -name = "noctua_core" -version = "0.1.0" -edition = "2021" -description = "Document core library for the Noctua viewer" -repository = "https://codeberg.org/wfx/noctua" -authors = ["Wolfgang Morawetz "] -license = "GPL-3.0-or-later" - -[lib] -name = "noctua_core" -path = "src/lib.rs" - -[features] -default = ["image", "vector", "portable"] -image = ["dep:image", "dep:kamadak-exif"] -vector = ["dep:resvg"] -portable = ["dep:pdfium-render"] -full = ["image", "vector", "portable"] - -[dependencies] -# Serialization -serde = { version = "1", features = ["derive"] } -ron = "0.8" - -# Feature-gated document backends -image = { version = "0.25", optional = true } -kamadak-exif = { version = "0.5", optional = true } -pdfium-render = { version = "0.8", optional = true } -resvg = { version = "0.45", optional = true } - -# Logging interface -log = "0.4" - -# Thumbnail cache (freedesktop.org spec) -dirs = "5.0" -md5 = "0.7" -png = "0.17" - -[dev-dependencies] -# CLI test harness (examples/cli.rs only) -clap = { version = "4", features = ["derive"] } -env_logger = "0.11" diff --git a/core/src/lib.rs b/core/src/lib.rs deleted file mode 100644 index b337937..0000000 --- a/core/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/lib.rs -// -// Public API of the noctua_core library. - -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; diff --git a/justfile b/justfile index c04c9d0..65263c9 100644 --- a/justfile +++ b/justfile @@ -60,13 +60,9 @@ check-json: (check '--message-format=json') test *args: cargo test --workspace {{args}} -# Runs the COSMIC UI application +# Runs the application (no args = COSMIC UI, with args = CLI mode) run *args: - env RUST_BACKTRACE=full cargo run --release -p noctua_ui {{args}} - -# Runs the CLI test harness (core only, no UI deps) -run-cli *args: - env RUST_BACKTRACE=full cargo run --example cli -p noctua_core {{args}} + env RUST_BACKTRACE=full cargo run --release {{args}} # Installs files install: diff --git a/core/src/document/command.rs b/src/document/command.rs similarity index 100% rename from core/src/document/command.rs rename to src/document/command.rs diff --git a/core/src/document/loader.rs b/src/document/loader.rs similarity index 100% rename from core/src/document/loader.rs rename to src/document/loader.rs diff --git a/core/src/document/manager.rs b/src/document/manager.rs similarity index 100% rename from core/src/document/manager.rs rename to src/document/manager.rs diff --git a/core/src/document/mod.rs b/src/document/mod.rs similarity index 87% rename from core/src/document/mod.rs rename to src/document/mod.rs index c719136..f620821 100644 --- a/core/src/document/mod.rs +++ b/src/document/mod.rs @@ -14,7 +14,7 @@ use self::model::{PortableModel, RasterModel, VectorModel}; /// The decoded content of a loaded document. /// -/// This enum is internal to `noctua_core`. The UI only ever sees a `RawImage` +/// This enum is internal to Noctua. The UI only ever sees a `RawImage` /// produced by the render subsystem – it never matches on this type. #[derive(Debug)] pub enum DocumentContent { diff --git a/core/src/document/model/mod.rs b/src/document/model/mod.rs similarity index 100% rename from core/src/document/model/mod.rs rename to src/document/model/mod.rs diff --git a/core/src/document/model/portable.rs b/src/document/model/portable.rs similarity index 100% rename from core/src/document/model/portable.rs rename to src/document/model/portable.rs diff --git a/core/src/document/model/raster.rs b/src/document/model/raster.rs similarity index 100% rename from core/src/document/model/raster.rs rename to src/document/model/raster.rs diff --git a/core/src/document/model/vector.rs b/src/document/model/vector.rs similarity index 100% rename from core/src/document/model/vector.rs rename to src/document/model/vector.rs diff --git a/core/src/document/session/command.rs b/src/document/session/command.rs similarity index 100% rename from core/src/document/session/command.rs rename to src/document/session/command.rs diff --git a/core/src/document/session/data.rs b/src/document/session/data.rs similarity index 100% rename from core/src/document/session/data.rs rename to src/document/session/data.rs diff --git a/core/src/document/session/mod.rs b/src/document/session/mod.rs similarity index 67% rename from core/src/document/session/mod.rs rename to src/document/session/mod.rs index 1e82946..5d25ca6 100644 --- a/core/src/document/session/mod.rs +++ b/src/document/session/mod.rs @@ -6,6 +6,3 @@ pub mod command; pub mod data; pub mod store; - -pub use command::SessionCommand; -pub use data::{CollectionKind, SessionData, SessionItem}; diff --git a/core/src/document/session/store.rs b/src/document/session/store.rs similarity index 100% rename from core/src/document/session/store.rs rename to src/document/session/store.rs diff --git a/core/src/document/types/image.rs b/src/document/types/image.rs similarity index 100% rename from core/src/document/types/image.rs rename to src/document/types/image.rs diff --git a/core/src/document/types/layout.rs b/src/document/types/layout.rs similarity index 100% rename from core/src/document/types/layout.rs rename to src/document/types/layout.rs diff --git a/core/src/document/types/mod.rs b/src/document/types/mod.rs similarity index 91% rename from core/src/document/types/mod.rs rename to src/document/types/mod.rs index eae29b1..7a5f836 100644 --- a/core/src/document/types/mod.rs +++ b/src/document/types/mod.rs @@ -7,6 +7,5 @@ pub mod image; pub mod layout; pub mod viewbox; -pub use image::RawImage; pub use layout::PageLayout; pub use viewbox::ViewBox; diff --git a/core/src/document/types/viewbox.rs b/src/document/types/viewbox.rs similarity index 100% rename from core/src/document/types/viewbox.rs rename to src/document/types/viewbox.rs diff --git a/core/src/error.rs b/src/error.rs similarity index 86% rename from core/src/error.rs rename to src/error.rs index c17566b..402a70a 100644 --- a/core/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // src/error.rs // -// Central error and result types for noctua_core. +// Central error and result types for Noctua. use std::fmt; @@ -13,8 +13,6 @@ pub enum Error { NotFound(String), /// The file format or operation is not supported. Unsupported(String), - /// A rendering operation failed. - Render(String), /// A session could not be loaded or saved. Session(String), /// An underlying I/O error. @@ -26,7 +24,6 @@ impl fmt::Display for Error { 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}"), } diff --git a/core/examples/cli.rs b/src/main.rs similarity index 72% rename from core/examples/cli.rs rename to src/main.rs index 7d4edb3..da53220 100644 --- a/core/examples/cli.rs +++ b/src/main.rs @@ -1,13 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// examples/cli.rs +// src/main.rs // -// CLI test harness for the noctua_core document foundation. +// Entry point for the Noctua COSMIC application. -use clap::{ArgGroup, Parser}; -use noctua_core::document::manager::{DocumentManager, DocumentState}; -use noctua_core::document::session::command::SessionCommand; -use noctua_core::document::session::data::{CollectionKind, SessionData}; -use std::path::PathBuf; +mod document; +mod error; +mod render; + +use clap::Parser; +use document::manager::{DocumentManager, DocumentState}; +use document::session::command::SessionCommand; +use document::session::data::{CollectionKind, SessionData}; +use std::path::{Path, PathBuf}; // ANSI helpers @@ -87,50 +91,45 @@ fn print_active_session(mgr: &DocumentManager) { } #[derive(Parser, Debug)] -#[command( - name = "noctua", - about = "A document viewer for the COSMIC desktop", - group( - ArgGroup::new("source") - .required(true) - .args(["open_document", "open_directory", "open_session"]) - ) -)] -struct Opt { +#[command(name = "noctua", about = "A document viewer for the COSMIC desktop")] +struct Args { /// Open a document (raster, vector, portable) - #[arg(long, value_name = "PATH")] - open_document: Option, + #[arg(long = "open-document", value_name = "PATH")] + document: Option, /// Open a directory - #[arg(long, value_name = "PATH")] - open_directory: Option, + #[arg(long = "open-directory", value_name = "PATH")] + directory: Option, /// Open a session file (.ron) - #[arg(long, value_name = "PATH")] - open_session: Option, + #[arg(long = "open-session", value_name = "PATH")] + session: Option, } fn main() { env_logger::init(); - let cli = Opt::parse(); - let mut mgr = DocumentManager::new(); + let args = Args::parse(); - // Open source + if args.document.is_none() && args.directory.is_none() && args.session.is_none() { + // TODO: Launch COSMIC UI. + return; + } - heading("Opening source"); + run_cli(args); +} - if let Some(path) = cli.open_session { +fn open_source(mgr: &mut DocumentManager, args: Args) { + if let Some(path) = args.session { exec( - &mut mgr, + mgr, "Open session file", SessionCommand::OpenSession { path }, ); - } else if let Some(path) = cli.open_document { + } else if let Some(path) = args.document { let dir = path .parent() - .map(|p| p.to_path_buf()) - .unwrap_or_else(|| PathBuf::from(".")); + .map_or_else(|| PathBuf::from("."), Path::to_path_buf); let session_name = dir .file_name() @@ -139,30 +138,30 @@ fn main() { .to_string(); exec( - &mut mgr, + mgr, &format!("New session '{session_name}'"), SessionCommand::NewSession { name: session_name.clone(), }, ); exec( - &mut mgr, + mgr, &format!("Select '{session_name}'"), SessionCommand::SelectSession { name: session_name.clone(), }, ); exec( - &mut mgr, + mgr, &format!("Add directory {}", dir.display()), SessionCommand::AddDirectoryToSession { dir }, ); exec( - &mut mgr, + mgr, &format!("Add document {}", path.display()), SessionCommand::AddDocumentToSession { path }, ); - } else if let Some(path) = cli.open_directory { + } else if let Some(path) = args.directory { let session_name = path .file_name() .and_then(|n| n.to_str()) @@ -170,69 +169,44 @@ fn main() { .to_string(); exec( - &mut mgr, + mgr, &format!("New session '{session_name}'"), SessionCommand::NewSession { name: session_name.clone(), }, ); exec( - &mut mgr, + mgr, &format!("Select '{session_name}'"), SessionCommand::SelectSession { name: session_name.clone(), }, ); exec( - &mut mgr, + mgr, &format!("Add directory {}", path.display()), SessionCommand::AddDirectoryToSession { dir: path }, ); } +} - // State summary - - heading("State after open"); - - match &mgr.state { - DocumentState::Empty => info("No document loaded"), - DocumentState::Loading => info("Loading…"), - DocumentState::Error(e) => err(&format!("{e}")), - DocumentState::Loaded(_) => ok("Document loaded"), - } - - let active_name = match mgr.active_session().map(|s| s.name.clone()) { - Some(n) => n, - None => { - info("No active session – nothing more to test."); - return; - } - }; - - print_active_session(&mgr); - - // Navigation - +fn run_navigation_test(mgr: &mut DocumentManager) { heading("Navigation"); - exec(&mut mgr, "NextDocument", SessionCommand::NextDocument); + exec(mgr, "NextDocument", SessionCommand::NextDocument); ok(&format!( "After NextDocument → index {:?}", mgr.active_document_index() )); - exec( - &mut mgr, - "PreviousDocument", - SessionCommand::PreviousDocument, - ); + exec(mgr, "PreviousDocument", SessionCommand::PreviousDocument); ok(&format!( "After PreviousDocument → index {:?}", mgr.active_document_index() )); exec( - &mut mgr, + mgr, "SelectDocument { index: 0 }", SessionCommand::SelectDocument { index: 0 }, ); @@ -241,42 +215,36 @@ fn main() { mgr.active_document_index() )); - // Move operations - heading("Move operations"); step("Before MoveDocumentDown:"); - print_active_session(&mgr); + print_active_session(mgr); - exec( - &mut mgr, - "MoveDocumentDown", - SessionCommand::MoveDocumentDown, - ); + exec(mgr, "MoveDocumentDown", SessionCommand::MoveDocumentDown); ok(&format!( "After MoveDocumentDown → index {:?}", mgr.active_document_index() )); - exec(&mut mgr, "MoveDocumentUp", SessionCommand::MoveDocumentUp); + exec(mgr, "MoveDocumentUp", SessionCommand::MoveDocumentUp); ok(&format!( "After MoveDocumentUp → index {:?}", mgr.active_document_index() )); +} - // Copy / Paste - +fn run_copy_paste_test(mgr: &mut DocumentManager, active_name: &str) { heading("Copy / Paste"); exec( - &mut mgr, + mgr, "New session 'Favorites'", SessionCommand::NewSession { name: "Favorites".to_string(), }, ); exec( - &mut mgr, + mgr, "New session 'Workspace'", SessionCommand::NewSession { name: "Workspace".to_string(), @@ -288,61 +256,56 @@ fn main() { print_session(session, None); } - // SelectSession source → SelectDocument → CopyDocument → SelectSession target → PasteDocument exec( - &mut mgr, + mgr, &format!("SelectSession '{active_name}'"), SessionCommand::SelectSession { - name: active_name.clone(), + name: active_name.to_string(), }, ); exec( - &mut mgr, + mgr, "SelectDocument { index: 0 }", SessionCommand::SelectDocument { index: 0 }, ); - exec(&mut mgr, "CopyDocument", SessionCommand::CopyDocument); + exec(mgr, "CopyDocument", SessionCommand::CopyDocument); exec( - &mut mgr, + mgr, "SelectSession 'Favorites'", SessionCommand::SelectSession { name: "Favorites".to_string(), }, ); - exec(&mut mgr, "PasteDocument", SessionCommand::PasteDocument); + exec(mgr, "PasteDocument", SessionCommand::PasteDocument); step("Favorites after paste:"); - print_active_session(&mgr); - - // Move pasted item to front via MoveDocumentTo + print_active_session(mgr); exec( - &mut mgr, + mgr, "MoveDocumentTo { index: 0 }", SessionCommand::MoveDocumentTo { index: 0 }, ); step("Favorites after MoveDocumentTo(0):"); - print_active_session(&mgr); + print_active_session(mgr); - // Remove active document - - exec(&mut mgr, "RemoveDocument", SessionCommand::RemoveDocument); + exec(mgr, "RemoveDocument", SessionCommand::RemoveDocument); step("Favorites after RemoveDocument:"); - print_active_session(&mgr); - - // Session operations + print_active_session(mgr); +} +fn run_session_test(mgr: &mut DocumentManager, active_name: &str) { heading("Session operations"); exec( - &mut mgr, + mgr, &format!("SelectSession '{active_name}'"), SessionCommand::SelectSession { - name: active_name.clone(), + name: active_name.to_string(), }, ); exec( - &mut mgr, + mgr, "DuplicateSession 'Snapshot'", SessionCommand::DuplicateSession { new_name: "Snapshot".to_string(), @@ -355,14 +318,14 @@ fn main() { } exec( - &mut mgr, + mgr, "SelectSession 'Workspace'", SessionCommand::SelectSession { name: "Workspace".to_string(), }, ); exec( - &mut mgr, + mgr, "RenameSession 'Archive'", SessionCommand::RenameSession { name: "Archive".to_string(), @@ -371,9 +334,7 @@ fn main() { step("All sessions after rename:"); for session in mgr.sessions() { - let is_active = mgr - .active_session() - .map_or(false, |a| a.name == session.name); + let is_active = mgr.active_session().is_some_and(|a| a.name == session.name); let marker = if is_active { format!("{GREEN}*{RESET}") } else { @@ -383,7 +344,7 @@ fn main() { } exec( - &mut mgr, + mgr, "CloseSession (closes 'Archive')", SessionCommand::CloseSession, ); @@ -392,6 +353,33 @@ fn main() { for session in mgr.sessions() { println!(" {DIM}-{RESET} {}", session.name); } +} + +fn run_cli(args: Args) { + let mut mgr = DocumentManager::new(); + + heading("Opening source"); + open_source(&mut mgr, args); + + heading("State after open"); + + match &mgr.state { + DocumentState::Empty => info("No document loaded"), + DocumentState::Loading => info("Loading…"), + DocumentState::Error(e) => err(&format!("{e}")), + DocumentState::Loaded(_) => ok("Document loaded"), + } + + let Some(active_name) = mgr.active_session().map(|s| s.name.clone()) else { + info("No active session – nothing more to test."); + return; + }; + + print_active_session(&mgr); + + run_navigation_test(&mut mgr); + run_copy_paste_test(&mut mgr, &active_name); + run_session_test(&mut mgr, &active_name); heading("Done"); } diff --git a/core/src/render/converter.rs b/src/render/converter.rs similarity index 100% rename from core/src/render/converter.rs rename to src/render/converter.rs diff --git a/core/src/render/mod.rs b/src/render/mod.rs similarity index 100% rename from core/src/render/mod.rs rename to src/render/mod.rs diff --git a/core/src/render/thumbnail.rs b/src/render/thumbnail.rs similarity index 100% rename from core/src/render/thumbnail.rs rename to src/render/thumbnail.rs diff --git a/ui/Cargo.toml b/ui/Cargo.toml deleted file mode 100644 index 98bb336..0000000 --- a/ui/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later -# ui/Cargo.toml -# -# Binary crate for the Noctua COSMIC desktop application. - -[package] -name = "noctua" -version = "0.1.0" -edition = "2021" -description = "Noctua – a document viewer for the COSMIC™ desktop" -repository = "https://codeberg.org/wfx/noctua" -authors = ["Wolfgang Morawetz "] -license = "GPL-3.0-or-later" - -[dependencies] -noctua_core = { path = "../core" } - -# Logging -log = "0.4" -env_logger = "0.11" - -# i18n / localization -i18n-embed = { version = "0.16", features = [ - "fluent-system", - "desktop-requester", -] } -i18n-embed-fl = "0.10" - -# Resource embedding -rust-embed = "8" - -# Open files/URLs with the system default application -open = "5" - -# Async runtime (required by libcosmic) -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } - -[dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic.git" -features = [ - "a11y", - "about", - "dbus-config", - "multi-window", - "single-instance", - "tokio", - "winit", - "wayland", - "wgpu", -] diff --git a/ui/src/main.rs b/ui/src/main.rs deleted file mode 100644 index 7269760..0000000 --- a/ui/src/main.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/main.rs -// -// Entry point for the Noctua COSMIC application. - -fn main() { - // TODO: Initialize the COSMIC application. -}