feat(printers): options d'impression par defaut (format, qualite, recto-verso)
Le tiroir de details charge les capacites reelles de la file (lpoptions -l) et expose des menus deroulants PageSize, cupsPrintQuality et Duplex. Chaque selection est persistee comme defaut systeme de la file via lpadmin -o puis relue. Les options non supportees par un pilote sont masquees.
This commit is contained in:
parent
aec716fa6a
commit
8d528f9e45
4 changed files with 256 additions and 3 deletions
|
|
@ -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<String>,
|
||||
pub media_default: Option<String>,
|
||||
pub qualities: Vec<String>,
|
||||
pub quality_default: Option<String>,
|
||||
pub sides: Vec<String>,
|
||||
pub sides_default: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn printer_options(name: &str) -> Result<PrinterOptions, String> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ pub struct Page {
|
|||
printers: Vec<backend::Printer>,
|
||||
jobs: Vec<backend::Job>,
|
||||
selected: Option<String>,
|
||||
options: Option<(String, backend::PrinterOptions)>,
|
||||
notice: Option<String>,
|
||||
dialog: Option<Dialog>,
|
||||
add: AddFlow,
|
||||
|
|
@ -65,6 +66,13 @@ pub enum Message {
|
|||
CancelDialog,
|
||||
CancelJob(i32),
|
||||
OperationDone(Result<(), String>),
|
||||
OptionsLoaded(String, Result<backend::PrinterOptions, String>),
|
||||
SetOption {
|
||||
printer: String,
|
||||
option: String,
|
||||
value: String,
|
||||
},
|
||||
Surface(cosmic::surface::Action),
|
||||
OpenAdd,
|
||||
Devices(Result<Vec<backend::Device>, String>),
|
||||
Drivers(Result<Vec<backend::Driver>, 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<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("printers", "printer-symbolic")
|
||||
|
|
@ -137,6 +163,7 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
|
||||
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<crate::Message> = 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<cosmic::Task<crate::Message>> =
|
||||
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<crate::Message> = cosmic::task::message(
|
||||
|
|
@ -378,6 +432,13 @@ impl Page {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
fn fetch_options(name: String) -> Task<crate::pages::Message> {
|
||||
cosmic::task::future(async move {
|
||||
Message::OptionsLoaded(name.clone(), backend::printer_options(&name).await)
|
||||
})
|
||||
.map(crate::pages::Message::Printers)
|
||||
}
|
||||
|
||||
fn reload(&self) -> Task<crate::pages::Message> {
|
||||
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<String> = 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<String> = 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()
|
||||
|
|
|
|||
10
i18n/en/cosmic_settings.ftl
vendored
10
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
i18n/fr/cosmic_settings.ftl
vendored
10
i18n/fr/cosmic_settings.ftl
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue