// SPDX-License-Identifier: GPL-3.0-or-later // examples/cli.rs // // CLI test harness for the noctua_core document foundation. 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; // ANSI helpers const RESET: &str = "\x1b[0m"; const BOLD: &str = "\x1b[1m"; const DIM: &str = "\x1b[2m"; const GREEN: &str = "\x1b[32m"; const YELLOW: &str = "\x1b[33m"; const RED: &str = "\x1b[31m"; const CYAN: &str = "\x1b[36m"; const MAGENTA: &str = "\x1b[35m"; fn heading(text: &str) { println!("\n{BOLD}{CYAN}── {text} ──{RESET}"); } fn step(text: &str) { println!(" {DIM}>{RESET} {text}"); } fn ok(text: &str) { println!(" {GREEN}Ok{RESET} {text}"); } fn err(text: &str) { eprintln!(" {RED}X{RESET} {text}"); } fn info(text: &str) { println!(" {YELLOW}i{RESET} {text}"); } fn print_session(session: &SessionData, active_doc_index: Option) { let kind_label = match session.kind { CollectionKind::DirectoryBrowser => "DirectoryBrowser", CollectionKind::DocumentCollection => "DocumentCollection", }; println!( " {MAGENTA}Session: {BOLD}{}{RESET} {DIM}({kind_label}, {} items, active: {:?}){RESET}", session.name, session.items.len(), active_doc_index, ); for (i, item) in session.items.iter().enumerate() { let marker = if active_doc_index == Some(i) { format!("{GREEN}=>{RESET}") } else { format!("{DIM} {RESET}") }; let name = item .path .file_name() .and_then(|n| n.to_str()) .unwrap_or("?"); println!( " {marker} {DIM}[{i}]{RESET} {name} {DIM}(page {}){RESET}", item.page_index ); } } fn exec(manager: &mut DocumentManager, label: &str, command: SessionCommand) { step(label); manager.handle(command); if let DocumentState::Error(e) = &manager.state { err(&format!("{e}")); manager.state = DocumentState::Empty; } } fn print_active_session(mgr: &DocumentManager) { if let Some(session) = mgr.active_session() { print_session(session, mgr.active_document_index()); } else { info("No active session"); } } #[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 { /// Open a document (raster, vector, portable) #[arg(long, value_name = "PATH")] open_document: Option, /// Open a directory #[arg(long, value_name = "PATH")] open_directory: Option, /// Open a session file (.ron) #[arg(long, value_name = "PATH")] open_session: Option, } fn main() { env_logger::init(); let cli = Opt::parse(); let mut mgr = DocumentManager::new(); // Open source heading("Opening source"); if let Some(path) = cli.open_session { exec( &mut mgr, "Open session file", SessionCommand::OpenSession { path }, ); } else if let Some(path) = cli.open_document { let dir = path .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| PathBuf::from(".")); let session_name = dir .file_name() .and_then(|n| n.to_str()) .unwrap_or("Browse") .to_string(); exec( &mut mgr, &format!("New session '{session_name}'"), SessionCommand::NewSession { name: session_name.clone(), }, ); exec( &mut mgr, &format!("Select '{session_name}'"), SessionCommand::SelectSession { name: session_name.clone(), }, ); exec( &mut mgr, &format!("Add directory {}", dir.display()), SessionCommand::AddDirectoryToSession { dir }, ); exec( &mut mgr, &format!("Add document {}", path.display()), SessionCommand::AddDocumentToSession { path }, ); } else if let Some(path) = cli.open_directory { let session_name = path .file_name() .and_then(|n| n.to_str()) .unwrap_or("Browse") .to_string(); exec( &mut mgr, &format!("New session '{session_name}'"), SessionCommand::NewSession { name: session_name.clone(), }, ); exec( &mut mgr, &format!("Select '{session_name}'"), SessionCommand::SelectSession { name: session_name.clone(), }, ); exec( &mut 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 heading("Navigation"); exec(&mut mgr, "NextDocument", SessionCommand::NextDocument); ok(&format!( "After NextDocument → index {:?}", mgr.active_document_index() )); exec( &mut mgr, "PreviousDocument", SessionCommand::PreviousDocument, ); ok(&format!( "After PreviousDocument → index {:?}", mgr.active_document_index() )); exec( &mut mgr, "SelectDocument { index: 0 }", SessionCommand::SelectDocument { index: 0 }, ); ok(&format!( "After SelectDocument(0) → index {:?}", mgr.active_document_index() )); // Move operations heading("Move operations"); step("Before MoveDocumentDown:"); print_active_session(&mgr); exec( &mut mgr, "MoveDocumentDown", SessionCommand::MoveDocumentDown, ); ok(&format!( "After MoveDocumentDown → index {:?}", mgr.active_document_index() )); exec(&mut mgr, "MoveDocumentUp", SessionCommand::MoveDocumentUp); ok(&format!( "After MoveDocumentUp → index {:?}", mgr.active_document_index() )); // Copy / Paste heading("Copy / Paste"); exec( &mut mgr, "New session 'Favorites'", SessionCommand::NewSession { name: "Favorites".to_string(), }, ); exec( &mut mgr, "New session 'Workspace'", SessionCommand::NewSession { name: "Workspace".to_string(), }, ); step("Favorites before paste:"); if let Some(session) = mgr.session_by_name("Favorites") { print_session(session, None); } // SelectSession source → SelectDocument → CopyDocument → SelectSession target → PasteDocument exec( &mut mgr, &format!("SelectSession '{active_name}'"), SessionCommand::SelectSession { name: active_name.clone(), }, ); exec( &mut mgr, "SelectDocument { index: 0 }", SessionCommand::SelectDocument { index: 0 }, ); exec(&mut mgr, "CopyDocument", SessionCommand::CopyDocument); exec( &mut mgr, "SelectSession 'Favorites'", SessionCommand::SelectSession { name: "Favorites".to_string(), }, ); exec(&mut mgr, "PasteDocument", SessionCommand::PasteDocument); step("Favorites after paste:"); print_active_session(&mgr); // Move pasted item to front via MoveDocumentTo exec( &mut mgr, "MoveDocumentTo { index: 0 }", SessionCommand::MoveDocumentTo { index: 0 }, ); step("Favorites after MoveDocumentTo(0):"); print_active_session(&mgr); // Remove active document exec(&mut mgr, "RemoveDocument", SessionCommand::RemoveDocument); step("Favorites after RemoveDocument:"); print_active_session(&mgr); // Session operations heading("Session operations"); exec( &mut mgr, &format!("SelectSession '{active_name}'"), SessionCommand::SelectSession { name: active_name.clone(), }, ); exec( &mut mgr, "DuplicateSession 'Snapshot'", SessionCommand::DuplicateSession { new_name: "Snapshot".to_string(), }, ); step("Snapshot session:"); if let Some(session) = mgr.session_by_name("Snapshot") { print_session(session, None); } exec( &mut mgr, "SelectSession 'Workspace'", SessionCommand::SelectSession { name: "Workspace".to_string(), }, ); exec( &mut mgr, "RenameSession 'Archive'", SessionCommand::RenameSession { name: "Archive".to_string(), }, ); 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 marker = if is_active { format!("{GREEN}*{RESET}") } else { format!("{DIM} {RESET}") }; println!(" {marker} {}", session.name); } exec( &mut mgr, "CloseSession (closes 'Archive')", SessionCommand::CloseSession, ); step("All sessions after close:"); for session in mgr.sessions() { println!(" {DIM}-{RESET} {}", session.name); } heading("Done"); }