diff --git a/cosmic-settings/src/pages/printers/backend.rs b/cosmic-settings/src/pages/printers/backend.rs index 4590e5d..055902c 100644 --- a/cosmic-settings/src/pages/printers/backend.rs +++ b/cosmic-settings/src/pages/printers/backend.rs @@ -200,6 +200,72 @@ pub async fn remove_printer(name: &str) -> Result<(), String> { } } +/// Default job options supported by a queue, with their current values. +/// +/// Values are taken from the queue's PPD view (`lpoptions -l`): `PageSize`, +/// `cupsPrintQuality` and `Duplex`, which `lpadmin -o` can persist. +#[derive(Clone, Debug, Default)] +pub struct PrinterOptions { + pub media: Vec, + pub media_default: Option, + pub qualities: Vec, + pub quality_default: Option, + pub sides: Vec, + pub sides_default: Option, +} + +pub async fn printer_options(name: &str) -> Result { + let output = run_cli_capture("lpoptions", &["-p", name, "-l"]).await?; + + let mut options = PrinterOptions::default(); + + for line in output.lines() { + let Some((key, values)) = line.split_once(':') else { + continue; + }; + let key = key.split('/').next().unwrap_or(key).trim(); + + let parsed: Vec<(String, bool)> = values + .split_whitespace() + .map(|value| { + ( + value.trim_start_matches('*').to_string(), + value.starts_with('*'), + ) + }) + .collect(); + + let (list, default) = match key { + "PageSize" => (&mut options.media, &mut options.media_default), + "cupsPrintQuality" => (&mut options.qualities, &mut options.quality_default), + "Duplex" => (&mut options.sides, &mut options.sides_default), + _ => continue, + }; + + for (value, selected) in parsed { + // Custom.WIDTHxHEIGHT placeholders are not selectable values. + if value.starts_with("Custom.") { + continue; + } + if selected { + *default = Some(value.clone()); + } + list.push(value); + } + } + + Ok(options) +} + +/// Set a queue-wide default job option (PageSize, cupsPrintQuality, Duplex…). +pub async fn set_default_option(printer: &str, option: &str, value: &str) -> Result<(), String> { + run_cli( + "lpadmin", + &["-p", printer, "-o", &format!("{option}={value}")], + ) + .await +} + pub async fn set_default(name: &str) -> Result<(), String> { run_cli("lpadmin", &["-d", name]).await } diff --git a/cosmic-settings/src/pages/printers/mod.rs b/cosmic-settings/src/pages/printers/mod.rs index 830716d..a180f95 100644 --- a/cosmic-settings/src/pages/printers/mod.rs +++ b/cosmic-settings/src/pages/printers/mod.rs @@ -46,6 +46,7 @@ pub struct Page { printers: Vec, jobs: Vec, selected: Option, + options: Option<(String, backend::PrinterOptions)>, notice: Option, dialog: Option, add: AddFlow, @@ -65,6 +66,13 @@ pub enum Message { CancelDialog, CancelJob(i32), OperationDone(Result<(), String>), + OptionsLoaded(String, Result), + SetOption { + printer: String, + option: String, + value: String, + }, + Surface(cosmic::surface::Action), OpenAdd, Devices(Result, String>), Drivers(Result, String>), @@ -112,6 +120,24 @@ fn printer_icon(printer: &backend::Printer) -> &'static str { } } +fn quality_label(value: &str) -> String { + match value.to_lowercase().as_str() { + "draft" => fl!("printers-quality-draft"), + "normal" => fl!("printers-quality-normal"), + "high" => fl!("printers-quality-high"), + _ => value.to_string(), + } +} + +fn sides_label(value: &str) -> String { + match value { + "None" => fl!("printers-sides-one-sided"), + "DuplexNoTumble" => fl!("printers-sides-long-edge"), + "DuplexTumble" => fl!("printers-sides-short-edge"), + _ => value.to_string(), + } +} + impl page::Page for Page { fn info(&self) -> page::Info { page::Info::new("printers", "printer-symbolic") @@ -137,6 +163,7 @@ impl page::Page for Page { fn on_context_drawer_close(&mut self) -> Task { self.add = AddFlow::default(); self.selected = None; + self.options = None; Task::none() } @@ -217,11 +244,12 @@ impl Page { } Message::Select(name) => { - self.selected = Some(name); + self.selected = Some(name.clone()); + self.options = None; let open_drawer: cosmic::Task = cosmic::task::message( crate::app::Message::OpenContextDrawer(self.entity), ); - return open_drawer; + return Task::batch([open_drawer, Self::fetch_options(name).map(Into::into)]); } Message::SetDefault(name) => { @@ -252,6 +280,7 @@ impl Page { return Task::none(); }; self.selected = None; + self.options = None; return Self::admin(async move { backend::remove_printer(&name).await }); } @@ -271,7 +300,31 @@ impl Page { if let Err(err) = result { self.notice = Some(fl!("printers-error", error = err)); } - return self.reload().map(Into::into); + let mut tasks: Vec> = + vec![self.reload().map(Into::into)]; + if let Some(selected) = self.selected.clone() { + tasks.push(Self::fetch_options(selected).map(Into::into)); + } + return Task::batch(tasks); + } + + Message::OptionsLoaded(name, result) => { + self.options = result.ok().map(|options| (name, options)); + } + + Message::SetOption { + printer, + option, + value, + } => { + self.notice = None; + return Self::admin(async move { + backend::set_default_option(&printer, &option, &value).await + }); + } + + Message::Surface(action) => { + return cosmic::task::message(crate::app::Message::Surface(action)); } Message::OpenAdd => { @@ -360,6 +413,7 @@ impl Page { Message::AddDone(result) => match result { Ok(name) => { self.add = AddFlow::default(); + self.options = None; self.notice = Some(fl!("printers-added", name = name)); let close_drawer: cosmic::Task = cosmic::task::message( @@ -378,6 +432,13 @@ impl Page { Task::none() } + fn fetch_options(name: String) -> Task { + cosmic::task::future(async move { + Message::OptionsLoaded(name.clone(), backend::printer_options(&name).await) + }) + .map(crate::pages::Message::Printers) + } + fn reload(&self) -> Task { Task::batch([ cosmic::task::future(async { @@ -487,6 +548,112 @@ impl Page { content = content.push(actions); + if let Some((options_name, options)) = self.options.as_ref() + && options_name == &printer.name + && (!options.media.is_empty() + || !options.qualities.is_empty() + || !options.sides.is_empty()) + { + content = content.push(text::body(fl!("printers-options"))); + + let mut list = widget::list_column(); + + if !options.media.is_empty() { + let values = options.media.clone(); + let selected = options + .media_default + .as_ref() + .and_then(|default| values.iter().position(|value| value == default)); + let labels = values.clone(); + let name = printer.name.clone(); + + list = list.add( + widget::settings::item::builder(fl!("printers-option-media")).control( + widget::dropdown::popup_dropdown( + labels, + selected, + move |index| Message::SetOption { + printer: name.clone(), + option: "PageSize".to_string(), + value: values.get(index).cloned().unwrap_or_default(), + }, + cosmic::iced::window::Id::RESERVED, + Message::Surface, + |action| { + crate::app::Message::PageMessage(crate::pages::Message::Printers( + action, + )) + }, + ), + ), + ); + } + + if !options.qualities.is_empty() { + let values = options.qualities.clone(); + let selected = options + .quality_default + .as_ref() + .and_then(|default| values.iter().position(|value| value == default)); + let labels: Vec = values.iter().map(|v| quality_label(v)).collect(); + let name = printer.name.clone(); + + list = list.add( + widget::settings::item::builder(fl!("printers-option-quality")).control( + widget::dropdown::popup_dropdown( + labels, + selected, + move |index| Message::SetOption { + printer: name.clone(), + option: "cupsPrintQuality".to_string(), + value: values.get(index).cloned().unwrap_or_default(), + }, + cosmic::iced::window::Id::RESERVED, + Message::Surface, + |action| { + crate::app::Message::PageMessage(crate::pages::Message::Printers( + action, + )) + }, + ), + ), + ); + } + + if !options.sides.is_empty() { + let values = options.sides.clone(); + let selected = options + .sides_default + .as_ref() + .and_then(|default| values.iter().position(|value| value == default)); + let labels: Vec = values.iter().map(|v| sides_label(v)).collect(); + let name = printer.name.clone(); + + list = list.add( + widget::settings::item::builder(fl!("printers-option-sides")).control( + widget::dropdown::popup_dropdown( + labels, + selected, + move |index| Message::SetOption { + printer: name.clone(), + option: "Duplex".to_string(), + value: values.get(index).cloned().unwrap_or_default(), + }, + cosmic::iced::window::Id::RESERVED, + Message::Surface, + |action| { + crate::app::Message::PageMessage(crate::pages::Message::Printers( + action, + )) + }, + ), + ), + ); + } + + content = content.push(list); + } + let jobs: Vec<&backend::Job> = self .jobs .iter() diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index f662da9..8bd4cbf 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -673,12 +673,22 @@ printers-no-matching-drivers = No matching drivers printers-no-printers = No printers configured printers-no-printers-desc = Printers connected to this computer or discovered on the network will appear here. printers-not-accepting = Not accepting jobs +printers-option-media = Paper format +printers-option-quality = Print quality +printers-option-sides = Two-sided printing +printers-options = Print options printers-pause = Pause +printers-quality-draft = Draft +printers-quality-high = High +printers-quality-normal = Normal printers-remove = Remove printer printers-remove-confirm = Remove this printer? printers-remove-confirm-button = Remove printers-remove-confirm-description = “{ $name }” will be removed from this system. Print jobs queued on it will be discarded. printers-resume = Resume +printers-sides-long-edge = Long edge +printers-sides-one-sided = One-sided +printers-sides-short-edge = Short edge printers-state = State printers-state-idle = Idle printers-state-processing = Printing diff --git a/i18n/fr/cosmic_settings.ftl b/i18n/fr/cosmic_settings.ftl index ae3797c..5ef94de 100644 --- a/i18n/fr/cosmic_settings.ftl +++ b/i18n/fr/cosmic_settings.ftl @@ -461,12 +461,22 @@ printers-no-matching-drivers = Aucun pilote ne correspond printers-no-printers = Aucune imprimante configurée printers-no-printers-desc = Les imprimantes connectées à cet ordinateur ou découvertes sur le réseau apparaîtront ici. printers-not-accepting = N'accepte pas les travaux +printers-option-media = Format du papier +printers-option-quality = Qualité d'impression +printers-option-sides = Impression recto-verso +printers-options = Options d'impression printers-pause = Suspendre +printers-quality-draft = Brouillon +printers-quality-high = Haute +printers-quality-normal = Normale printers-remove = Supprimer l'imprimante printers-remove-confirm = Supprimer cette imprimante ? printers-remove-confirm-button = Supprimer printers-remove-confirm-description = « { $name } » sera supprimée du système. Les travaux en attente sur cette imprimante seront abandonnés. printers-resume = Reprendre +printers-sides-long-edge = Bords longs +printers-sides-one-sided = Recto seul +printers-sides-short-edge = Bords courts printers-state = État printers-state-idle = Disponible printers-state-processing = Impression en cours