diff --git a/src/main.rs b/src/main.rs index 943c93b..ddbbf6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,8 +10,6 @@ use cosmic::iced::{ keyboard::{self, key::Named, Key}, mouse, Alignment, ContentFit, Event, Length, Limits, Subscription, }; -use cosmic::widget::menu::action::MenuAction; -use cosmic::widget::menu::Item as MenuItem; use cosmic::{executor, theme, widget, Application, Element}; use std::collections::hash_map::DefaultHasher; use std::ffi::OsStr; @@ -117,25 +115,6 @@ enum Message { PrintTask { result: Result<(), String>, }, - About, -} - -/// Menu action type – only the variants that can appear in the menu. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum MenuActionType { - Print, - About, -} - -impl MenuAction for MenuActionType { - type Message = Message; - - fn message(&self) -> Self::Message { - match self { - MenuActionType::Print => Message::Print, - MenuActionType::About => Message::About, - } - } } struct NoctuaApp { @@ -342,12 +321,6 @@ impl Application for NoctuaApp { } } } - Message::About => { - // Simple about: show info in the document area - if let Some(document) = self.current_document_mut() { - document.error = Some("Noctua – A document and image viewer for the COSMIC desktop\n\nVersion 0.1.0\n© 2025 Wolfgang Morawetz\nLicensed under GPL-3.0-or-later".to_string()); - } - } } self.refresh_title(); @@ -377,18 +350,6 @@ impl Application for NoctuaApp { }) } - fn header_start(&self) -> Vec> { - let doc = self.current_document(); - let has_document = doc.is_some(); - - let menu_bar = widget::menu::bar(vec![widget::menu::Tree::with_children( - cosmic::widget::RcElementWrapper::new(Element::from(widget::menu::root("File"))), - widget::menu::items(&std::collections::HashMap::new(), menu_items(has_document)), - )]); - - vec![Element::from(menu_bar)] - } - fn view(&self) -> Element<'_, Self::Message> { let content = match self.current_document() { Some(document) => self.document_view(document), @@ -427,7 +388,7 @@ impl Application for NoctuaApp { format!("{} / {}", self.current + 1, self.documents.len()) }; - let row = widget::row::with_capacity(9) + let row = widget::row::with_capacity(10) .align_y(Alignment::Center) .spacing(8) .padding(8) @@ -452,6 +413,10 @@ impl Application for NoctuaApp { .width(Length::Fixed(36.0)), ) .push(widget::space::horizontal().width(Length::Fill)) + .push( + widget::button::standard("Print") + .on_press(Message::Print), + ) .push(widget::text::body(page_label)) .push( widget::button::standard("Prev page") @@ -466,31 +431,6 @@ impl Application for NoctuaApp { } } -fn menu_items(has_document: bool) -> Vec> { - let mut items = Vec::new(); - - // Print button (only when a document is open) - if has_document { - items.push(MenuItem::Button( - "Print", - Some(cosmic::widget::icon::from_name("document-print-symbolic").into()), - MenuActionType::Print, - )); - } - - // Separator - items.push(MenuItem::Divider); - - // About - items.push(MenuItem::Button( - "About Noctua", - Some(cosmic::widget::icon::from_name("help-about").into()), - MenuActionType::About, - )); - - items -} - impl NoctuaApp { fn zoom_in(&mut self) { self.zoom = (self.zoom * 1.2).min(5.0); diff --git a/src/print_service.rs b/src/print_service.rs deleted file mode 100644 index 64054f1..0000000 --- a/src/print_service.rs +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/print_service.rs -// -// Print service – sends documents to the system printer via CUPS (lp). -// -// Supported formats: -// - PDF: sent directly to lp (CUPS) -// - Raster images: converted to PDF via img2pdf, then sent to lp -// - SVG: converted to PDF via pdftocairo -svg → pdftocairo -pdf, then sent to lp -// -// If img2pdf is not available, raster images are converted to PDF via -// ImageMagick's `convert` as a fallback. - -use std::path::PathBuf; -use std::process::Command; - -/// Result of a print operation. -#[derive(Debug)] -pub enum PrintResult { - /// The document was sent to the printer successfully. - Success, - /// The operation was cancelled by the user. - Cancelled, - /// An error occurred during printing. - Error(String), -} - -/// Print the currently active document. -/// -/// For PDF files the document is sent directly to CUPS via `lp`. -/// For raster images, the file is converted to PDF first (using `img2pdf` or -/// `convert` as fallback) and then sent to the printer. -/// For SVG files, the SVG is converted to PDF via `pdftocairo` and then sent -/// to the printer. -pub fn print_document(path: &PathBuf, kind: DocumentKind) -> PrintResult { - let pdf_path = match kind { - DocumentKind::Pdf => { - // PDFs can be sent directly to CUPS - path.clone() - } - DocumentKind::Raster => { - // Raster images need conversion to PDF first - convert_raster_to_pdf(path) - } - DocumentKind::Svg => { - // SVG needs conversion to PDF first - convert_svg_to_pdf(path) - } - }; - - match pdf_path { - Ok(pdf_path) => send_to_printer(&pdf_path), - Err(e) => PrintResult::Error(e), - } -} - -/// Convert a raster image to a temporary PDF file. -/// -/// Uses `img2pdf` first, then falls back to `convert` (ImageMagick). -fn convert_raster_to_pdf(path: &PathBuf) -> 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); - } - - // Fallback to ImageMagick convert - convert_with_cmd("convert", vec![path.to_string_lossy().into_owned()], path) -} - -/// Convert an SVG to a temporary PDF file. -/// -/// Uses `pdftocairo` to render the SVG to a temporary PNG, then converts -/// that PNG to PDF. For a more direct approach, we try `svg2pdf` if available. -fn convert_svg_to_pdf(path: &PathBuf) -> Result { - // Try svg2pdf first (direct SVG → PDF) - if let Ok(result) = convert_with_cmd( - "svg2pdf", - vec![ - path.to_string_lossy().into_owned(), - "-".to_string(), - ], - path, - ) { - return Ok(result); - } - - // Fallback: render SVG to PNG with pdftocairo, then convert PNG to PDF - // First render SVG to PNG - 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)); - if !png_path.exists() { - return Err("pdftocairo did not produce output".to_string()); - } - - // Convert PNG to PDF - convert_with_cmd("img2pdf", vec![png_path.to_string_lossy().into_owned()], path) -} - -/// Convert a file to PDF using a command-line tool. -/// -/// Writes the output to a temporary file and returns its path. -fn convert_with_cmd( - cmd: &str, - args: Vec, - _source_path: &PathBuf, -) -> Result { - let status = Command::new(cmd) - .args(&args) - .output() - .map_err(|e| format!("Failed to run {cmd}: {e}"))?; - - if !status.status.success() { - let stderr = String::from_utf8_lossy(&status.stderr); - return Err(format!("{cmd} failed: {stderr}")); - } - - // img2pdf writes to stdout by default, convert writes to a file - // For img2pdf, we need to capture stdout - // For convert, the output is specified in args - // Let's handle both: - // - img2pdf: write to temp file - // - convert: find the output file - - // For img2pdf, we need to re-run with output redirect - // Actually, let's use a simpler approach: write to a known temp file - let output_path = temp_prefix(_source_path, "print"); - - // Re-run with explicit output - let mut cmd = Command::new(cmd); - if cmd.get_program().to_string() == "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 {cmd}: {e}"))?; - if !status.success() { - return Err(format!("{cmd} failed")); - } - - if output_path.exists() { - Ok(output_path) - } else { - Err(format!("{cmd} did not produce output")) - } -} - -/// Send a PDF file to the system printer via CUPS (lp command). -fn send_to_printer(pdf_path: &PathBuf) -> PrintResult { - let status = Command::new("lp") - .arg(pdf_path) - .status() - .map_err(|e| format!("Failed to run lp: {e}"))?; - - if status.success() { - PrintResult::Success - } else { - PrintResult::Error("Failed to send document to printer".to_string()) - } -} - -/// Generate a temporary file path for printing. -fn temp_prefix(source_path: &PathBuf, prefix: &str) -> PathBuf { - std::env::temp_dir() - .join(format!( - "noctua-print-{}-{}", - path_hash(source_path), - prefix - )) - .with_extension("pdf") -} - -/// Hash a path for use in temp file names. -fn path_hash(path: &PathBuf) -> u64 { - use std::hash::{Hash, Hasher}; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - path.hash(&mut hasher); - hasher.finish() -} - -/// The document kind for print operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DocumentKind { - Pdf, - Raster, - Svg, -}