feat(printers): page native de gestion des imprimantes (cups_rs + CUPS CLI)
Nouvelle page Imprimantes: liste et etat des files CUPS, ajout (decouverte lpinfo, pilote driverless IPP Everywhere par defaut), suppression, imprimante par defaut, suspension/reprise, page de test et file des travaux avec annulation. Lectures via cups_rs (FFI libcups serialisee), mutations via lpadmin/cupsenable. i18n en/fr, sous-commande 'cosmic-settings printers'.
This commit is contained in:
parent
f9b27c8d4d
commit
26dcaa6fd3
9 changed files with 1280 additions and 16 deletions
127
Cargo.lock
generated
127
Cargo.lock
generated
|
|
@ -278,7 +278,7 @@ version = "1.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -289,7 +289,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell_polyfill",
|
"once_cell_polyfill",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -648,7 +648,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
"nom",
|
"nom 8.0.0",
|
||||||
"num-rational",
|
"num-rational",
|
||||||
"v_frame",
|
"v_frame",
|
||||||
]
|
]
|
||||||
|
|
@ -692,6 +692,26 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bindgen"
|
||||||
|
version = "0.71.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools 0.13.0",
|
||||||
|
"log",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash 2.1.3",
|
||||||
|
"shlex 1.3.0",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
@ -1008,6 +1028,15 @@ dependencies = [
|
||||||
"shlex 2.0.1",
|
"shlex 2.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom 7.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfb"
|
name = "cfb"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
|
@ -1080,6 +1109,17 @@ dependencies = [
|
||||||
"inout",
|
"inout",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
|
|
@ -1511,6 +1551,7 @@ dependencies = [
|
||||||
"cosmic-settings-sound",
|
"cosmic-settings-sound",
|
||||||
"cosmic-settings-upower-subscription",
|
"cosmic-settings-upower-subscription",
|
||||||
"cosmic-settings-wallpaper",
|
"cosmic-settings-wallpaper",
|
||||||
|
"cups_rs",
|
||||||
"derive_setters",
|
"derive_setters",
|
||||||
"dirs",
|
"dirs",
|
||||||
"eyre",
|
"eyre",
|
||||||
|
|
@ -1527,7 +1568,7 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"intmap",
|
"intmap",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"itoa",
|
"itoa",
|
||||||
"jiff",
|
"jiff",
|
||||||
"libcosmic-yoda",
|
"libcosmic-yoda",
|
||||||
|
|
@ -1907,6 +1948,19 @@ dependencies = [
|
||||||
"uncased",
|
"uncased",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cups_rs"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ad179868ddceb8948487316c7094aa251e3efb0ecf15cab88b08f22eabf724"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"chrono",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cursor-icon"
|
name = "cursor-icon"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -2122,7 +2176,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2265,7 +2319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2595,7 +2649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc6d3a3635983a889f065aa9ce760384713f23a9b4a04f696f86c39a5d7a6a5a"
|
checksum = "fc6d3a3635983a889f065aa9ce760384713f23a9b4a04f696f86c39a5d7a6a5a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"nom",
|
"nom 8.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2821,6 +2875,12 @@ version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
|
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glow"
|
name = "glow"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
@ -3988,6 +4048,15 @@ version = "1.70.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -4363,7 +4432,7 @@ version = "1.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638"
|
checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4801,6 +4870,12 @@ dependencies = [
|
||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -4944,6 +5019,16 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "8.0.0"
|
version = "8.0.0"
|
||||||
|
|
@ -5001,7 +5086,7 @@ version = "0.50.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5749,6 +5834,16 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
|
|
@ -6003,7 +6098,7 @@ dependencies = [
|
||||||
"built",
|
"built",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"interpolate_name",
|
"interpolate_name",
|
||||||
"itertools",
|
"itertools 0.14.0",
|
||||||
"libc",
|
"libc",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -6361,7 +6456,7 @@ dependencies = [
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6830,7 +6925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7064,10 +7159,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.4.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7453,7 +7548,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -8123,7 +8218,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ cosmic-settings-sound = { path = "../pages/sound", optional = true }
|
||||||
cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true }
|
cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true }
|
||||||
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
|
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
|
||||||
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
|
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
|
||||||
|
cups_rs = { version = "0.3.0", optional = true }
|
||||||
derive_setters = "0.1.9"
|
derive_setters = "0.1.9"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
|
|
@ -130,6 +131,7 @@ linux = [
|
||||||
"page-legacy-applications",
|
"page-legacy-applications",
|
||||||
"page-networking",
|
"page-networking",
|
||||||
"page-power",
|
"page-power",
|
||||||
|
"page-printers",
|
||||||
"page-region",
|
"page-region",
|
||||||
"page-sound",
|
"page-sound",
|
||||||
"page-users",
|
"page-users",
|
||||||
|
|
@ -174,6 +176,7 @@ page-networking = [
|
||||||
"dep:zbus",
|
"dep:zbus",
|
||||||
]
|
]
|
||||||
page-power = ["dep:upower_dbus", "dep:zbus"]
|
page-power = ["dep:upower_dbus", "dep:zbus"]
|
||||||
|
page-printers = ["dep:cups_rs"]
|
||||||
page-region = [
|
page-region = [
|
||||||
"gettext",
|
"gettext",
|
||||||
"dep:locales-rs",
|
"dep:locales-rs",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ use crate::pages::input;
|
||||||
use crate::pages::networking;
|
use crate::pages::networking;
|
||||||
#[cfg(feature = "page-power")]
|
#[cfg(feature = "page-power")]
|
||||||
use crate::pages::power;
|
use crate::pages::power;
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
use crate::pages::printers;
|
||||||
#[cfg(feature = "page-sound")]
|
#[cfg(feature = "page-sound")]
|
||||||
use crate::pages::sound;
|
use crate::pages::sound;
|
||||||
use crate::pages::{self, applications, system, time};
|
use crate::pages::{self, applications, system, time};
|
||||||
|
|
@ -116,6 +118,8 @@ impl SettingsApp {
|
||||||
}
|
}
|
||||||
#[cfg(feature = "page-power")]
|
#[cfg(feature = "page-power")]
|
||||||
PageCommands::Power => self.pages.page_id::<power::Page>(),
|
PageCommands::Power => self.pages.page_id::<power::Page>(),
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
PageCommands::Printers => self.pages.page_id::<printers::Page>(),
|
||||||
#[cfg(feature = "page-region")]
|
#[cfg(feature = "page-region")]
|
||||||
PageCommands::RegionLanguage => self.pages.page_id::<time::region::Page>(),
|
PageCommands::RegionLanguage => self.pages.page_id::<time::region::Page>(),
|
||||||
#[cfg(feature = "page-sound")]
|
#[cfg(feature = "page-sound")]
|
||||||
|
|
@ -225,6 +229,8 @@ impl cosmic::Application for SettingsApp {
|
||||||
app.insert_page::<power::Page>();
|
app.insert_page::<power::Page>();
|
||||||
#[cfg(feature = "page-input")]
|
#[cfg(feature = "page-input")]
|
||||||
app.insert_page::<input::Page>();
|
app.insert_page::<input::Page>();
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
app.insert_page::<printers::Page>();
|
||||||
app.insert_page::<applications::Page>();
|
app.insert_page::<applications::Page>();
|
||||||
app.insert_page::<time::Page>();
|
app.insert_page::<time::Page>();
|
||||||
app.insert_page::<system::Page>();
|
app.insert_page::<system::Page>();
|
||||||
|
|
@ -654,6 +660,13 @@ impl cosmic::Application for SettingsApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
crate::pages::Message::Printers(message) => {
|
||||||
|
if let Some(page) = self.pages.page_mut::<printers::Page>() {
|
||||||
|
return page.update(message).map(Into::into);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "page-networking")]
|
#[cfg(feature = "page-networking")]
|
||||||
crate::pages::Message::Vpn(message) => {
|
crate::pages::Message::Vpn(message) => {
|
||||||
if let Some(page) = self.pages.page_mut::<networking::vpn::Page>() {
|
if let Some(page) = self.pages.page_mut::<networking::vpn::Page>() {
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ pub enum PageCommands {
|
||||||
/// Power settings page
|
/// Power settings page
|
||||||
#[cfg(feature = "page-power")]
|
#[cfg(feature = "page-power")]
|
||||||
Power,
|
Power,
|
||||||
|
/// Printers settings page
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
Printers,
|
||||||
/// Region & Language settings page
|
/// Region & Language settings page
|
||||||
#[cfg(feature = "page-region")]
|
#[cfg(feature = "page-region")]
|
||||||
RegionLanguage,
|
RegionLanguage,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ pub mod input;
|
||||||
pub mod networking;
|
pub mod networking;
|
||||||
#[cfg(feature = "page-power")]
|
#[cfg(feature = "page-power")]
|
||||||
pub mod power;
|
pub mod power;
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
pub mod printers;
|
||||||
#[cfg(feature = "page-sound")]
|
#[cfg(feature = "page-sound")]
|
||||||
pub mod sound;
|
pub mod sound;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
|
@ -82,6 +84,8 @@ pub enum Message {
|
||||||
PanelApplet(desktop::panel::applets_inner::Message),
|
PanelApplet(desktop::panel::applets_inner::Message),
|
||||||
#[cfg(feature = "page-power")]
|
#[cfg(feature = "page-power")]
|
||||||
Power(power::Message),
|
Power(power::Message),
|
||||||
|
#[cfg(feature = "page-printers")]
|
||||||
|
Printers(printers::Message),
|
||||||
#[cfg(feature = "page-region")]
|
#[cfg(feature = "page-region")]
|
||||||
Region(time::region::Message),
|
Region(time::region::Message),
|
||||||
#[cfg(feature = "page-sound")]
|
#[cfg(feature = "page-sound")]
|
||||||
|
|
|
||||||
285
cosmic-settings/src/pages/printers/backend.rs
Normal file
285
cosmic-settings/src/pages/printers/backend.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
// Copyright 2026 Lionel DARNIS <lionel@wopr.io>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
//! CUPS backend for the printers page.
|
||||||
|
//!
|
||||||
|
//! Read-only operations (destinations, jobs) use the `cups_rs` bindings to
|
||||||
|
//! libcups. Mutating operations (add, remove, default, pause) go through the
|
||||||
|
//! CUPS command line tools, which authenticate transparently against the
|
||||||
|
//! local scheduler for users in the CUPS system group.
|
||||||
|
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub use cups_rs::{JobStatus, PrinterState};
|
||||||
|
|
||||||
|
const TEST_PAGE: &str = "/usr/share/cups/data/testprint";
|
||||||
|
|
||||||
|
/// libcups keeps process-wide global state; serialize every FFI call.
|
||||||
|
static CUPS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||||
|
|
||||||
|
fn cups_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||||
|
CUPS_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A printer queue known to the CUPS scheduler.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Printer {
|
||||||
|
pub name: String,
|
||||||
|
pub info: String,
|
||||||
|
pub make_and_model: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub device_uri: Option<String>,
|
||||||
|
pub state: PrinterState,
|
||||||
|
pub state_reasons: Vec<String>,
|
||||||
|
pub accepting_jobs: bool,
|
||||||
|
pub is_default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An active print job.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Job {
|
||||||
|
pub id: i32,
|
||||||
|
pub title: String,
|
||||||
|
pub dest: String,
|
||||||
|
pub status: JobStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A device that a printer queue can be created for.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Device {
|
||||||
|
pub uri: String,
|
||||||
|
pub info: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A printer driver (PPD or driverless profile).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Driver {
|
||||||
|
pub id: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read operations (blocking FFI; call from a blocking task).
|
||||||
|
|
||||||
|
pub fn printers_sync() -> Result<Vec<Printer>, String> {
|
||||||
|
let _guard = cups_lock();
|
||||||
|
let destinations = cups_rs::get_all_destinations().map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
Ok(destinations
|
||||||
|
.iter()
|
||||||
|
.map(|dest| Printer {
|
||||||
|
name: dest.name.clone(),
|
||||||
|
info: dest
|
||||||
|
.info()
|
||||||
|
.filter(|info| !info.is_empty())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| dest.name.clone()),
|
||||||
|
make_and_model: dest.make_and_model().cloned(),
|
||||||
|
location: dest.location().cloned(),
|
||||||
|
device_uri: dest.device_uri().cloned(),
|
||||||
|
state: dest.state(),
|
||||||
|
state_reasons: dest
|
||||||
|
.state_reasons()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|reason| reason != "none")
|
||||||
|
.collect(),
|
||||||
|
accepting_jobs: dest.is_accepting_jobs(),
|
||||||
|
is_default: dest.is_default,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jobs_sync() -> Result<Vec<Job>, String> {
|
||||||
|
let _guard = cups_lock();
|
||||||
|
let jobs = cups_rs::get_active_jobs(None).map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
Ok(jobs
|
||||||
|
.into_iter()
|
||||||
|
.map(|job| Job {
|
||||||
|
id: job.id,
|
||||||
|
title: job.title,
|
||||||
|
dest: job.dest,
|
||||||
|
status: job.status,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_job_sync(job_id: i32) -> Result<(), String> {
|
||||||
|
let _guard = cups_lock();
|
||||||
|
cups_rs::job::cancel_job(job_id).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_test_page_sync(printer: &str) -> Result<(), String> {
|
||||||
|
let _guard = cups_lock();
|
||||||
|
let dest = cups_rs::get_destination(printer).map_err(|err| err.to_string())?;
|
||||||
|
let job = cups_rs::create_job(&dest, "COSMIC Settings test page")
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
job.submit_file(TEST_PAGE, "application/postscript")
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discovery and administration (CUPS command line tools).
|
||||||
|
|
||||||
|
pub async fn devices() -> Result<Vec<Device>, String> {
|
||||||
|
let output = run_cli_capture("lpinfo", &["-v"]).await?;
|
||||||
|
|
||||||
|
Ok(output
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let (_, uri) = line.split_once(char::is_whitespace)?;
|
||||||
|
let uri = uri.trim();
|
||||||
|
uri.contains("://").then(|| Device {
|
||||||
|
info: device_info(uri),
|
||||||
|
uri: uri.to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn drivers() -> Result<Vec<Driver>, String> {
|
||||||
|
let output = run_cli_capture("lpinfo", &["-m"]).await?;
|
||||||
|
|
||||||
|
let mut drivers: Vec<Driver> = output
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let (id, description) = line.split_once(char::is_whitespace)?;
|
||||||
|
Some(Driver {
|
||||||
|
id: id.trim().to_string(),
|
||||||
|
description: description.trim().to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Offer the driverless IPP Everywhere profile first.
|
||||||
|
drivers.sort_by_key(|driver| driver.id != "everywhere");
|
||||||
|
Ok(drivers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_printer(name: &str, uri: &str, driver: &str) -> Result<(), String> {
|
||||||
|
run_cli("lpadmin", &["-p", name, "-E", "-v", uri, "-m", driver]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_printer(name: &str) -> Result<(), String> {
|
||||||
|
run_cli("lpadmin", &["-x", name]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_default(name: &str) -> Result<(), String> {
|
||||||
|
run_cli("lpadmin", &["-d", name]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_enabled(name: &str, enabled: bool) -> Result<(), String> {
|
||||||
|
if enabled {
|
||||||
|
run_cli("cupsenable", &[name]).await
|
||||||
|
} else {
|
||||||
|
run_cli("cupsdisable", &[name]).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize a display name into a valid CUPS queue name and make it unique
|
||||||
|
/// against the current printer list.
|
||||||
|
pub fn unique_name(raw: &str, existing: &[Printer]) -> String {
|
||||||
|
let sanitized: String = raw
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if c.is_alphanumeric() || matches!(c, '-' | '_' | '.') {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let base = if sanitized.is_empty() {
|
||||||
|
"Printer".to_string()
|
||||||
|
} else {
|
||||||
|
sanitized
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut candidate = base.clone();
|
||||||
|
let mut index = 2;
|
||||||
|
while existing.iter().any(|printer| printer.name == candidate) {
|
||||||
|
candidate = format!("{base}_{index}");
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_cli(program: &str, args: &[&str]) -> Result<(), String> {
|
||||||
|
let output = run_cli_capture(program, args).await?;
|
||||||
|
drop(output);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_cli_capture(program: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
let output = Command::new(program)
|
||||||
|
.args(args)
|
||||||
|
.stdin(std::process::Stdio::null())
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("{program}: {err}"))?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore deprecation notices lpadmin prints on stderr for PPD drivers.
|
||||||
|
let message = stderr
|
||||||
|
.lines()
|
||||||
|
.filter(|line| !line.contains("deprecated"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
Err(if message.trim().is_empty() {
|
||||||
|
format!("{program} {}", args.join(" "))
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_info(uri: &str) -> String {
|
||||||
|
if let Some(rest) = uri.strip_prefix("dnssd://") {
|
||||||
|
let name = rest.split(['/', '?']).next().unwrap_or(rest);
|
||||||
|
let name = name.split("._").next().unwrap_or(name);
|
||||||
|
let decoded = percent_decode(name);
|
||||||
|
if !decoded.is_empty() {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(rest) = uri.strip_prefix("usb://") {
|
||||||
|
let path = rest.split('?').next().unwrap_or(rest);
|
||||||
|
let decoded = percent_decode(path).replace('/', " ");
|
||||||
|
if !decoded.trim().is_empty() {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uri.starts_with("cups-pdf") {
|
||||||
|
return "PDF".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
percent_decode(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percent_decode(input: &str) -> String {
|
||||||
|
let bytes = input.as_bytes();
|
||||||
|
let mut decoded = Vec::with_capacity(bytes.len());
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
|
while index < bytes.len() {
|
||||||
|
if bytes[index] == b'%' && index + 2 < bytes.len() {
|
||||||
|
let hex = &input[index + 1..index + 3];
|
||||||
|
if let Ok(value) = u8::from_str_radix(hex, 16) {
|
||||||
|
decoded.push(value);
|
||||||
|
index += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decoded.push(bytes[index]);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
String::from_utf8_lossy(&decoded).into_owned()
|
||||||
|
}
|
||||||
772
cosmic-settings/src/pages/printers/mod.rs
Normal file
772
cosmic-settings/src/pages/printers/mod.rs
Normal file
|
|
@ -0,0 +1,772 @@
|
||||||
|
// Copyright 2026 Lionel DARNIS <lionel@wopr.io>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
//! Printers page: manage CUPS queues, the default printer, and print jobs.
|
||||||
|
|
||||||
|
mod backend;
|
||||||
|
|
||||||
|
use cosmic::iced::{Alignment, Length, Subscription};
|
||||||
|
use cosmic::widget::{self, text};
|
||||||
|
use cosmic::{Apply, Element, Task, theme};
|
||||||
|
use cosmic_settings_page::{self as page, Section, section};
|
||||||
|
use slotmap::SlotMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use backend::{JobStatus, PrinterState};
|
||||||
|
|
||||||
|
const DRIVERLESS: &str = "everywhere";
|
||||||
|
const DRIVER_LIST_LIMIT: usize = 200;
|
||||||
|
const REFRESH_EVERY: Duration = Duration::from_secs(8);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AddFlow {
|
||||||
|
active: bool,
|
||||||
|
loading: bool,
|
||||||
|
submitting: bool,
|
||||||
|
devices: Vec<backend::Device>,
|
||||||
|
selected_device: Option<usize>,
|
||||||
|
name: String,
|
||||||
|
drivers: Vec<backend::Driver>,
|
||||||
|
driver_filter: String,
|
||||||
|
selected_driver: String,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Dialog {
|
||||||
|
RemovePrinter { name: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Page {
|
||||||
|
entity: page::Entity,
|
||||||
|
printers: Vec<backend::Printer>,
|
||||||
|
jobs: Vec<backend::Job>,
|
||||||
|
selected: Option<String>,
|
||||||
|
notice: Option<String>,
|
||||||
|
dialog: Option<Dialog>,
|
||||||
|
add: AddFlow,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Message {
|
||||||
|
Reload,
|
||||||
|
Printers(Result<Vec<backend::Printer>, String>),
|
||||||
|
Jobs(Result<Vec<backend::Job>, String>),
|
||||||
|
Select(Option<String>),
|
||||||
|
SetDefault(String),
|
||||||
|
ToggleEnabled(String, bool),
|
||||||
|
PrintTestPage(String),
|
||||||
|
Remove(String),
|
||||||
|
ConfirmRemove,
|
||||||
|
CancelDialog,
|
||||||
|
CancelJob(i32),
|
||||||
|
OperationDone(Result<(), String>),
|
||||||
|
OpenAdd,
|
||||||
|
Devices(Result<Vec<backend::Device>, String>),
|
||||||
|
Drivers(Result<Vec<backend::Driver>, String>),
|
||||||
|
SelectDevice(usize),
|
||||||
|
NameInput(String),
|
||||||
|
DriverFilter(String),
|
||||||
|
SelectDriver(String),
|
||||||
|
SubmitAdd,
|
||||||
|
AddDone(Result<String, String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Message> for crate::pages::Message {
|
||||||
|
fn from(message: Message) -> Self {
|
||||||
|
crate::pages::Message::Printers(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl page::Page<crate::pages::Message> for Page {
|
||||||
|
fn info(&self) -> page::Info {
|
||||||
|
page::Info::new("printers", "printer-symbolic")
|
||||||
|
.title(fl!("printers"))
|
||||||
|
.description(fl!("xdg-entry-printers-comment"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(
|
||||||
|
&self,
|
||||||
|
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||||
|
) -> Option<page::Content> {
|
||||||
|
Some(vec![
|
||||||
|
sections.insert(printers_section()),
|
||||||
|
sections.insert(selected_printer_section()),
|
||||||
|
sections.insert(jobs_section()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_id(&mut self, entity: page::Entity) {
|
||||||
|
self.entity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> Task<crate::pages::Message> {
|
||||||
|
self.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
|
||||||
|
self.add = AddFlow::default();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
|
||||||
|
let Dialog::RemovePrinter { name } = self.dialog.as_ref()?;
|
||||||
|
|
||||||
|
let confirm = widget::button::destructive(fl!("printers-remove"))
|
||||||
|
.on_press(crate::pages::Message::from(Message::ConfirmRemove));
|
||||||
|
|
||||||
|
let cancel = widget::button::standard(fl!("cancel"))
|
||||||
|
.on_press(crate::pages::Message::from(Message::CancelDialog));
|
||||||
|
|
||||||
|
Some(
|
||||||
|
widget::dialog()
|
||||||
|
.title(fl!("printers-remove-confirm"))
|
||||||
|
.body(fl!(
|
||||||
|
"printers-remove-confirm-description",
|
||||||
|
name = name.to_string()
|
||||||
|
))
|
||||||
|
.primary_action(confirm)
|
||||||
|
.secondary_action(cancel)
|
||||||
|
.apply(Element::from),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context_drawer(&self) -> Option<cosmic::app::ContextDrawer<'_, crate::pages::Message>> {
|
||||||
|
if !self.add.active {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
cosmic::app::context_drawer(
|
||||||
|
self.add_view().map(crate::pages::Message::Printers),
|
||||||
|
crate::pages::Message::CloseContextDrawer,
|
||||||
|
)
|
||||||
|
.title(fl!("printers-add")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscription(&self, _core: &cosmic::Core) -> Subscription<crate::pages::Message> {
|
||||||
|
cosmic::iced::time::every(REFRESH_EVERY)
|
||||||
|
.map(|_| crate::pages::Message::Printers(Message::Reload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Page {
|
||||||
|
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::Message> {
|
||||||
|
match message {
|
||||||
|
Message::Reload => return self.reload().map(Into::into),
|
||||||
|
|
||||||
|
Message::Printers(Ok(printers)) => {
|
||||||
|
if let Some(selected) = self.selected.as_deref()
|
||||||
|
&& !printers.iter().any(|printer| printer.name == selected)
|
||||||
|
{
|
||||||
|
self.selected = None;
|
||||||
|
}
|
||||||
|
self.printers = printers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Printers(Err(err)) | Message::Jobs(Err(err)) => {
|
||||||
|
self.notice = Some(fl!("printers-error", error = err));
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Jobs(Ok(jobs)) => {
|
||||||
|
self.jobs = jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Select(name) => {
|
||||||
|
self.selected = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SetDefault(name) => {
|
||||||
|
self.notice = None;
|
||||||
|
return Self::admin(async move { backend::set_default(&name).await });
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ToggleEnabled(name, enabled) => {
|
||||||
|
self.notice = None;
|
||||||
|
return Self::admin(async move { backend::set_enabled(&name, enabled).await });
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PrintTestPage(name) => {
|
||||||
|
self.notice = None;
|
||||||
|
return cosmic::task::future(async move {
|
||||||
|
Message::OperationDone(tokio::task::spawn_blocking(move || {
|
||||||
|
backend::print_test_page_sync(&name)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| Err(err.to_string())))
|
||||||
|
})
|
||||||
|
.map(crate::pages::Message::Printers)
|
||||||
|
.map(Into::into);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Remove(name) => {
|
||||||
|
self.dialog = Some(Dialog::RemovePrinter { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::CancelDialog => {
|
||||||
|
self.dialog = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ConfirmRemove => {
|
||||||
|
self.dialog = None;
|
||||||
|
if let Some(name) = self.selected_printer_name() {
|
||||||
|
return Self::admin(async move { backend::remove_printer(&name).await });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::CancelJob(id) => {
|
||||||
|
return cosmic::task::future(async move {
|
||||||
|
Message::OperationDone(tokio::task::spawn_blocking(move || {
|
||||||
|
backend::cancel_job_sync(id)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| Err(err.to_string())))
|
||||||
|
})
|
||||||
|
.map(crate::pages::Message::Printers)
|
||||||
|
.map(Into::into);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::OperationDone(result) => {
|
||||||
|
if let Err(err) = result {
|
||||||
|
self.notice = Some(fl!("printers-error", error = err));
|
||||||
|
}
|
||||||
|
return self.reload().map(Into::into);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::OpenAdd => {
|
||||||
|
self.add = AddFlow {
|
||||||
|
active: true,
|
||||||
|
loading: true,
|
||||||
|
selected_driver: DRIVERLESS.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_drawer: cosmic::Task<crate::Message> = cosmic::task::message(
|
||||||
|
crate::app::Message::OpenContextDrawer(self.entity),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Task::batch([
|
||||||
|
cosmic::task::future(async { Message::Devices(backend::devices().await) })
|
||||||
|
.map(crate::pages::Message::Printers)
|
||||||
|
.map(Into::into),
|
||||||
|
cosmic::task::future(async { Message::Drivers(backend::drivers().await) })
|
||||||
|
.map(crate::pages::Message::Printers)
|
||||||
|
.map(Into::into),
|
||||||
|
open_drawer,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Devices(Ok(devices)) => {
|
||||||
|
self.add.devices = devices;
|
||||||
|
self.add.loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Devices(Err(err)) => {
|
||||||
|
self.add.loading = false;
|
||||||
|
self.add.error = Some(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Drivers(Ok(drivers)) => {
|
||||||
|
self.add.drivers = drivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Drivers(Err(err)) => {
|
||||||
|
self.add.error = Some(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectDevice(index) => {
|
||||||
|
if let Some(device) = self.add.devices.get(index) {
|
||||||
|
self.add.name = backend::unique_name(&device.info, &self.printers);
|
||||||
|
self.add.selected_device = Some(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::NameInput(value) => {
|
||||||
|
self.add.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DriverFilter(value) => {
|
||||||
|
self.add.driver_filter = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectDriver(id) => {
|
||||||
|
self.add.selected_driver = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SubmitAdd => {
|
||||||
|
let Some(device) = self
|
||||||
|
.add
|
||||||
|
.selected_device
|
||||||
|
.and_then(|index| self.add.devices.get(index))
|
||||||
|
else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = backend::unique_name(self.add.name.trim(), &self.printers);
|
||||||
|
let uri = device.uri.clone();
|
||||||
|
let driver = self.add.selected_driver.clone();
|
||||||
|
self.add.submitting = true;
|
||||||
|
self.add.error = None;
|
||||||
|
|
||||||
|
return cosmic::task::future(async move {
|
||||||
|
Message::AddDone(backend::add_printer(&name, &uri, &driver).await.map(|_| name))
|
||||||
|
})
|
||||||
|
.map(crate::pages::Message::Printers)
|
||||||
|
.map(Into::into);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::AddDone(result) => match result {
|
||||||
|
Ok(name) => {
|
||||||
|
self.add = AddFlow::default();
|
||||||
|
self.selected = Some(name.clone());
|
||||||
|
self.notice = Some(fl!("printers-added", name = name));
|
||||||
|
|
||||||
|
let close_drawer: cosmic::Task<crate::Message> = cosmic::task::message(
|
||||||
|
crate::Message::from(crate::pages::Message::CloseContextDrawer),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Task::batch([self.reload().map(Into::into), close_drawer]);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.add.submitting = false;
|
||||||
|
self.add.error = Some(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_printer_name(&self) -> Option<String> {
|
||||||
|
// Removal is confirmed from the dialog; operate on the selected printer.
|
||||||
|
match self.dialog.as_ref() {
|
||||||
|
Some(Dialog::RemovePrinter { name }) => Some(name.clone()),
|
||||||
|
None => self.selected.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload(&self) -> Task<crate::pages::Message> {
|
||||||
|
Task::batch([
|
||||||
|
cosmic::task::future(async {
|
||||||
|
Message::Printers(
|
||||||
|
tokio::task::spawn_blocking(backend::printers_sync)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| Err(err.to_string())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(crate::pages::Message::Printers),
|
||||||
|
cosmic::task::future(async {
|
||||||
|
Message::Jobs(
|
||||||
|
tokio::task::spawn_blocking(backend::jobs_sync)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| Err(err.to_string())),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(crate::pages::Message::Printers),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin(
|
||||||
|
operation: impl std::future::Future<Output = Result<(), String>> + Send + 'static,
|
||||||
|
) -> cosmic::Task<crate::Message> {
|
||||||
|
cosmic::task::future(async move { Message::OperationDone(operation.await) })
|
||||||
|
.map(crate::pages::Message::Printers)
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_view(&self) -> Element<'_, Message> {
|
||||||
|
let spacing = theme::spacing();
|
||||||
|
let mut content = widget::column::with_children(Vec::new()).spacing(spacing.space_s);
|
||||||
|
|
||||||
|
content = content.push(text::body(fl!("printers-device")));
|
||||||
|
|
||||||
|
let mut devices = widget::list_column();
|
||||||
|
if self.add.loading {
|
||||||
|
devices = devices.add(text::caption(fl!("printers-loading")));
|
||||||
|
} else if self.add.devices.is_empty() {
|
||||||
|
devices = devices.add(text::caption(fl!("printers-no-devices")));
|
||||||
|
} else {
|
||||||
|
for (index, device) in self.add.devices.iter().enumerate() {
|
||||||
|
let selected = self.add.selected_device == Some(index);
|
||||||
|
let control: Element<'_, Message> = if selected {
|
||||||
|
widget::icon::from_name("object-select-symbolic")
|
||||||
|
.size(16)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
widget::space::horizontal()
|
||||||
|
.width(Length::Fixed(16.0))
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
devices = devices.add(
|
||||||
|
widget::list::button(
|
||||||
|
widget::settings::item::builder(device.info.to_string())
|
||||||
|
.description(device.uri.to_string())
|
||||||
|
.control(control),
|
||||||
|
)
|
||||||
|
.on_press(Message::SelectDevice(index)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.push(widget::scrollable(devices).height(Length::Fixed(220.0)));
|
||||||
|
|
||||||
|
content = content.push(
|
||||||
|
widget::text_input("", &self.add.name)
|
||||||
|
.label(fl!("printers-name"))
|
||||||
|
.on_input(Message::NameInput),
|
||||||
|
);
|
||||||
|
|
||||||
|
content = content.push(text::body(fl!("printers-driver")));
|
||||||
|
|
||||||
|
content = content.push(
|
||||||
|
widget::search_input("", &self.add.driver_filter)
|
||||||
|
.on_input(Message::DriverFilter)
|
||||||
|
.on_clear(Message::DriverFilter(String::new())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let filter = self.add.driver_filter.to_lowercase();
|
||||||
|
let mut drivers = widget::list_column();
|
||||||
|
let mut shown = 0usize;
|
||||||
|
for driver in self.add.drivers.iter().filter(|driver| {
|
||||||
|
filter.is_empty()
|
||||||
|
|| driver.description.to_lowercase().contains(&filter)
|
||||||
|
|| driver.id.to_lowercase().contains(&filter)
|
||||||
|
}) {
|
||||||
|
let selected = self.add.selected_driver == driver.id;
|
||||||
|
let control: Element<'_, Message> = if selected {
|
||||||
|
widget::icon::from_name("object-select-symbolic").size(16).into()
|
||||||
|
} else {
|
||||||
|
widget::space::horizontal()
|
||||||
|
.width(Length::Fixed(16.0))
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
drivers = drivers.add(
|
||||||
|
widget::list::button(
|
||||||
|
widget::settings::item::builder(driver.description.to_string())
|
||||||
|
.description(driver.id.to_string())
|
||||||
|
.control(control),
|
||||||
|
)
|
||||||
|
.on_press(Message::SelectDriver(driver.id.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
shown += 1;
|
||||||
|
if shown == DRIVER_LIST_LIMIT {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shown == 0 {
|
||||||
|
drivers = drivers.add(text::caption(fl!("printers-no-matching-drivers")));
|
||||||
|
}
|
||||||
|
|
||||||
|
content = content.push(widget::scrollable(drivers).height(Length::Fixed(260.0)));
|
||||||
|
|
||||||
|
if let Some(error) = self.add.error.as_deref() {
|
||||||
|
content = content.push(text::caption(error.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let can_submit = self.add.selected_device.is_some()
|
||||||
|
&& !self.add.name.trim().is_empty()
|
||||||
|
&& !self.add.selected_driver.is_empty()
|
||||||
|
&& !self.add.submitting;
|
||||||
|
|
||||||
|
let label = if self.add.submitting {
|
||||||
|
fl!("printers-add-in-progress")
|
||||||
|
} else {
|
||||||
|
fl!("printers-submit")
|
||||||
|
};
|
||||||
|
|
||||||
|
content = content.push(
|
||||||
|
widget::row::with_children(vec![
|
||||||
|
widget::space::horizontal().apply(Element::from),
|
||||||
|
widget::button::suggested(label)
|
||||||
|
.on_press_maybe(can_submit.then_some(Message::SubmitAdd))
|
||||||
|
.apply(Element::from),
|
||||||
|
])
|
||||||
|
.spacing(spacing.space_s),
|
||||||
|
);
|
||||||
|
|
||||||
|
content.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_label(state: PrinterState, descriptions: &slab::Slab<String>, keys: &StateKeys) -> String {
|
||||||
|
match state {
|
||||||
|
PrinterState::Idle => descriptions[keys.idle].clone(),
|
||||||
|
PrinterState::Processing => descriptions[keys.processing].clone(),
|
||||||
|
PrinterState::Stopped => descriptions[keys.stopped].clone(),
|
||||||
|
PrinterState::Unknown => descriptions[keys.unknown].clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StateKeys {
|
||||||
|
idle: usize,
|
||||||
|
processing: usize,
|
||||||
|
stopped: usize,
|
||||||
|
unknown: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn printer_icon(printer: &backend::Printer) -> &'static str {
|
||||||
|
match printer.state {
|
||||||
|
PrinterState::Processing => "printer-printing-symbolic",
|
||||||
|
PrinterState::Stopped => "printer-error-symbolic",
|
||||||
|
_ => "printer-symbolic",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn printers_section() -> Section<crate::pages::Message> {
|
||||||
|
crate::slab!(descriptions {
|
||||||
|
add = fl!("printers-add");
|
||||||
|
default = fl!("printers-default");
|
||||||
|
make_default = fl!("printers-make-default");
|
||||||
|
no_printers = fl!("printers-no-printers");
|
||||||
|
not_accepting = fl!("printers-not-accepting");
|
||||||
|
idle = fl!("printers-state-idle");
|
||||||
|
processing = fl!("printers-state-processing");
|
||||||
|
stopped = fl!("printers-state-stopped");
|
||||||
|
unknown = fl!("printers-state-unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
let keys = StateKeys {
|
||||||
|
idle,
|
||||||
|
processing,
|
||||||
|
stopped,
|
||||||
|
unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
Section::default()
|
||||||
|
.title(fl!("printers"))
|
||||||
|
.descriptions(descriptions)
|
||||||
|
.view::<Page>(move |_binder, page, section| {
|
||||||
|
let descriptions = §ion.descriptions;
|
||||||
|
let mut list = widget::list_column();
|
||||||
|
|
||||||
|
if let Some(notice) = page.notice.as_deref() {
|
||||||
|
list = list.add(text::caption(notice.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if page.printers.is_empty() {
|
||||||
|
list = list.add(text::body(descriptions[no_printers].clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for printer in &page.printers {
|
||||||
|
let is_selected = page.selected.as_deref() == Some(printer.name.as_str());
|
||||||
|
|
||||||
|
let mut parts: Vec<String> = Vec::new();
|
||||||
|
if let Some(model) = printer.make_and_model.as_deref()
|
||||||
|
&& !model.is_empty()
|
||||||
|
{
|
||||||
|
parts.push(model.to_string());
|
||||||
|
}
|
||||||
|
parts.push(state_label(printer.state, descriptions, &keys));
|
||||||
|
if !printer.accepting_jobs {
|
||||||
|
parts.push(descriptions[not_accepting].clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let control: Element<'_, crate::pages::Message> = if printer.is_default {
|
||||||
|
widget::button::suggested(descriptions[default].clone())
|
||||||
|
.apply(Element::from)
|
||||||
|
} else {
|
||||||
|
widget::button::link(descriptions[make_default].clone())
|
||||||
|
.on_press(Message::SetDefault(printer.name.clone()).into())
|
||||||
|
.apply(Element::from)
|
||||||
|
};
|
||||||
|
|
||||||
|
let row = widget::settings::item::builder(printer.info.to_string())
|
||||||
|
.description(parts.join(" · "))
|
||||||
|
.icon(widget::icon::from_name(printer_icon(printer)).size(24))
|
||||||
|
.control(control);
|
||||||
|
|
||||||
|
let message = if is_selected {
|
||||||
|
Message::Select(None)
|
||||||
|
} else {
|
||||||
|
Message::Select(Some(printer.name.clone()))
|
||||||
|
};
|
||||||
|
|
||||||
|
list = list.add(widget::list::button(row).on_press(message.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
list = list.add(
|
||||||
|
widget::row::with_children(vec![
|
||||||
|
widget::space::horizontal().apply(Element::from),
|
||||||
|
widget::button::suggested(descriptions[add].clone())
|
||||||
|
.on_press(Message::OpenAdd.into())
|
||||||
|
.apply(Element::from),
|
||||||
|
])
|
||||||
|
.spacing(theme::spacing().space_s),
|
||||||
|
);
|
||||||
|
|
||||||
|
list.apply(Element::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_printer_section() -> Section<crate::pages::Message> {
|
||||||
|
crate::slab!(descriptions {
|
||||||
|
make_default = fl!("printers-make-default");
|
||||||
|
pause = fl!("printers-pause");
|
||||||
|
resume = fl!("printers-resume");
|
||||||
|
remove = fl!("printers-remove");
|
||||||
|
test_page = fl!("printers-test-page");
|
||||||
|
idle = fl!("printers-state-idle");
|
||||||
|
processing = fl!("printers-state-processing");
|
||||||
|
stopped = fl!("printers-state-stopped");
|
||||||
|
unknown = fl!("printers-state-unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
let keys = StateKeys {
|
||||||
|
idle,
|
||||||
|
processing,
|
||||||
|
stopped,
|
||||||
|
unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
Section::default()
|
||||||
|
.title(fl!("printers-details"))
|
||||||
|
.descriptions(descriptions)
|
||||||
|
.show_while::<Page>(|page| page.selected.is_some())
|
||||||
|
.view::<Page>(move |_binder, page, section| {
|
||||||
|
let descriptions = §ion.descriptions;
|
||||||
|
|
||||||
|
let Some(printer) = page
|
||||||
|
.selected
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|name| page.printers.iter().find(|p| p.name == name))
|
||||||
|
else {
|
||||||
|
return Element::from(widget::column::with_children(Vec::new()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let spacing = theme::spacing();
|
||||||
|
let mut column = widget::list_column();
|
||||||
|
|
||||||
|
column = column.add(
|
||||||
|
widget::settings::flex_item(
|
||||||
|
fl!("printers-state"),
|
||||||
|
text::body(state_label(printer.state, descriptions, &keys)),
|
||||||
|
)
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(location) = printer.location.as_deref()
|
||||||
|
&& !location.is_empty()
|
||||||
|
{
|
||||||
|
column = column.add(
|
||||||
|
widget::settings::flex_item(fl!("printers-location"), text::body(location.to_string()))
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(uri) = printer.device_uri.as_deref() {
|
||||||
|
column = column.add(
|
||||||
|
widget::settings::flex_item(fl!("printers-device-uri"), text::body(uri.to_string()))
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !printer.state_reasons.is_empty() {
|
||||||
|
column = column.add(
|
||||||
|
widget::settings::flex_item(
|
||||||
|
fl!("printers-state-reasons"),
|
||||||
|
text::body(printer.state_reasons.join(", ")),
|
||||||
|
)
|
||||||
|
.align_items(Alignment::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = printer.name.clone();
|
||||||
|
let mut actions: Vec<Element<'_, crate::pages::Message>> = Vec::new();
|
||||||
|
|
||||||
|
if !printer.is_default {
|
||||||
|
actions.push(
|
||||||
|
widget::button::standard(descriptions[make_default].clone())
|
||||||
|
.on_press(Message::SetDefault(name.clone()).into())
|
||||||
|
.apply(Element::from),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let paused = printer.state == PrinterState::Stopped;
|
||||||
|
actions.push(if paused {
|
||||||
|
widget::button::standard(descriptions[resume].clone())
|
||||||
|
.on_press(Message::ToggleEnabled(name.clone(), true).into())
|
||||||
|
.apply(Element::from)
|
||||||
|
} else {
|
||||||
|
widget::button::standard(descriptions[pause].clone())
|
||||||
|
.on_press(Message::ToggleEnabled(name.clone(), false).into())
|
||||||
|
.apply(Element::from)
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.push(
|
||||||
|
widget::button::standard(descriptions[test_page].clone())
|
||||||
|
.on_press(Message::PrintTestPage(name.clone()).into())
|
||||||
|
.apply(Element::from),
|
||||||
|
);
|
||||||
|
|
||||||
|
actions.push(widget::space::horizontal().apply(Element::from));
|
||||||
|
|
||||||
|
actions.push(
|
||||||
|
widget::button::destructive(descriptions[remove].clone())
|
||||||
|
.on_press(Message::Remove(name).into())
|
||||||
|
.apply(Element::from),
|
||||||
|
);
|
||||||
|
|
||||||
|
column = column.add(widget::row::with_children(actions).spacing(spacing.space_s));
|
||||||
|
|
||||||
|
column.apply(Element::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn job_status_label(status: JobStatus) -> String {
|
||||||
|
match status {
|
||||||
|
JobStatus::Pending => fl!("printers-job-pending"),
|
||||||
|
JobStatus::Processing => fl!("printers-job-processing"),
|
||||||
|
JobStatus::Held => fl!("printers-job-held"),
|
||||||
|
JobStatus::Stopped => fl!("printers-job-stopped"),
|
||||||
|
JobStatus::Completed => fl!("printers-job-completed"),
|
||||||
|
JobStatus::Canceled => fl!("printers-job-canceled"),
|
||||||
|
JobStatus::Aborted => fl!("printers-job-aborted"),
|
||||||
|
JobStatus::Unknown => fl!("printers-job-unknown"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jobs_section() -> Section<crate::pages::Message> {
|
||||||
|
crate::slab!(descriptions {
|
||||||
|
cancel_job = fl!("printers-cancel-job");
|
||||||
|
});
|
||||||
|
|
||||||
|
Section::default()
|
||||||
|
.title(fl!("printers-jobs"))
|
||||||
|
.descriptions(descriptions)
|
||||||
|
.show_while::<Page>(|page| !page.jobs.is_empty())
|
||||||
|
.view::<Page>(move |_binder, page, section| {
|
||||||
|
let descriptions = §ion.descriptions;
|
||||||
|
let mut list = widget::list_column();
|
||||||
|
|
||||||
|
for job in &page.jobs {
|
||||||
|
let cancel_button = widget::icon::from_name("window-close-symbolic")
|
||||||
|
.apply(widget::button::icon)
|
||||||
|
.on_press(Message::CancelJob(job.id).into());
|
||||||
|
|
||||||
|
let title = if job.title.is_empty() {
|
||||||
|
format!("#{}", job.id)
|
||||||
|
} else {
|
||||||
|
job.title.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
list = list.add(
|
||||||
|
widget::settings::item::builder(title)
|
||||||
|
.description(format!("{} · {}", job.dest, job_status_label(job.status)))
|
||||||
|
.control(widget::tooltip::tooltip(
|
||||||
|
cancel_button,
|
||||||
|
text::caption(descriptions[cancel_job].clone()),
|
||||||
|
widget::tooltip::Position::Top,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.apply(Element::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||||
44
i18n/en/cosmic_settings.ftl
vendored
44
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -642,6 +642,50 @@ sound-usb-audio = USB Audio
|
||||||
# Profiles for sound card devices
|
# Profiles for sound card devices
|
||||||
sound-device-profiles = Device profiles
|
sound-device-profiles = Device profiles
|
||||||
|
|
||||||
|
# Printers settings page
|
||||||
|
printers = Printers
|
||||||
|
xdg-entry-printers-comment = Manage printers and print jobs
|
||||||
|
printers-add = Add printer
|
||||||
|
printers-add-in-progress = Adding printer…
|
||||||
|
printers-added = Printer “{ $name }” added
|
||||||
|
printers-cancel-job = Cancel job
|
||||||
|
printers-default = Default
|
||||||
|
printers-details = Printer details
|
||||||
|
printers-device = Device
|
||||||
|
printers-device-uri = Device URI
|
||||||
|
printers-driver = Driver
|
||||||
|
printers-error = Operation failed: { $error }
|
||||||
|
printers-job-aborted = Aborted
|
||||||
|
printers-job-canceled = Canceled
|
||||||
|
printers-job-completed = Completed
|
||||||
|
printers-job-held = Held
|
||||||
|
printers-job-pending = Pending
|
||||||
|
printers-job-processing = Printing
|
||||||
|
printers-job-stopped = Stopped
|
||||||
|
printers-job-unknown = Unknown
|
||||||
|
printers-jobs = Print jobs
|
||||||
|
printers-loading = Searching for devices…
|
||||||
|
printers-location = Location
|
||||||
|
printers-make-default = Set as default
|
||||||
|
printers-name = Name
|
||||||
|
printers-no-devices = No devices found
|
||||||
|
printers-no-matching-drivers = No matching drivers
|
||||||
|
printers-no-printers = No printers configured
|
||||||
|
printers-not-accepting = Not accepting jobs
|
||||||
|
printers-pause = Pause
|
||||||
|
printers-remove = Remove printer
|
||||||
|
printers-remove-confirm = Remove this printer?
|
||||||
|
printers-remove-confirm-description = “{ $name }” will be removed from this system. Print jobs queued on it will be discarded.
|
||||||
|
printers-resume = Resume
|
||||||
|
printers-state = State
|
||||||
|
printers-state-idle = Idle
|
||||||
|
printers-state-processing = Printing
|
||||||
|
printers-state-reasons = State reasons
|
||||||
|
printers-state-stopped = Paused
|
||||||
|
printers-state-unknown = Unknown
|
||||||
|
printers-submit = Add
|
||||||
|
printers-test-page = Print test page
|
||||||
|
|
||||||
# Power & Battery settings page
|
# Power & Battery settings page
|
||||||
power = Power & battery
|
power = Power & battery
|
||||||
|
|
||||||
|
|
|
||||||
45
i18n/fr/cosmic_settings.ftl
vendored
45
i18n/fr/cosmic_settings.ftl
vendored
|
|
@ -429,6 +429,51 @@ sound-alerts = Alertes
|
||||||
sound-applications = Applications
|
sound-applications = Applications
|
||||||
.desc = Volumes et paramètres d'application
|
.desc = Volumes et paramètres d'application
|
||||||
|
|
||||||
|
## Printers
|
||||||
|
|
||||||
|
printers = Imprimantes
|
||||||
|
xdg-entry-printers-comment = Gérer les imprimantes et les travaux d'impression
|
||||||
|
printers-add = Ajouter une imprimante
|
||||||
|
printers-add-in-progress = Ajout de l'imprimante…
|
||||||
|
printers-added = Imprimante « { $name } » ajoutée
|
||||||
|
printers-cancel-job = Annuler le travail
|
||||||
|
printers-default = Par défaut
|
||||||
|
printers-details = Détails de l'imprimante
|
||||||
|
printers-device = Périphérique
|
||||||
|
printers-device-uri = URI du périphérique
|
||||||
|
printers-driver = Pilote
|
||||||
|
printers-error = Échec de l'opération : { $error }
|
||||||
|
printers-job-aborted = Interrompu
|
||||||
|
printers-job-canceled = Annulé
|
||||||
|
printers-job-completed = Terminé
|
||||||
|
printers-job-held = Retenu
|
||||||
|
printers-job-pending = En attente
|
||||||
|
printers-job-processing = Impression en cours
|
||||||
|
printers-job-stopped = Arrêté
|
||||||
|
printers-job-unknown = Inconnu
|
||||||
|
printers-jobs = Travaux d'impression
|
||||||
|
printers-loading = Recherche des périphériques…
|
||||||
|
printers-location = Emplacement
|
||||||
|
printers-make-default = Définir par défaut
|
||||||
|
printers-name = Nom
|
||||||
|
printers-no-devices = Aucun périphérique trouvé
|
||||||
|
printers-no-matching-drivers = Aucun pilote ne correspond
|
||||||
|
printers-no-printers = Aucune imprimante configurée
|
||||||
|
printers-not-accepting = N'accepte pas les travaux
|
||||||
|
printers-pause = Suspendre
|
||||||
|
printers-remove = Supprimer l'imprimante
|
||||||
|
printers-remove-confirm = Supprimer cette imprimante ?
|
||||||
|
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-state = État
|
||||||
|
printers-state-idle = Disponible
|
||||||
|
printers-state-processing = Impression en cours
|
||||||
|
printers-state-reasons = Causes de l'état
|
||||||
|
printers-state-stopped = En pause
|
||||||
|
printers-state-unknown = Inconnu
|
||||||
|
printers-submit = Ajouter
|
||||||
|
printers-test-page = Imprimer une page de test
|
||||||
|
|
||||||
## Power
|
## Power
|
||||||
|
|
||||||
power = Alimentation
|
power = Alimentation
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue