diff --git a/.gitignore b/.gitignore index f0973f6..0e76d10 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,3 @@ debian/* !debian/install !debian/rules !debian/source -CLAUDE.md diff --git a/src/document/command.rs b/src/document/command.rs index 7f0ab61..7fe0ee1 100644 --- a/src/document/command.rs +++ b/src/document/command.rs @@ -3,13 +3,11 @@ // // Commands that operate on the active document itself. -/// Commands operating on the active document (zoom, export, print, …). +/// 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 { - /// Print the active document to the system printer. - Print, // Reserved for future use: Rotate, Flip, Zoom, Export, Annotate, … } diff --git a/src/main.rs b/src/main.rs index 375c042..0478017 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ use std::ffi::OsStr; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::process::Command; -use std::time::Duration; #[derive(Parser, Debug)] #[command( @@ -29,7 +28,7 @@ struct Args { #[arg(value_name = "FILE")] files: Vec, - /// Compatibility with the previous harness. + /// Compatibility with the previous CLI harness. #[arg(long = "open-document", value_name = "PATH")] document: Option, @@ -38,7 +37,7 @@ struct Args { directory: Option, } -#[derive(Clone, Debug, Copy, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] enum DocumentKind { Raster, Svg, @@ -111,20 +110,6 @@ enum Message { page: usize, result: Result, }, - // Print messages - Print, - PrintTask { - result: Result<(), String>, - }, - ClearPrintStatus, - KeyChar(Key), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -enum PrintStatus { - Sending, - Sent, - Failed(String), } struct NoctuaApp { @@ -133,8 +118,6 @@ struct NoctuaApp { current: usize, zoom: f32, modifiers: keyboard::Modifiers, - printing: bool, - print_status: Option, } fn main() -> cosmic::iced::Result { @@ -197,8 +180,6 @@ impl Application for NoctuaApp { current, zoom: 1.0, modifiers: keyboard::Modifiers::default(), - printing: false, - print_status: None, }; app.refresh_title(); @@ -294,36 +275,6 @@ impl Application for NoctuaApp { } } } - // Print handling - Message::Print => return self.start_print(), - Message::KeyChar(key) => { - // Handle Ctrl+P shortcut for printing - if self.modifiers.control() && key == Key::Character("p".into()) { - return self.start_print(); - } - } - Message::PrintTask { result } => { - self.printing = false; - match result { - Ok(()) => { - self.print_status = Some(PrintStatus::Sent); - return Task::perform( - tokio::task::spawn_blocking(|| { - std::thread::sleep(Duration::from_secs(4)); - }), - |_| cosmic::Action::App(Message::ClearPrintStatus), - ); - } - Err(err) => { - self.print_status = Some(PrintStatus::Failed(err)); - } - } - } - Message::ClearPrintStatus => { - if matches!(self.print_status, Some(PrintStatus::Sent)) { - self.print_status = None; - } - } } self.refresh_title(); @@ -336,10 +287,6 @@ impl Application for NoctuaApp { Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { Some(Message::ModifiersChanged(modifiers)) } - Event::Keyboard(keyboard::Event::KeyPressed { - key: Key::Character(c), - .. - }) if c == "p" => Some(Message::KeyChar(Key::Character(c))), Event::Keyboard(keyboard::Event::KeyPressed { key: Key::Named(Named::PageUp | Named::ArrowUp | Named::ArrowLeft), .. @@ -373,7 +320,6 @@ impl Application for NoctuaApp { } fn footer(&self) -> Option> { - #[allow(clippy::question_mark)] let Some(document) = self.current_document() else { return None; }; @@ -395,7 +341,7 @@ impl Application for NoctuaApp { format!("{} / {}", self.current + 1, self.documents.len()) }; - let row = widget::row::with_capacity(10) + let row = widget::row::with_capacity(9) .align_y(Alignment::Center) .spacing(8) .padding(8) @@ -420,12 +366,6 @@ impl Application for NoctuaApp { .width(Length::Fixed(36.0)), ) .push(widget::space::horizontal().width(Length::Fill)) - .push( - widget::button::standard("Print") - .leading_icon(widget::icon::from_name("printer-symbolic").size(16).handle()) - .on_press_maybe((!self.printing).then_some(Message::Print)), - ) - .push(print_status_view(self.print_status.as_ref())) .push(widget::text::body(page_label)) .push( widget::button::standard("Prev page") @@ -436,33 +376,7 @@ impl Application for NoctuaApp { .on_press_maybe(has_next_page.then_some(Message::NextPage)), ); - Some( - widget::container(row) - .width(Length::Fill) - .class(theme::Container::custom(|t| { - let cosmic = t.cosmic(); - let background = cosmic.background(false); - cosmic::iced_widget::container::Style { - icon_color: Some(background.on.into()), - text_color: Some(background.on.into()), - background: Some(cosmic::iced::Background::Color( - background.base.into(), - )), - border: cosmic::iced::Border { - radius: [ - 0.0, - 0.0, - cosmic.corner_radii.radius_s[2], - cosmic.corner_radii.radius_s[3], - ] - .into(), - ..Default::default() - }, - ..Default::default() - } - })) - .into(), - ) + Some(row.into()) } } @@ -514,32 +428,6 @@ impl NoctuaApp { self.core.set_header_title(title); } - fn start_print(&mut self) -> Task { - if self.printing { - return Task::none(); - } - - let document = match self.current_document() { - Some(doc) => doc, - None => return Task::none(), - }; - let path = document.path.clone(); - let kind = document.kind; - - self.printing = true; - self.print_status = Some(PrintStatus::Sending); - - Task::perform( - tokio::task::spawn_blocking(move || print_document_sync(&path, kind)), - |result| { - let inner = result - .map_err(|e| format!("Spawn error: {e}")) - .and_then(|r| r); - cosmic::Action::App(Message::PrintTask { result: inner }) - }, - ) - } - fn ensure_current_rendered(&mut self) -> Task { let render_task = { let Some(document) = self.current_document_mut() else { @@ -663,25 +551,6 @@ fn empty_view<'a>(message: impl Into) -> Element<'a, Message> { .into() } -fn print_status_view(status: Option<&PrintStatus>) -> Element<'_, Message> { - match status { - Some(PrintStatus::Sending) => widget::progress_bar::indeterminate_circular() - .size(16.0) - .bar_height(2.0) - .into(), - Some(PrintStatus::Sent) => widget::text::body("Sent to printer") - .class(theme::Text::Accent) - .into(), - Some(PrintStatus::Failed(err)) => widget::text::body(format!("Print failed: {err}")) - .class(theme::Text::Custom(|t| cosmic::iced_widget::text::Style { - color: Some(t.cosmic().destructive_color().into()), - ..Default::default() - })) - .into(), - None => widget::space::horizontal().width(Length::Fixed(0.0)).into(), - } -} - fn scroll_delta_y(delta: mouse::ScrollDelta) -> Option { let y = match delta { mouse::ScrollDelta::Lines { y, .. } => y, @@ -863,151 +732,3 @@ fn path_hash(path: &Path) -> u64 { hasher.finish() } - -// ─── Print service ────────────────────────────────────────────────────────── - -/// Print the currently active document to the system printer via CUPS (lp). -/// -/// - **PDF** → sent directly to CUPS via `lp` -/// - **Raster images** → converted to PDF via `img2pdf` (normalized to -/// opaque 8-bit when needed, or `convert` as fallback) -/// - **SVG** → converted to PDF via `pdftocairo` → `img2pdf` -fn print_document_sync(path: &Path, kind: DocumentKind) -> Result<(), String> { - let pdf_path = match kind { - DocumentKind::Pdf => path.to_path_buf(), - DocumentKind::Raster => convert_raster_to_pdf(path)?, - DocumentKind::Svg => convert_svg_to_pdf(path)?, - }; - - send_to_printer(&pdf_path) -} - -fn convert_raster_to_pdf(path: &Path) -> Result { - // Try img2pdf first (cleaner, no quality loss) - if let Ok(result) = convert_with_cmd("img2pdf", vec![path.to_string_lossy().into_owned()], path) - { - return Ok(result); - } - - // img2pdf refuses alpha channels and >8 bit channels: normalize to - // opaque 8-bit and retry before falling back to ImageMagick. - if let Ok(normalized) = normalize_raster(path) { - let result = convert_with_cmd( - "img2pdf", - vec![normalized.to_string_lossy().into_owned()], - path, - ); - let _ = std::fs::remove_file(&normalized); - if let Ok(result) = result { - return Ok(result); - } - } - - // Fallback: ImageMagick convert - convert_with_cmd("convert", vec![path.to_string_lossy().into_owned()], path) -} - -fn normalize_raster(path: &Path) -> Result { - let extension = path.extension().and_then(OsStr::to_str).unwrap_or("png"); - let output_path = temp_prefix(path, "normalized").with_extension(extension); - - let status = Command::new("convert") - .arg(path) - .args(["-depth", "8", "-alpha", "off"]) - .arg(&output_path) - .status() - .map_err(|e| format!("Failed to run convert: {e}"))?; - if !status.success() { - return Err("convert failed".to_string()); - } - - if output_path.exists() { - Ok(output_path) - } else { - Err("convert did not produce output".to_string()) - } -} - -fn convert_svg_to_pdf(path: &Path) -> Result { - // Render SVG to PNG with pdftocairo, then convert PNG to PDF - let prefix = temp_prefix(path, "svg"); - let status = Command::new("pdftocairo") - .arg("-png") - .arg("-singlefile") - .arg("-f") - .arg("1") - .arg("-l") - .arg("1") - .arg("-scale-to") - .arg("2400") - .arg(path) - .arg(&prefix) - .status() - .map_err(|e| format!("Failed to run pdftocairo: {e}"))?; - - if !status.success() { - return Err("Failed to render SVG with pdftocairo".to_string()); - } - - let png_path = PathBuf::from(format!("{}.png", prefix.display())); - if !png_path.exists() { - return Err("pdftocairo did not produce output".to_string()); - } - - convert_raster_to_pdf(&png_path) -} - -fn convert_with_cmd( - cmd_name: &str, - args: Vec, - source_path: &Path, -) -> Result { - let output_path = temp_prefix(source_path, "print"); - - let mut cmd = Command::new(cmd_name); - if cmd_name == "img2pdf" { - cmd.args(&args).arg("-o").arg(&output_path); - } else { - // For convert, the last arg is the output - if !args.is_empty() { - cmd.args(&args[..args.len().saturating_sub(1)]) - .arg(&output_path); - } - } - - let status = cmd - .status() - .map_err(|e| format!("Failed to run {}: {e}", cmd_name))?; - if !status.success() { - return Err(format!("{} failed", cmd_name)); - } - - if output_path.exists() { - Ok(output_path) - } else { - Err(format!("{} did not produce output", cmd_name)) - } -} - -fn send_to_printer(pdf_path: &Path) -> Result<(), String> { - let status = Command::new("lp") - .arg(pdf_path) - .status() - .map_err(|e| format!("Failed to run lp: {e}"))?; - - if status.success() { - Ok(()) - } else { - Err("Failed to send document to printer".to_string()) - } -} - -fn temp_prefix(source_path: &Path, prefix: &str) -> PathBuf { - std::env::temp_dir() - .join(format!( - "noctua-print-{}-{}", - path_hash(source_path), - prefix - )) - .with_extension("pdf") -}