2026-03-07 20:29:18 +01:00
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
// examples/cli.rs
|
|
|
|
|
|
//
|
|
|
|
|
|
// CLI test harness for the noctua_core document foundation.
|
|
|
|
|
|
|
|
|
|
|
|
use clap::{ArgGroup, Parser};
|
2026-03-08 09:04:28 +01:00
|
|
|
|
use noctua_core::document::manager::{DocumentManager, DocumentState};
|
2026-03-08 17:41:05 +01:00
|
|
|
|
use noctua_core::document::session::command::SessionCommand;
|
2026-03-09 21:06:51 +01:00
|
|
|
|
use noctua_core::document::session::data::{CollectionKind, SessionData};
|
2026-03-07 20:29:18 +01:00
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
// ANSI helpers
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
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}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
fn print_session(session: &SessionData, active_doc_index: Option<usize>) {
|
|
|
|
|
|
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}")
|
2026-03-08 09:04:28 +01:00
|
|
|
|
};
|
2026-03-09 21:06:51 +01:00
|
|
|
|
let name = item
|
|
|
|
|
|
.path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.and_then(|n| n.to_str())
|
|
|
|
|
|
.unwrap_or("?");
|
2026-03-07 22:31:42 +01:00
|
|
|
|
println!(
|
2026-03-09 21:06:51 +01:00
|
|
|
|
" {marker} {DIM}[{i}]{RESET} {name} {DIM}(page {}){RESET}",
|
|
|
|
|
|
item.page_index
|
2026-03-07 22:31:42 +01:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 09:04:28 +01:00
|
|
|
|
fn exec(manager: &mut DocumentManager, label: &str, command: SessionCommand) {
|
|
|
|
|
|
step(label);
|
2026-03-07 22:31:42 +01:00
|
|
|
|
manager.handle(command);
|
2026-03-08 09:04:28 +01:00
|
|
|
|
if let DocumentState::Error(e) = &manager.state {
|
|
|
|
|
|
err(&format!("{e}"));
|
|
|
|
|
|
manager.state = DocumentState::Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
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");
|
2026-03-07 22:31:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 20:29:18 +01:00
|
|
|
|
#[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"])
|
|
|
|
|
|
)
|
|
|
|
|
|
)]
|
2026-03-08 09:04:28 +01:00
|
|
|
|
struct Opt {
|
|
|
|
|
|
/// Open a document (raster, vector, portable)
|
2026-03-07 20:29:18 +01:00
|
|
|
|
#[arg(long, value_name = "PATH")]
|
|
|
|
|
|
open_document: Option<PathBuf>,
|
|
|
|
|
|
|
|
|
|
|
|
/// Open a directory
|
|
|
|
|
|
#[arg(long, value_name = "PATH")]
|
|
|
|
|
|
open_directory: Option<PathBuf>,
|
|
|
|
|
|
|
|
|
|
|
|
/// Open a session file (.ron)
|
|
|
|
|
|
#[arg(long, value_name = "PATH")]
|
|
|
|
|
|
open_session: Option<PathBuf>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
|
env_logger::init();
|
|
|
|
|
|
|
2026-03-08 09:04:28 +01:00
|
|
|
|
let cli = Opt::parse();
|
|
|
|
|
|
let mut mgr = DocumentManager::new();
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
// Open source
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
heading("Opening source");
|
2026-03-07 20:29:18 +01:00
|
|
|
|
|
|
|
|
|
|
if let Some(path) = cli.open_session {
|
2026-03-09 21:06:51 +01:00
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
"Open session file",
|
|
|
|
|
|
SessionCommand::OpenSession { path },
|
|
|
|
|
|
);
|
2026-03-07 20:29:18 +01:00
|
|
|
|
} 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();
|
|
|
|
|
|
|
2026-03-08 09:04:28 +01:00
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
&format!("New session '{session_name}'"),
|
|
|
|
|
|
SessionCommand::NewSession {
|
2026-03-07 20:29:18 +01:00
|
|
|
|
name: session_name.clone(),
|
2026-03-08 09:04:28 +01:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
&format!("Select '{session_name}'"),
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::SelectSession {
|
2026-03-08 09:04:28 +01:00
|
|
|
|
name: session_name.clone(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-08 17:41:05 +01:00
|
|
|
|
&format!("Add directory {}", dir.display()),
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::AddDirectoryToSession { dir },
|
2026-03-08 09:04:28 +01:00
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
&format!("Add document {}", path.display()),
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::AddDocumentToSession { path },
|
2026-03-08 09:04:28 +01:00
|
|
|
|
);
|
2026-03-07 20:29:18 +01:00
|
|
|
|
} 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();
|
|
|
|
|
|
|
2026-03-08 09:04:28 +01:00
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
&format!("New session '{session_name}'"),
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::NewSession {
|
2026-03-07 20:29:18 +01:00
|
|
|
|
name: session_name.clone(),
|
2026-03-08 09:04:28 +01:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
&format!("Select '{session_name}'"),
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::SelectSession {
|
2026-03-08 09:04:28 +01:00
|
|
|
|
name: session_name.clone(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-08 17:41:05 +01:00
|
|
|
|
&format!("Add directory {}", path.display()),
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::AddDirectoryToSession { dir: path },
|
2026-03-08 09:04:28 +01:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
// State summary
|
2026-03-07 20:29:18 +01:00
|
|
|
|
|
2026-03-08 09:04:28 +01:00
|
|
|
|
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"),
|
2026-03-07 20:29:18 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
let active_name = match mgr.active_session().map(|s| s.name.clone()) {
|
2026-03-08 09:04:28 +01:00
|
|
|
|
Some(n) => n,
|
|
|
|
|
|
None => {
|
|
|
|
|
|
info("No active session – nothing more to test.");
|
|
|
|
|
|
return;
|
2026-03-07 20:29:18 +01:00
|
|
|
|
}
|
2026-03-08 09:04:28 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
print_active_session(&mgr);
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
// Navigation
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
heading("Navigation");
|
|
|
|
|
|
|
|
|
|
|
|
exec(&mut mgr, "NextDocument", SessionCommand::NextDocument);
|
2026-03-09 21:06:51 +01:00
|
|
|
|
ok(&format!(
|
|
|
|
|
|
"After NextDocument → index {:?}",
|
|
|
|
|
|
mgr.active_document_index()
|
|
|
|
|
|
));
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
"PreviousDocument",
|
|
|
|
|
|
SessionCommand::PreviousDocument,
|
|
|
|
|
|
);
|
2026-03-09 21:06:51 +01:00
|
|
|
|
ok(&format!(
|
|
|
|
|
|
"After PreviousDocument → index {:?}",
|
|
|
|
|
|
mgr.active_document_index()
|
|
|
|
|
|
));
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
"SelectDocument { index: 0 }",
|
|
|
|
|
|
SessionCommand::SelectDocument { index: 0 },
|
|
|
|
|
|
);
|
2026-03-09 21:06:51 +01:00
|
|
|
|
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()
|
|
|
|
|
|
));
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
// Copy / Paste
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
heading("Copy / Paste");
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
"New session 'Favorites'",
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::NewSession {
|
2026-03-08 09:04:28 +01:00
|
|
|
|
name: "Favorites".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
|
|
|
|
|
"New session 'Workspace'",
|
2026-03-09 21:06:51 +01:00
|
|
|
|
SessionCommand::NewSession {
|
2026-03-08 09:04:28 +01:00
|
|
|
|
name: "Workspace".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
step("Favorites before paste:");
|
|
|
|
|
|
if let Some(session) = mgr.session_by_name("Favorites") {
|
|
|
|
|
|
print_session(session, None);
|
|
|
|
|
|
}
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
// SelectSession source → SelectDocument → CopyDocument → SelectSession target → PasteDocument
|
2026-03-08 09:04:28 +01:00
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
&format!("SelectSession '{active_name}'"),
|
|
|
|
|
|
SessionCommand::SelectSession {
|
|
|
|
|
|
name: active_name.clone(),
|
2026-03-08 09:04:28 +01:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-03-09 21:06:51 +01:00
|
|
|
|
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
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
"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 {
|
2026-03-08 09:04:28 +01:00
|
|
|
|
name: active_name.clone(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
"DuplicateSession 'Snapshot'",
|
|
|
|
|
|
SessionCommand::DuplicateSession {
|
|
|
|
|
|
new_name: "Snapshot".to_string(),
|
|
|
|
|
|
},
|
2026-03-08 09:04:28 +01:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
step("Snapshot session:");
|
|
|
|
|
|
if let Some(session) = mgr.session_by_name("Snapshot") {
|
|
|
|
|
|
print_session(session, None);
|
|
|
|
|
|
}
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
"SelectSession 'Workspace'",
|
|
|
|
|
|
SessionCommand::SelectSession {
|
|
|
|
|
|
name: "Workspace".to_string(),
|
2026-03-08 09:04:28 +01:00
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
"RenameSession 'Archive'",
|
|
|
|
|
|
SessionCommand::RenameSession {
|
|
|
|
|
|
name: "Archive".to_string(),
|
2026-03-08 09:04:28 +01:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-03-09 21:06:51 +01:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 09:04:28 +01:00
|
|
|
|
exec(
|
|
|
|
|
|
&mut mgr,
|
2026-03-09 21:06:51 +01:00
|
|
|
|
"CloseSession (closes 'Archive')",
|
|
|
|
|
|
SessionCommand::CloseSession,
|
2026-03-08 09:04:28 +01:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-09 21:06:51 +01:00
|
|
|
|
step("All sessions after close:");
|
|
|
|
|
|
for session in mgr.sessions() {
|
|
|
|
|
|
println!(" {DIM}-{RESET} {}", session.name);
|
|
|
|
|
|
}
|
2026-03-08 09:04:28 +01:00
|
|
|
|
|
|
|
|
|
|
heading("Done");
|
2026-03-07 20:29:18 +01:00
|
|
|
|
}
|