feat(networking): add VPN, WiFi, and Wired network pages
This commit is contained in:
parent
d035ba0cf7
commit
fa22b556dd
22 changed files with 2876 additions and 131 deletions
337
Cargo.lock
generated
337
Cargo.lock
generated
|
|
@ -267,7 +267,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -288,6 +288,12 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
|
||||
|
||||
[[package]]
|
||||
name = "as-result"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3702cac3c1601410cd655ae41650c4c87f7c3183dca6d1cd9acc4220ed56a8b7"
|
||||
|
||||
[[package]]
|
||||
name = "ash"
|
||||
version = "0.37.3+1.3.251"
|
||||
|
|
@ -519,7 +525,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -548,13 +554,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
|||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.81"
|
||||
version = "0.1.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -704,7 +710,7 @@ dependencies = [
|
|||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -828,7 +834,7 @@ dependencies = [
|
|||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"syn_derive",
|
||||
]
|
||||
|
||||
|
|
@ -900,7 +906,7 @@ checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1093,7 +1099,7 @@ dependencies = [
|
|||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1105,7 +1111,7 @@ dependencies = [
|
|||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1406,7 +1412,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-bg-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/cosmic-bg#e5e91d93fb7cd7e917922eceaddd4d412df46f93"
|
||||
source = "git+https://github.com/pop-os/cosmic-bg#876987a5e7b76d8005dcaf969baf0dd419296dab"
|
||||
dependencies = [
|
||||
"colorgrad",
|
||||
"cosmic-config",
|
||||
|
|
@ -1431,7 +1437,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-comp-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/cosmic-comp#ed64e26faf4b97221f0883bc1113f997acbadc50"
|
||||
source = "git+https://github.com/pop-os/cosmic-comp#52280e9823506a480248e84caaf7545426a871c8"
|
||||
dependencies = [
|
||||
"cosmic-config",
|
||||
"input",
|
||||
|
|
@ -1441,7 +1447,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"atomicwrites",
|
||||
"cosmic-config-derive",
|
||||
|
|
@ -1463,12 +1469,26 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-dbus-networkmanager"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#e0d6a04d6ebf6bcede1580721c84a7f01e5ef8bb"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"derive_builder",
|
||||
"procfs",
|
||||
"thiserror",
|
||||
"time",
|
||||
"zbus 4.4.0",
|
||||
"zvariant 4.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-panel-config"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1505,7 +1525,7 @@ source = "git+https://github.com/pop-os/cosmic-randr#71fabbb382fa8cf750f50fb77c4
|
|||
dependencies = [
|
||||
"cosmic-protocols",
|
||||
"futures-lite 2.3.0",
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"tachyonix",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
|
|
@ -1530,6 +1550,7 @@ name = "cosmic-settings"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as-result",
|
||||
"ashpd 0.9.1",
|
||||
"async-channel",
|
||||
"chrono",
|
||||
|
|
@ -1538,6 +1559,7 @@ dependencies = [
|
|||
"cosmic-bg-config",
|
||||
"cosmic-comp-config",
|
||||
"cosmic-config",
|
||||
"cosmic-dbus-networkmanager",
|
||||
"cosmic-panel-config",
|
||||
"cosmic-randr",
|
||||
"cosmic-randr-shell",
|
||||
|
|
@ -1550,6 +1572,7 @@ dependencies = [
|
|||
"derive_setters",
|
||||
"dirs",
|
||||
"downcast-rs",
|
||||
"eyre",
|
||||
"freedesktop-desktop-entry",
|
||||
"futures",
|
||||
"hostname-validator",
|
||||
|
|
@ -1558,7 +1581,7 @@ dependencies = [
|
|||
"i18n-embed-fl",
|
||||
"icu",
|
||||
"image 0.25.2",
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"itertools 0.13.0",
|
||||
"itoa",
|
||||
"libcosmic",
|
||||
|
|
@ -1567,6 +1590,7 @@ dependencies = [
|
|||
"regex",
|
||||
"ron",
|
||||
"rust-embed",
|
||||
"secure-string",
|
||||
"serde",
|
||||
"slab",
|
||||
"slotmap",
|
||||
|
|
@ -1600,7 +1624,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-settings-daemon"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#7aedc25e3295b95a90eb710f443029d4ec920aa8"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#e0d6a04d6ebf6bcede1580721c84a7f01e5ef8bb"
|
||||
dependencies = [
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
|
@ -1623,16 +1647,21 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-settings-subscriptions"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#f6fa655e4b74a5bd2dbfc2f6fdd94bc78f5e4fcc"
|
||||
source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#90df5c4b22c47f0e0213dcf2025519b0312437df"
|
||||
dependencies = [
|
||||
"cosmic-dbus-networkmanager",
|
||||
"futures",
|
||||
"iced_futures",
|
||||
"itertools 0.13.0",
|
||||
"libpulse-binding",
|
||||
"log",
|
||||
"pipewire",
|
||||
"rustix 0.38.35",
|
||||
"secure-string",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"upower_dbus",
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
|
@ -1669,7 +1698,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.12.1"
|
||||
source = "git+https://github.com/pop-os/cosmic-text.git#e16b39f29c84773a05457fe39577a602de27855c"
|
||||
source = "git+https://github.com/pop-os/cosmic-text.git#c7512170201910cfb8a021f8d749c4125dfed18d"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"fontdb",
|
||||
|
|
@ -1679,6 +1708,7 @@ dependencies = [
|
|||
"rustc-hash",
|
||||
"rustybuzz 0.14.1",
|
||||
"self_cell 1.0.4",
|
||||
"smol_str",
|
||||
"swash",
|
||||
"sys-locale",
|
||||
"ttf-parser 0.21.1",
|
||||
|
|
@ -1691,7 +1721,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"almost",
|
||||
"cosmic-config",
|
||||
|
|
@ -1811,14 +1841,38 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
||||
dependencies = [
|
||||
"darling_core 0.14.4",
|
||||
"darling_macro 0.14.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.20.10",
|
||||
"darling_macro 0.20.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1831,8 +1885,19 @@ dependencies = [
|
|||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.76",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.14.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
||||
dependencies = [
|
||||
"darling_core 0.14.4",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1841,9 +1906,9 @@ version = "0.20.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.20.10",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1887,16 +1952,47 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
|
||||
dependencies = [
|
||||
"darling 0.14.4",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_setters"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1954,7 +2050,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2073,7 +2169,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2377,7 +2473,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2415,7 +2511,7 @@ dependencies = [
|
|||
"gettext-rs",
|
||||
"log",
|
||||
"memchr",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"textdistance",
|
||||
"thiserror",
|
||||
"xdg",
|
||||
|
|
@ -2534,7 +2630,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2613,9 +2709,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gettext-rs"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e49ea8a8fad198aaa1f9655a2524b64b70eb06b2f3ff37da407566c93054f364"
|
||||
checksum = "4a6716b8a0db461a2720b850ba1623e5b69e4b1aa0224cf5e1fb23a0fe49e65c"
|
||||
dependencies = [
|
||||
"gettext-sys",
|
||||
"locale_config",
|
||||
|
|
@ -2623,9 +2719,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gettext-sys"
|
||||
version = "0.21.3"
|
||||
version = "0.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c63ce2e00f56a206778276704bbe38564c8695249fdc8f354b4ef71c57c3839d"
|
||||
checksum = "f7b8797f28f2dabfbe2caadb6db4f7fd739e251b5ede0a2ba49e506071edcf67"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"temp-dir",
|
||||
|
|
@ -2869,7 +2965,7 @@ checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2"
|
|||
[[package]]
|
||||
name = "hostname1-zbus"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#7aedc25e3295b95a90eb710f443029d4ec920aa8"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#e0d6a04d6ebf6bcede1580721c84a7f01e5ef8bb"
|
||||
dependencies = [
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
|
@ -2926,8 +3022,8 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.76",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.77",
|
||||
"unic-langid",
|
||||
]
|
||||
|
||||
|
|
@ -2941,7 +3037,7 @@ dependencies = [
|
|||
"i18n-config",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2970,7 +3066,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -2989,7 +3085,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_accessibility"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_unix",
|
||||
|
|
@ -2998,7 +3094,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_core"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"dnd",
|
||||
|
|
@ -3020,7 +3116,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_futures"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_core",
|
||||
|
|
@ -3033,7 +3129,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_graphics"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
|
|
@ -3057,7 +3153,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_renderer"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"iced_graphics",
|
||||
"iced_tiny_skia",
|
||||
|
|
@ -3069,7 +3165,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_runtime"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -3083,7 +3179,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_sctk"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"enum-repr",
|
||||
"float-cmp",
|
||||
|
|
@ -3109,7 +3205,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_style"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"iced_core",
|
||||
"once_cell",
|
||||
|
|
@ -3119,7 +3215,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_tiny_skia"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
|
|
@ -3136,7 +3232,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_wgpu"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"bitflags 2.6.0",
|
||||
|
|
@ -3165,7 +3261,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_widget"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_renderer",
|
||||
|
|
@ -3182,7 +3278,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_winit"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_graphics",
|
||||
|
|
@ -3556,7 +3652,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3700,9 +3796,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.5",
|
||||
|
|
@ -3775,7 +3871,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3995,7 +4091,7 @@ checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#0a1922d4b378de23f55b117b07bad23dda0d64d0"
|
||||
source = "git+https://github.com/pop-os/libcosmic#71cd25c06d230a742ebf660297478b732cf1882b"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"ashpd 0.9.1",
|
||||
|
|
@ -4397,7 +4493,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4473,7 +4569,7 @@ dependencies = [
|
|||
"bitflags 2.6.0",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"log",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
|
|
@ -4667,7 +4763,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4739,7 +4835,7 @@ dependencies = [
|
|||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4874,7 +4970,7 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4920,7 +5016,7 @@ dependencies = [
|
|||
"by_address",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5019,7 +5115,7 @@ dependencies = [
|
|||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5217,6 +5313,29 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "procfs"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"procfs-core",
|
||||
"rustix 0.38.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "procfs-core"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"hex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.15"
|
||||
|
|
@ -5233,7 +5352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5585,9 +5704,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.48"
|
||||
version = "0.8.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f86ae463694029097b846d8f99fd5536740602ae00022c0c50c5600720b2f71"
|
||||
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
|
@ -5665,7 +5784,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
@ -5829,6 +5948,16 @@ version = "4.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "secure-string"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "548ba8c9ff631f7bb3a64de1e8ad73fe20f6d04090724f2b496ed45314ad7488"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "0.10.3"
|
||||
|
|
@ -5873,7 +6002,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5882,7 +6011,7 @@ version = "1.0.127"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
|
||||
dependencies = [
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
|
|
@ -5897,7 +6026,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5919,7 +6048,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
|
|
@ -5933,10 +6062,10 @@ version = "3.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6245,6 +6374,12 @@ dependencies = [
|
|||
"float-cmp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -6300,9 +6435,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.76"
|
||||
version = "2.0.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -6318,7 +6453,7 @@ dependencies = [
|
|||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6329,7 +6464,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6456,7 +6591,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6514,7 +6649,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "timedate-zbus"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#7aedc25e3295b95a90eb710f443029d4ec920aa8"
|
||||
source = "git+https://github.com/pop-os/dbus-settings-bindings#e0d6a04d6ebf6bcede1580721c84a7f01e5ef8bb"
|
||||
dependencies = [
|
||||
"zbus 4.4.0",
|
||||
]
|
||||
|
|
@ -6618,7 +6753,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6668,7 +6803,7 @@ version = "0.19.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
|
@ -6679,7 +6814,7 @@ version = "0.22.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
|
||||
dependencies = [
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
|
|
@ -6705,7 +6840,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7123,7 +7258,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
|
@ -7157,7 +7292,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
|
@ -7393,7 +7528,7 @@ dependencies = [
|
|||
"bitflags 2.6.0",
|
||||
"cfg_aliases 0.1.1",
|
||||
"codespan-reporting",
|
||||
"indexmap 2.4.0",
|
||||
"indexmap 2.5.0",
|
||||
"log",
|
||||
"naga",
|
||||
"once_cell",
|
||||
|
|
@ -7571,7 +7706,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7582,7 +7717,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8056,7 +8191,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
|
|
@ -8158,7 +8293,7 @@ dependencies = [
|
|||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"zvariant_utils 2.1.0",
|
||||
]
|
||||
|
||||
|
|
@ -8208,7 +8343,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8228,10 +8363,16 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.1.3"
|
||||
|
|
@ -8262,7 +8403,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8339,7 +8480,7 @@ dependencies = [
|
|||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
"zvariant_utils 2.1.0",
|
||||
]
|
||||
|
||||
|
|
@ -8362,5 +8503,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.76",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -58,3 +58,7 @@ cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", rev =
|
|||
# libcosmic = { path = "../libcosmic" }
|
||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
||||
|
||||
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
|
||||
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
|
||||
# upower_dbus = { path = "../dbus-settings-bindings/upower" }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ license = "GPL-3.0"
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
as-result = "0.2.1"
|
||||
ashpd = { version = "0.9", default-features = false, features = ["tokio"] }
|
||||
async-channel = "2.3.1"
|
||||
chrono = "0.4.38"
|
||||
|
|
@ -14,6 +15,7 @@ color-eyre = "0.6.3"
|
|||
cosmic-bg-config.workspace = true
|
||||
cosmic-comp-config.workspace = true
|
||||
cosmic-config.workspace = true
|
||||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||
cosmic-panel-config.workspace = true
|
||||
cosmic-randr-shell.workspace = true
|
||||
cosmic-randr.workspace = true
|
||||
|
|
@ -25,6 +27,7 @@ derivative = "2.2.0"
|
|||
derive_setters = "0.1.6"
|
||||
dirs = "5.0.1"
|
||||
downcast-rs = "1.2.1"
|
||||
eyre = "0.6.12"
|
||||
freedesktop-desktop-entry = "0.7.3"
|
||||
futures = "0.3.30"
|
||||
hostname-validator = "1.1.1"
|
||||
|
|
@ -40,6 +43,7 @@ once_cell = "1.19.0"
|
|||
regex = "1.10.6"
|
||||
ron = "0.8"
|
||||
rust-embed = "8.5.0"
|
||||
secure-string = "0.3.0"
|
||||
serde = { version = "1.0.208", features = ["derive"] }
|
||||
slab = "0.4.9"
|
||||
slotmap = "1.0.7"
|
||||
|
|
@ -58,7 +62,7 @@ zbus = { version = "4.4.0", features = ["tokio"] }
|
|||
|
||||
[dependencies.cosmic-settings-subscriptions]
|
||||
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
|
||||
features = ["pipewire", "pulse"]
|
||||
features = ["network_manager", "pipewire", "pulse"]
|
||||
|
||||
[dependencies.icu]
|
||||
version = "1.5.0"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::pages::desktop::{
|
|||
},
|
||||
};
|
||||
use crate::pages::input::{self};
|
||||
use crate::pages::{self, display, power, sound, system, time};
|
||||
use crate::pages::{self, display, networking, power, sound, system, time};
|
||||
use crate::subscription::desktop_files;
|
||||
use crate::widget::{page_title, search_header};
|
||||
use crate::PageCommands;
|
||||
|
|
@ -77,10 +77,13 @@ impl SettingsApp {
|
|||
PageCommands::Time => self.pages.page_id::<time::Page>(),
|
||||
PageCommands::Touchpad => self.pages.page_id::<input::touchpad::Page>(),
|
||||
PageCommands::Users => self.pages.page_id::<system::users::Page>(),
|
||||
PageCommands::Vpn => self.pages.page_id::<networking::vpn::Page>(),
|
||||
PageCommands::Wallpaper => self.pages.page_id::<desktop::wallpaper::Page>(),
|
||||
PageCommands::WindowManagement => {
|
||||
self.pages.page_id::<desktop::window_management::Page>()
|
||||
}
|
||||
PageCommands::Wired => self.pages.page_id::<networking::wired::Page>(),
|
||||
PageCommands::Wireless => self.pages.page_id::<networking::wifi::Page>(),
|
||||
PageCommands::Workspaces => self.pages.page_id::<desktop::workspaces::Page>(),
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +144,7 @@ impl cosmic::Application for SettingsApp {
|
|||
search_selections: Vec::default(),
|
||||
};
|
||||
|
||||
app.insert_page::<networking::Page>();
|
||||
let desktop_id = app.insert_page::<desktop::Page>().id();
|
||||
app.insert_page::<display::Page>();
|
||||
app.insert_page::<sound::Page>();
|
||||
|
|
@ -452,9 +456,27 @@ impl cosmic::Application for SettingsApp {
|
|||
page::update!(self.pages, message, power::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Vpn(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<networking::vpn::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::WiFi(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<networking::wifi::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
crate::pages::Message::WindowManagement(message) => {
|
||||
page::update!(self.pages, message, desktop::window_management::Page);
|
||||
}
|
||||
|
||||
crate::pages::Message::Wired(message) => {
|
||||
if let Some(page) = self.pages.page_mut::<networking::wired::Page>() {
|
||||
return page.update(message).map(Into::into);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Message::OutputAdded(info, output) => {
|
||||
|
|
@ -730,7 +752,7 @@ impl SettingsApp {
|
|||
custom_header.map(Message::from)
|
||||
} else if let Some(parent) = page_info.parent {
|
||||
let page_header = crate::widget::sub_page_header(
|
||||
page_info.title.as_str(),
|
||||
page.title().unwrap_or_else(|| page_info.title.as_str()),
|
||||
self.pages.info[parent].title.as_str(),
|
||||
Message::Page(parent),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -78,10 +78,16 @@ pub enum PageCommands {
|
|||
Touchpad,
|
||||
/// Users settings page
|
||||
Users,
|
||||
/// VPN settings page
|
||||
Vpn,
|
||||
/// Wallpaper settings page
|
||||
Wallpaper,
|
||||
/// Window management settings page
|
||||
WindowManagement,
|
||||
/// Wired settings page
|
||||
Wired,
|
||||
/// WiFi settings page
|
||||
Wireless,
|
||||
/// Workspaces settings page
|
||||
Workspaces,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,18 +24,11 @@ use cosmic::{
|
|||
},
|
||||
};
|
||||
use cosmic::{
|
||||
iced::{wayland::actions::window::SctkWindowSettings, window, Color, Length},
|
||||
iced::{Color, Length},
|
||||
prelude::CollectionWidget,
|
||||
};
|
||||
use cosmic::{
|
||||
iced_core::Alignment,
|
||||
iced_sctk::commands::window::{close_window, get_window},
|
||||
widget::icon,
|
||||
};
|
||||
use cosmic::{
|
||||
iced_core::{alignment, layout},
|
||||
iced_runtime::core::image::Handle as ImageHandle,
|
||||
};
|
||||
use cosmic::{iced_core::alignment, iced_runtime::core::image::Handle as ImageHandle};
|
||||
use cosmic::{iced_core::Alignment, widget::icon};
|
||||
use cosmic::{
|
||||
widget::{color_picker::ColorPickerUpdate, ColorPickerModel},
|
||||
Element,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use cosmic_settings_page::Entity;
|
|||
pub mod desktop;
|
||||
pub mod display;
|
||||
pub mod input;
|
||||
pub mod networking;
|
||||
pub mod power;
|
||||
pub mod sound;
|
||||
pub mod system;
|
||||
|
|
@ -37,7 +38,10 @@ pub enum Message {
|
|||
Sound(sound::Message),
|
||||
SystemShortcuts(input::keyboard::shortcuts::ShortcutMessage),
|
||||
TilingShortcuts(input::keyboard::shortcuts::ShortcutMessage),
|
||||
Vpn(networking::vpn::Message),
|
||||
WiFi(networking::wifi::Message),
|
||||
WindowManagement(desktop::window_management::Message),
|
||||
Wired(networking::wired::Message),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::Message {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("online-accounts", "goa-panel-symbolic")
|
||||
.title(fl!("online-accounts"))
|
||||
.description(fl!("online-accounts", "desc"))
|
||||
}
|
||||
|
|
@ -1,5 +1,62 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod accounts;
|
||||
pub mod vpn;
|
||||
pub mod wifi;
|
||||
pub mod wired;
|
||||
|
||||
use std::{ffi::OsStr, io, process::ExitStatus};
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
|
||||
static NM_CONNECTION_EDITOR: &str = "nm-connection-editor";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page;
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new(
|
||||
"network-and-wireless",
|
||||
"preferences-network-and-wireless-symbolic",
|
||||
)
|
||||
.title(fl!("network-and-wireless"))
|
||||
}
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {
|
||||
fn sub_pages(
|
||||
page: cosmic_settings_page::Insert<crate::pages::Message>,
|
||||
) -> cosmic_settings_page::Insert<crate::pages::Message> {
|
||||
page.sub_page::<wired::Page>()
|
||||
.sub_page::<wifi::Page>()
|
||||
.sub_page::<vpn::Page>()
|
||||
}
|
||||
}
|
||||
|
||||
async fn nm_add_vpn_file<P: AsRef<OsStr>>(type_: &str, path: P) -> io::Result<ExitStatus> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(["connection", "import", "type", type_, "file"])
|
||||
.arg(path)
|
||||
.status()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn nm_add_wired() -> io::Result<ExitStatus> {
|
||||
nm_connection_editor(&["--type=802-3-ethernet", "-c"]).await
|
||||
}
|
||||
|
||||
async fn nm_add_wifi() -> io::Result<ExitStatus> {
|
||||
nm_connection_editor(&["--type=802-11-wireless", "-c"]).await
|
||||
}
|
||||
|
||||
async fn nm_edit_connection(uuid: &str) -> io::Result<ExitStatus> {
|
||||
nm_connection_editor(&[&["--edit=", uuid].concat()]).await
|
||||
}
|
||||
|
||||
async fn nm_connection_editor(args: &[&str]) -> io::Result<ExitStatus> {
|
||||
tokio::process::Command::new(NM_CONNECTION_EDITOR)
|
||||
.args(args)
|
||||
.status()
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
877
cosmic-settings/src/pages/networking/vpn/mod.rs
Normal file
877
cosmic-settings/src/pages/networking/vpn/mod.rs
Normal file
|
|
@ -0,0 +1,877 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
mod nmcli;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use ashpd::desktop::file_chooser::FileFilter;
|
||||
use cosmic::{
|
||||
iced::{alignment, Length},
|
||||
iced_core::text::Wrap,
|
||||
prelude::CollectionWidget,
|
||||
widget::{self, icon},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use cosmic_settings_subscriptions::network_manager::{
|
||||
self, current_networks::ActiveConnectionInfo, NetworkManagerState, UUID,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use indexmap::IndexMap;
|
||||
use secure_string::SecureString;
|
||||
use slab::Slab;
|
||||
|
||||
pub type ConnectionId = Arc<str>;
|
||||
pub type InterfaceId = String;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Activate a connection
|
||||
Activate(ConnectionId),
|
||||
/// Add a network connection
|
||||
AddNetwork,
|
||||
/// Cancels an active dialog.
|
||||
CancelDialog,
|
||||
/// Connect to a VPN with the given username and password
|
||||
ConnectWithPassword,
|
||||
/// Deactivate a connection.
|
||||
Deactivate(ConnectionId),
|
||||
/// An error occurred.
|
||||
Error(String),
|
||||
/// Update the list of known connections.
|
||||
KnownConnections(IndexMap<UUID, VpnConnectionSettings>),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(
|
||||
(
|
||||
zbus::Connection,
|
||||
tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
),
|
||||
),
|
||||
/// Updates the password text input
|
||||
PasswordUpdate(SecureString),
|
||||
/// Refresh devices and their connection profiles
|
||||
Refresh,
|
||||
/// Create a dialog to ask for confirmation of removal.
|
||||
RemoveProfileRequest(ConnectionId),
|
||||
/// Remove a connection profile
|
||||
RemoveProfile(ConnectionId),
|
||||
/// Opens settings page for the access point.
|
||||
Settings(ConnectionId),
|
||||
/// Toggles visibility of password input.
|
||||
TogglePasswordVisibility,
|
||||
/// Update NetworkManagerState
|
||||
UpdateState(NetworkManagerState),
|
||||
/// Update the devices lists
|
||||
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
|
||||
/// Updates the username text input
|
||||
UsernameUpdate(String),
|
||||
/// Display more options for an access point
|
||||
ViewMore(Option<ConnectionId>),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Vpn(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Vpn(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct VpnConnectionSettings {
|
||||
id: String,
|
||||
username: Option<String>,
|
||||
connection_type: Option<ConnectionType>,
|
||||
password_flag: Option<PasswordFlag>,
|
||||
}
|
||||
|
||||
impl VpnConnectionSettings {
|
||||
fn password_flag(&self) -> Option<PasswordFlag> {
|
||||
self.connection_type
|
||||
.as_ref()
|
||||
.map_or(false, |ct| match ct {
|
||||
ConnectionType::Password => true,
|
||||
})
|
||||
.then(|| self.password_flag)
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum ConnectionType {
|
||||
Password,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
enum PasswordFlag {
|
||||
/// The system is responsible for providing and storing this secret.
|
||||
None = 0,
|
||||
/// A user-session secret agent is responsible for providing and storing
|
||||
/// this secret; when it is required, agents will be asked to provide it.
|
||||
AgentOwned = 1,
|
||||
/// This secret should not be saved but should be requested from the user
|
||||
/// each time it is required. This flag should be used for One-Time-Pad
|
||||
/// secrets, PIN codes from hardware tokens, or if the user simply does not
|
||||
/// want to save the secret.
|
||||
NotSaved = 2,
|
||||
/// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user.
|
||||
NotRequired = 4,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum VpnDialog {
|
||||
Password {
|
||||
id: String,
|
||||
uuid: Arc<str>,
|
||||
username: String,
|
||||
password: SecureString,
|
||||
password_hidden: bool,
|
||||
},
|
||||
RemoveProfile(ConnectionId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmState {
|
||||
conn: zbus::Connection,
|
||||
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
|
||||
active_conns: Vec<ActiveConnectionInfo>,
|
||||
devices: Vec<network_manager::devices::DeviceInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<VpnDialog>,
|
||||
view_more_popup: Option<ConnectionId>,
|
||||
known_connections: IndexMap<UUID, VpnConnectionSettings>,
|
||||
/// Withhold device update if the view more popup is shown.
|
||||
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
||||
/// Withhold active connections update if the view more popup is shown.
|
||||
withheld_active_conns: Option<Vec<ActiveConnectionInfo>>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new("vpn", "preferences-vpn-symbolic")
|
||||
.title(fl!("vpn"))
|
||||
.description(fl!("connections-and-profiles", variant = "vpn"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(devices_view())])
|
||||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
|
||||
self.dialog.as_ref().map(|dialog| match dialog {
|
||||
VpnDialog::Password {
|
||||
username,
|
||||
password,
|
||||
password_hidden,
|
||||
..
|
||||
} => {
|
||||
let username = widget::text_input(fl!("username"), username.as_str())
|
||||
.on_input(Message::UsernameUpdate);
|
||||
|
||||
let password = widget::text_input::secure_input(
|
||||
fl!("password"),
|
||||
password.unsecure(),
|
||||
Some(Message::TogglePasswordVisibility),
|
||||
*password_hidden,
|
||||
)
|
||||
.on_input(|input| Message::PasswordUpdate(SecureString::from(input)))
|
||||
.on_submit(Message::ConnectWithPassword);
|
||||
|
||||
let controls = widget::column::with_capacity(2)
|
||||
.spacing(12)
|
||||
.push(username)
|
||||
.push(password)
|
||||
.apply(Element::from);
|
||||
|
||||
let primary_action = widget::button::suggested(fl!("connect"))
|
||||
.on_press(Message::ConnectWithPassword);
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("auth-dialog"))
|
||||
.icon(icon::from_name("network-vpn-symbolic").size(64))
|
||||
.body(fl!("auth-dialog", "vpn-description"))
|
||||
.control(controls)
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn)
|
||||
}
|
||||
|
||||
VpnDialog::RemoveProfile(uuid) => {
|
||||
let primary_action = widget::button::destructive(fl!("remove"))
|
||||
.on_press(Message::RemoveProfile(uuid.clone()));
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("remove-connection-dialog"))
|
||||
.icon(icon::from_name("dialog-information").size(64))
|
||||
.body(fl!("remove-connection-dialog", "vpn-description"))
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_view(&self) -> Option<Element<'_, crate::pages::Message>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
.trailing_icon(icon::from_name("window-pop-out-symbolic"))
|
||||
.on_press(Message::AddNetwork)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_page::Entity,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) -> cosmic::Command<crate::pages::Message> {
|
||||
if self.nm_task.is_none() {
|
||||
return cosmic::command::future(async move {
|
||||
zbus::Connection::system()
|
||||
.await
|
||||
.context("failed to create system dbus connection")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
|conn| Message::NetworkManagerConnect((conn, sender.clone())),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
self.view_more_popup = None;
|
||||
self.nm_state = None;
|
||||
self.withheld_active_conns = None;
|
||||
self.withheld_devices = None;
|
||||
self.dialog = None;
|
||||
|
||||
if let Some(cancel) = self.nm_task.take() {
|
||||
_ = cancel.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::NetworkManager(network_manager::Event::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
success,
|
||||
}) => {
|
||||
if !success {
|
||||
tracing::error!(request = ?req, "network-manager request failed");
|
||||
}
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
let conn = conn.clone();
|
||||
self.update_active_conns(state);
|
||||
return cosmic::command::batch(vec![
|
||||
connection_settings(conn.clone()),
|
||||
update_devices(conn),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::KnownConnections(connections) => {
|
||||
self.known_connections = connections;
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_active_conns(state);
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns | network_manager::Event::Devices,
|
||||
) => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
connection_settings(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
}) => {
|
||||
self.nm_state = Some(NmState {
|
||||
conn: conn.clone(),
|
||||
sender,
|
||||
devices: Vec::new(),
|
||||
active_conns: state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Vpn { .. }))
|
||||
.collect(),
|
||||
});
|
||||
|
||||
return cosmic::command::batch(vec![
|
||||
connection_settings(conn.clone()),
|
||||
update_devices(conn),
|
||||
]);
|
||||
}
|
||||
|
||||
Message::NetworkManager(_event) => (),
|
||||
|
||||
Message::AddNetwork => return add_network(),
|
||||
|
||||
Message::Activate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
if let Some(settings) = self.known_connections.get(&uuid) {
|
||||
match settings.password_flag() {
|
||||
Some(PasswordFlag::NotSaved | PasswordFlag::AgentOwned) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(VpnDialog::Password {
|
||||
id: settings.id.clone(),
|
||||
uuid: uuid.clone(),
|
||||
username: settings.username.clone().unwrap_or_default(),
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
_ => {
|
||||
let connection_name = settings.id.clone();
|
||||
return cosmic::command::future(async move {
|
||||
if let Err(why) = nmcli::connect(&connection_name).await {
|
||||
return Message::Error(format!(
|
||||
"failed to connect to VPN: {why}"
|
||||
));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::Deactivate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Deactivate(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::RemoveProfileRequest(uuid) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(VpnDialog::RemoveProfile(uuid));
|
||||
}
|
||||
|
||||
Message::RemoveProfile(uuid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Remove(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(uuid) => {
|
||||
self.view_more_popup = uuid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
return cosmic::command::future(async move {
|
||||
super::nm_edit_connection(uuid.as_ref())
|
||||
.then(|res| async move {
|
||||
match res.context("failed to open connection editor") {
|
||||
Ok(_) => Message::Refresh,
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
Message::Refresh => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
connection_settings(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordUpdate(pass) => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut password, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
if let VpnDialog::Password {
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
..
|
||||
} = dialog
|
||||
{
|
||||
return self
|
||||
.activate_with_password(id, username, password)
|
||||
.map(crate::app::Message::from);
|
||||
}
|
||||
}
|
||||
|
||||
Message::UsernameUpdate(user) => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut username, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*username = user;
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::TogglePasswordVisibility => {
|
||||
if let Some(VpnDialog::Password {
|
||||
ref mut password_hidden,
|
||||
..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password_hidden = !*password_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why, "error in VPN settings page");
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect((conn, output)) => {
|
||||
self.connect(conn.clone(), output);
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn activate_with_password(
|
||||
&mut self,
|
||||
connection_name: String,
|
||||
username: String,
|
||||
password: SecureString,
|
||||
) -> Command<Message> {
|
||||
cosmic::command::future(async move {
|
||||
if let Err(why) = nmcli::set_username(&connection_name, &username).await {
|
||||
return Message::Error(format!("failed to set VPN username: {why}"));
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password_flags_none(&connection_name).await {
|
||||
return Message::Error(format!(
|
||||
"failed to call nmcli to set VPN password-flags parameter: {why}"
|
||||
));
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::set_password(&connection_name, password.unsecure()).await {
|
||||
return Message::Error(format!("failed to call nmcli to set VPN password: {why}"));
|
||||
}
|
||||
|
||||
if let Err(why) = nmcli::connect(&connection_name).await {
|
||||
return Message::Error(format!("failed to connect to VPN: {why}"));
|
||||
}
|
||||
|
||||
Message::Refresh
|
||||
})
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&mut self,
|
||||
conn: zbus::Connection,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) {
|
||||
if self.nm_task.is_none() {
|
||||
self.nm_task = Some(crate::utils::forward_event_loop(
|
||||
sender,
|
||||
|event| crate::pages::Message::Vpn(Message::NetworkManager(event)),
|
||||
move |tx| async move {
|
||||
futures::join!(
|
||||
network_manager::watch(conn.clone(), tx.clone()),
|
||||
network_manager::active_conns::watch(conn.clone(), tx.clone()),
|
||||
network_manager::devices::watch(conn, true, tx)
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the view more popup and applies any withheld updates.
|
||||
fn close_popup_and_apply_updates(&mut self) {
|
||||
self.view_more_popup = None;
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if let Some(active_conns) = self.withheld_active_conns.take() {
|
||||
nm_state.active_conns = active_conns;
|
||||
}
|
||||
|
||||
if let Some(devices) = self.withheld_devices.take() {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_devices(&mut self, devices: Vec<network_manager::devices::DeviceInfo>) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_devices = Some(devices);
|
||||
} else {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_active_conns(&mut self, state: NetworkManagerState) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
let conns = state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Vpn { .. }))
|
||||
.collect();
|
||||
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_active_conns = Some(conns);
|
||||
} else {
|
||||
nm_state.active_conns = conns;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn devices_view() -> Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
vpn_conns_txt = fl!("vpn", "connections");
|
||||
remove_txt = fl!("vpn", "remove");
|
||||
connect_txt = fl!("connect");
|
||||
connected_txt = fl!("connected");
|
||||
settings_txt = fl!("settings");
|
||||
disconnect_txt = fl!("disconnect");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let Some(NmState {
|
||||
ref active_conns, ..
|
||||
}) = page.nm_state
|
||||
else {
|
||||
return cosmic::widget::column().into();
|
||||
};
|
||||
|
||||
let theme = cosmic::theme::active();
|
||||
let spacing = &theme.cosmic().spacing;
|
||||
|
||||
let mut view = widget::column::with_capacity(4);
|
||||
|
||||
let vpn_connections =
|
||||
widget::settings::view_section(§ion.descriptions[vpn_conns_txt]);
|
||||
|
||||
if page.known_connections.is_empty() {
|
||||
view = view.push(vpn_connections.add(widget::settings::item_row(vec![
|
||||
widget::text::body(fl!("no-vpn")).into(),
|
||||
])));
|
||||
} else {
|
||||
let known_networks = page.known_connections.iter().fold(
|
||||
vpn_connections,
|
||||
|networks, (uuid, connection)| {
|
||||
let is_connected = active_conns.iter().any(|conn| match conn {
|
||||
ActiveConnectionInfo::Vpn { name, .. } => {
|
||||
name.as_str() == connection.id.as_str()
|
||||
}
|
||||
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let (connect_txt, connect_msg) = if is_connected {
|
||||
(§ion.descriptions[connected_txt], None)
|
||||
} else {
|
||||
(
|
||||
§ion.descriptions[connect_txt],
|
||||
Some(Message::Activate(uuid.clone())),
|
||||
)
|
||||
};
|
||||
|
||||
let identifier =
|
||||
widget::text::body(connection.id.as_str()).wrap(Wrap::Glyph);
|
||||
|
||||
let connect: Element<'_, Message> = if let Some(msg) = connect_msg {
|
||||
widget::button::text(connect_txt).on_press(msg).into()
|
||||
} else {
|
||||
widget::text::body(connect_txt)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let view_more_button =
|
||||
widget::button::icon(widget::icon::from_name("view-more-symbolic"));
|
||||
|
||||
let view_more: Option<Element<_>> = if page
|
||||
.view_more_popup
|
||||
.as_deref()
|
||||
.map_or(false, |id| id == uuid.as_ref())
|
||||
{
|
||||
widget::popover(view_more_button.on_press(Message::ViewMore(None)))
|
||||
.position(widget::popover::Position::Bottom)
|
||||
.on_close(Message::ViewMore(None))
|
||||
.popup({
|
||||
widget::column()
|
||||
.push_maybe(is_connected.then(|| {
|
||||
popup_button(
|
||||
Message::Deactivate(uuid.clone()),
|
||||
§ion.descriptions[disconnect_txt],
|
||||
)
|
||||
}))
|
||||
.push(popup_button(
|
||||
Message::Settings(uuid.clone()),
|
||||
§ion.descriptions[settings_txt],
|
||||
))
|
||||
.push(popup_button(
|
||||
Message::RemoveProfileRequest(uuid.clone()),
|
||||
§ion.descriptions[remove_txt],
|
||||
))
|
||||
.width(Length::Fixed(200.0))
|
||||
.apply(widget::container)
|
||||
.style(cosmic::style::Container::Dialog)
|
||||
})
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else {
|
||||
view_more_button
|
||||
.on_press(Message::ViewMore(Some(uuid.clone())))
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
};
|
||||
|
||||
let controls = widget::row::with_capacity(2)
|
||||
.push(connect)
|
||||
.push_maybe(view_more)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let widget = widget::settings::item_row(vec![
|
||||
identifier.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
controls.into(),
|
||||
]);
|
||||
|
||||
networks.add(widget)
|
||||
},
|
||||
);
|
||||
|
||||
view = view.push(known_networks);
|
||||
}
|
||||
|
||||
view.spacing(spacing.space_l)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Vpn)
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_button<'a>(message: Message, text: &'a str) -> Element<'a, Message> {
|
||||
widget::text::body(text)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(widget::button)
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(cosmic::theme::Button::MenuItem)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update_state(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
match NetworkManagerState::new(&conn).await {
|
||||
Ok(state) => Message::UpdateState(state),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_devices(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard);
|
||||
|
||||
match network_manager::devices::list(&conn, filter).await {
|
||||
Ok(devices) => Message::UpdateDevices(devices),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn add_network() -> Command<crate::app::Message> {
|
||||
let Some(dir) = dirs::download_dir().or_else(dirs::home_dir) else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
cosmic::dialog::file_chooser::open::Dialog::new()
|
||||
.directory(dir)
|
||||
.title(fl!("vpn", "select-file"))
|
||||
.filter(
|
||||
FileFilter::new("OpenVPN")
|
||||
.mimetype("application/x-openvpn-profile")
|
||||
.glob("*.ovpn"),
|
||||
)
|
||||
.open_file()
|
||||
.then(|result| async move {
|
||||
match result {
|
||||
Ok(response) => {
|
||||
_ = super::nm_add_vpn_file("openvpn", response.url().path()).await;
|
||||
Message::Refresh
|
||||
}
|
||||
Err(why) => {
|
||||
return Message::Error(why.to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
.apply(cosmic::command::future)
|
||||
}
|
||||
|
||||
fn connection_settings(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
let settings = async move {
|
||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
||||
|
||||
_ = settings.load_connections(&[]).await;
|
||||
|
||||
let settings = settings
|
||||
// Get a list of known connections.
|
||||
.list_connections()
|
||||
.await?
|
||||
// Prepare for wrapping in a concurrent stream.
|
||||
.into_iter()
|
||||
.map(|conn| async move { conn })
|
||||
// Create a concurrent stream for each connection.
|
||||
.apply(futures::stream::FuturesOrdered::from_iter)
|
||||
// Concurrently fetch settings for each connection, and filter for VPN.
|
||||
.filter_map(|conn| async move {
|
||||
let settings = conn.get_settings().await.ok()?;
|
||||
|
||||
let (connection, vpn) = settings.get("connection").zip(settings.get("vpn"))?;
|
||||
|
||||
if connection.get("type")?.downcast_ref::<String>().ok()? != "vpn" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
||||
|
||||
let (username, connection_type, password_flag) = vpn
|
||||
.get("data")
|
||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
|
||||
.map(|dict| {
|
||||
let (mut connection_type, mut password_flag) = (None, None);
|
||||
|
||||
let username = dict
|
||||
.get::<String, String>(&String::from("username"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|value| !value.is_empty());
|
||||
|
||||
if let Some("password") = dict
|
||||
.get::<String, String>(&String::from("connection-type"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref()
|
||||
{
|
||||
connection_type = Some(ConnectionType::Password);
|
||||
|
||||
password_flag = dict
|
||||
.get::<String, String>(&String::from("password-flags"))
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| match value.as_str() {
|
||||
"0" => Some(PasswordFlag::None),
|
||||
"1" => Some(PasswordFlag::AgentOwned),
|
||||
"2" => Some(PasswordFlag::NotSaved),
|
||||
"4" => Some(PasswordFlag::NotRequired),
|
||||
_ => None,
|
||||
});
|
||||
}
|
||||
|
||||
(username, connection_type, password_flag)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Some((
|
||||
Arc::from(uuid),
|
||||
VpnConnectionSettings {
|
||||
id,
|
||||
connection_type,
|
||||
password_flag,
|
||||
username,
|
||||
},
|
||||
))
|
||||
})
|
||||
// Reduce the settings list into
|
||||
.fold(IndexMap::new(), |mut set, (uuid, data)| async move {
|
||||
set.insert(uuid, data);
|
||||
set
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok::<_, zbus::Error>(settings)
|
||||
};
|
||||
|
||||
cosmic::command::future(async move {
|
||||
settings
|
||||
.await
|
||||
.context("failed to get connection settings")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
Message::KnownConnections,
|
||||
)
|
||||
})
|
||||
}
|
||||
49
cosmic-settings/src/pages/networking/vpn/nmcli.rs
Normal file
49
cosmic-settings/src/pages/networking/vpn/nmcli.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use as_result::IntoResult;
|
||||
use std::io;
|
||||
|
||||
pub async fn set_username(connection_name: &str, username: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&["con", "mod", connection_name, "vpn.user-name", username])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
|
||||
pub async fn set_password_flags_none(connection_name: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&[
|
||||
"con",
|
||||
"mod",
|
||||
connection_name,
|
||||
"+vpn.data",
|
||||
"password-flags=0",
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
|
||||
pub async fn set_password(connection_name: &str, password: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&[
|
||||
"con",
|
||||
"mod",
|
||||
&connection_name,
|
||||
"vpn.secrets",
|
||||
&format!("password={password}"),
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
|
||||
pub async fn connect(connection_name: &str) -> io::Result<()> {
|
||||
tokio::process::Command::new("nmcli")
|
||||
.args(&["con", "up", &connection_name])
|
||||
.status()
|
||||
.await
|
||||
.and_then(IntoResult::into_result)
|
||||
}
|
||||
787
cosmic-settings/src/pages/networking/wifi.rs
Normal file
787
cosmic-settings/src/pages/networking/wifi.rs
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use anyhow::Context;
|
||||
use cosmic::{
|
||||
iced::{alignment, Length},
|
||||
iced_core::text::Wrap,
|
||||
prelude::CollectionWidget,
|
||||
widget::{self, icon},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use cosmic_settings_subscriptions::network_manager::{
|
||||
self, available_wifi::AccessPoint, current_networks::ActiveConnectionInfo, NetworkManagerState,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use secure_string::SecureString;
|
||||
use slab::Slab;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Add a network connection with nm-connection-editor
|
||||
AddNetwork,
|
||||
/// Cancels a dialog.
|
||||
CancelDialog,
|
||||
/// Connect to a WiFi network access point.
|
||||
Connect(network_manager::SSID),
|
||||
/// Connect with a password
|
||||
ConnectWithPassword,
|
||||
/// Settings for known connections.
|
||||
ConnectionSettings(BTreeMap<Box<str>, Box<str>>),
|
||||
/// Disconnect from an access point.
|
||||
Disconnect(network_manager::SSID),
|
||||
/// An error occurred.
|
||||
Error(String),
|
||||
/// Create a dialog to ask for confirmation on forgetting a connection.
|
||||
ForgetRequest(network_manager::SSID),
|
||||
/// Forget a known access point.
|
||||
Forget(network_manager::SSID),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(
|
||||
(
|
||||
zbus::Connection,
|
||||
tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
),
|
||||
),
|
||||
/// Request an auth dialog
|
||||
PasswordRequest(network_manager::SSID),
|
||||
/// Update the password from the dialog
|
||||
PasswordUpdate(SecureString),
|
||||
/// Opens settings page for the access point.
|
||||
Settings(network_manager::SSID),
|
||||
/// Toggles visibility of the password input
|
||||
TogglePasswordVisibility,
|
||||
/// Update NetworkManagerState
|
||||
UpdateState(NetworkManagerState),
|
||||
/// Update the devices lists
|
||||
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
|
||||
/// Display more options for an access point
|
||||
ViewMore(Option<network_manager::SSID>),
|
||||
/// Toggle WiFi access
|
||||
WiFiEnable(bool),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::WiFi(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::WiFi(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum WiFiDialog {
|
||||
Forget(network_manager::SSID),
|
||||
Password {
|
||||
ssid: network_manager::SSID,
|
||||
password: SecureString,
|
||||
password_hidden: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<WiFiDialog>,
|
||||
view_more_popup: Option<network_manager::SSID>,
|
||||
connecting: BTreeSet<network_manager::SSID>,
|
||||
ssid_to_uuid: BTreeMap<Box<str>, Box<str>>,
|
||||
/// Withhold device update if the view more popup is shown.
|
||||
withheld_devices: Option<Vec<network_manager::devices::DeviceInfo>>,
|
||||
/// Withhold state update if the view more popup is shown.
|
||||
withheld_state: Option<NetworkManagerState>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmState {
|
||||
conn: zbus::Connection,
|
||||
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
|
||||
state: network_manager::NetworkManagerState,
|
||||
devices: Vec<network_manager::devices::DeviceInfo>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new("wifi", "preferences-wireless-symbolic")
|
||||
.title(fl!("wifi"))
|
||||
.description(fl!("connections-and-profiles", variant = "wifi"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(devices_view())])
|
||||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
|
||||
self.dialog.as_ref().map(|dialog| match dialog {
|
||||
WiFiDialog::Password {
|
||||
password,
|
||||
password_hidden,
|
||||
..
|
||||
} => {
|
||||
let password = widget::text_input::secure_input(
|
||||
fl!("password"),
|
||||
password.unsecure(),
|
||||
Some(Message::TogglePasswordVisibility),
|
||||
*password_hidden,
|
||||
)
|
||||
.on_input(|input| Message::PasswordUpdate(SecureString::from(input)))
|
||||
.on_submit(Message::ConnectWithPassword);
|
||||
|
||||
let primary_action = widget::button::suggested(fl!("connect"))
|
||||
.on_press(Message::ConnectWithPassword);
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("auth-dialog"))
|
||||
.icon(icon::from_name("preferences-wireless-symbolic").size(64))
|
||||
.body(fl!("auth-dialog", "wifi-description"))
|
||||
.control(password)
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi)
|
||||
}
|
||||
|
||||
WiFiDialog::Forget(ssid) => {
|
||||
let primary_action = widget::button::destructive(fl!("forget"))
|
||||
.on_press(Message::Forget(ssid.clone()));
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("forget-dialog"))
|
||||
.icon(icon::from_name("dialog-information").size(64))
|
||||
.body(fl!("forget-dialog", "description"))
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
.trailing_icon(icon::from_name("window-pop-out-symbolic"))
|
||||
.on_press(Message::AddNetwork)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_page::Entity,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) -> cosmic::Command<crate::pages::Message> {
|
||||
if self.nm_task.is_none() {
|
||||
return cosmic::command::future(async move {
|
||||
zbus::Connection::system()
|
||||
.await
|
||||
.context("failed to create system dbus connection")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
|conn| Message::NetworkManagerConnect((conn, sender.clone())),
|
||||
)
|
||||
.apply(crate::pages::Message::WiFi)
|
||||
});
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
self.view_more_popup = None;
|
||||
self.nm_state = None;
|
||||
self.ssid_to_uuid.clear();
|
||||
self.connecting.clear();
|
||||
self.withheld_state = None;
|
||||
self.withheld_devices = None;
|
||||
|
||||
if let Some(cancel) = self.nm_task.take() {
|
||||
_ = cancel.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::NetworkManager(network_manager::Event::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
success,
|
||||
}) => {
|
||||
if !success {
|
||||
tracing::error!(request = ?req, "network-manager request failed");
|
||||
}
|
||||
|
||||
match req {
|
||||
network_manager::Request::Password(ssid, _) => {
|
||||
if success {
|
||||
self.connecting.remove(&ssid);
|
||||
} else {
|
||||
// Request to retry
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
network_manager::Request::SelectAccessPoint(ssid) => {
|
||||
self.connecting.remove(&ssid);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.update_state(state);
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return update_devices(conn.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_state(state);
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return connection_settings(conn.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns
|
||||
| network_manager::Event::Devices
|
||||
| network_manager::Event::WiFiEnabled(_)
|
||||
| network_manager::Event::WirelessAccessPoints,
|
||||
) => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectionSettings(settings) => {
|
||||
self.ssid_to_uuid = settings;
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
}) => {
|
||||
self.nm_state = Some(NmState {
|
||||
conn: conn.clone(),
|
||||
sender,
|
||||
state,
|
||||
devices: Vec::new(),
|
||||
});
|
||||
|
||||
return update_devices(conn);
|
||||
}
|
||||
|
||||
Message::AddNetwork => {
|
||||
tokio::task::spawn(super::nm_add_wifi());
|
||||
}
|
||||
|
||||
Message::Connect(ssid) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
self.connecting.insert(ssid.clone());
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::SelectAccessPoint(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::PasswordRequest(ssid) => {
|
||||
self.dialog = Some(WiFiDialog::Password {
|
||||
ssid,
|
||||
password: SecureString::from(""),
|
||||
password_hidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
Message::PasswordUpdate(pass) => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut password, ..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ConnectWithPassword => {
|
||||
let Some(dialog) = self.dialog.take() else {
|
||||
return Command::none();
|
||||
};
|
||||
|
||||
if let WiFiDialog::Password { ssid, password, .. } = dialog {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
self.connecting.insert(ssid.clone());
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Password(ssid, password));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::TogglePasswordVisibility => {
|
||||
if let Some(WiFiDialog::Password {
|
||||
ref mut password_hidden,
|
||||
..
|
||||
}) = self.dialog
|
||||
{
|
||||
*password_hidden = !*password_hidden;
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(ssid) => {
|
||||
self.view_more_popup = ssid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Disconnect(ssid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Disconnect(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ForgetRequest(ssid) => {
|
||||
self.dialog = Some(WiFiDialog::Forget(ssid));
|
||||
self.view_more_popup = None;
|
||||
}
|
||||
|
||||
Message::Forget(ssid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::Forget(ssid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(ssid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
if let Some(uuid) = self.ssid_to_uuid.get(ssid.as_ref()).cloned() {
|
||||
tokio::task::spawn(
|
||||
async move { super::nm_edit_connection(uuid.as_ref()).await },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Message::WiFiEnable(enable) => {
|
||||
if let Some(nm) = self.nm_state.as_mut() {
|
||||
_ = nm
|
||||
.sender
|
||||
.unbounded_send(network_manager::Request::SetWiFi(enable));
|
||||
_ = nm.sender.unbounded_send(network_manager::Request::Reload);
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why, "error in wifi settings page");
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect((conn, output)) => {
|
||||
self.connect(conn.clone(), output);
|
||||
|
||||
return connection_settings(conn);
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&mut self,
|
||||
conn: zbus::Connection,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) {
|
||||
if self.nm_task.is_none() {
|
||||
self.nm_task = Some(crate::utils::forward_event_loop(
|
||||
sender,
|
||||
|event| crate::pages::Message::WiFi(Message::NetworkManager(event)),
|
||||
move |tx| async move {
|
||||
futures::join!(
|
||||
network_manager::watch(conn.clone(), tx.clone()),
|
||||
network_manager::active_conns::watch(conn.clone(), tx.clone()),
|
||||
network_manager::wireless_enabled::watch(conn.clone(), tx.clone()),
|
||||
network_manager::watch_connections_changed(conn, tx)
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the view more popup and applies any withheld updates.
|
||||
fn close_popup_and_apply_updates(&mut self) {
|
||||
self.view_more_popup = None;
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if let Some(state) = self.withheld_state.take() {
|
||||
nm_state.state = state;
|
||||
}
|
||||
|
||||
if let Some(devices) = self.withheld_devices.take() {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_devices(&mut self, devices: Vec<network_manager::devices::DeviceInfo>) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_devices = Some(devices);
|
||||
} else {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_state(&mut self, state: NetworkManagerState) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_state = Some(state);
|
||||
} else {
|
||||
nm_state.state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn devices_view() -> Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
airplane_mode_txt = fl!("airplane-on");
|
||||
connect_txt = fl!("connect");
|
||||
connected_txt = fl!("connected");
|
||||
connecting_txt = fl!("connecting");
|
||||
disconnect_txt = fl!("disconnect");
|
||||
forget_txt = fl!("wifi", "forget");
|
||||
known_networks_txt = fl!("known-networks");
|
||||
no_networks_txt = fl!("no-networks");
|
||||
settings_txt = fl!("settings");
|
||||
visible_networks_txt = fl!("visible-networks");
|
||||
wifi_txt = fl!("wifi");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let Some(NmState { ref state, .. }) = page.nm_state else {
|
||||
return cosmic::widget::column().into();
|
||||
};
|
||||
|
||||
let theme = cosmic::theme::active();
|
||||
let spacing = &theme.cosmic().spacing;
|
||||
|
||||
let wifi_enable =
|
||||
widget::settings::item::builder(§ion.descriptions[wifi_txt]).control(
|
||||
widget::toggler(None, state.wifi_enabled, Message::WiFiEnable),
|
||||
);
|
||||
|
||||
let mut view = widget::column::with_capacity(4)
|
||||
.push(widget::list_column().add(wifi_enable))
|
||||
.push_maybe(state.airplane_mode.then(|| {
|
||||
widget::row::with_capacity(2)
|
||||
.push(icon::from_name("airplane-mode-symbolic"))
|
||||
.push(widget::text::body(§ion.descriptions[airplane_mode_txt]))
|
||||
.spacing(8)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
}));
|
||||
|
||||
if !state.airplane_mode
|
||||
&& state.known_access_points.is_empty()
|
||||
&& state.wireless_access_points.is_empty()
|
||||
{
|
||||
let no_networks_found =
|
||||
widget::container(widget::text::body(§ion.descriptions[no_networks_txt]))
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.width(Length::Fill);
|
||||
|
||||
view = view.push(no_networks_found);
|
||||
} else {
|
||||
let mut has_known = false;
|
||||
let mut has_visible = false;
|
||||
|
||||
// Create separate sections for known and visible networks.
|
||||
let (known_networks, visible_networks) = state.wireless_access_points.iter().fold(
|
||||
(
|
||||
widget::settings::view_section(§ion.descriptions[known_networks_txt]),
|
||||
widget::settings::view_section(§ion.descriptions[visible_networks_txt]),
|
||||
),
|
||||
|(mut known_networks, mut visible_networks), network| {
|
||||
let is_connected = is_connected(state, network);
|
||||
|
||||
let is_known = state
|
||||
.known_access_points
|
||||
.iter()
|
||||
.map(|known| known.ssid.as_ref())
|
||||
.chain(state.active_conns.iter().filter_map(|active| {
|
||||
if let ActiveConnectionInfo::WiFi { name, .. } = active {
|
||||
Some(name.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
.any(|known| known == network.ssid.as_ref());
|
||||
|
||||
// TODO: detect if access point is secured or not.
|
||||
let is_encrypted = true;
|
||||
|
||||
let (connect_txt, connect_msg) = if is_connected {
|
||||
(§ion.descriptions[connected_txt], None)
|
||||
} else if page.connecting.contains(&network.ssid) {
|
||||
(§ion.descriptions[connecting_txt], None)
|
||||
} else {
|
||||
(
|
||||
§ion.descriptions[connect_txt],
|
||||
Some(if is_known || !is_encrypted {
|
||||
Message::Connect(network.ssid.clone())
|
||||
} else {
|
||||
Message::PasswordRequest(network.ssid.clone())
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let identifier = widget::row::with_capacity(3)
|
||||
.push(widget::icon::from_name("network-wireless-good-symbolic"))
|
||||
.push_maybe(
|
||||
is_encrypted
|
||||
.then(|| widget::icon::from_name("connection-secure-symbolic")),
|
||||
)
|
||||
.push(widget::text::body(network.ssid.as_ref()).wrap(Wrap::Glyph))
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let connect: Element<'_, Message> = if let Some(msg) = connect_msg {
|
||||
widget::button::text(connect_txt).on_press(msg).into()
|
||||
} else {
|
||||
widget::text::body(connect_txt)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let view_more_button =
|
||||
widget::button::icon(widget::icon::from_name("view-more-symbolic"));
|
||||
|
||||
let view_more: Option<Element<_>> = if page
|
||||
.view_more_popup
|
||||
.as_deref()
|
||||
.map_or(false, |id| id == network.ssid.as_ref())
|
||||
{
|
||||
widget::popover(view_more_button.on_press(Message::ViewMore(None)))
|
||||
.position(widget::popover::Position::Bottom)
|
||||
.on_close(Message::ViewMore(None))
|
||||
.popup({
|
||||
widget::column()
|
||||
.push_maybe(is_connected.then(|| {
|
||||
popup_button(
|
||||
Message::Disconnect(network.ssid.clone()),
|
||||
§ion.descriptions[disconnect_txt],
|
||||
)
|
||||
}))
|
||||
.push(popup_button(
|
||||
Message::Settings(network.ssid.clone()),
|
||||
§ion.descriptions[settings_txt],
|
||||
))
|
||||
.push_maybe(is_known.then(|| {
|
||||
popup_button(
|
||||
Message::ForgetRequest(network.ssid.clone()),
|
||||
§ion.descriptions[forget_txt],
|
||||
)
|
||||
}))
|
||||
.width(Length::Fixed(170.0))
|
||||
.apply(widget::container)
|
||||
.style(cosmic::style::Container::Dialog)
|
||||
})
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else if is_known {
|
||||
view_more_button
|
||||
.on_press(Message::ViewMore(Some(network.ssid.clone())))
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let controls = widget::row::with_capacity(2)
|
||||
.push(connect)
|
||||
.push_maybe(view_more)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let widget = widget::settings::item_row(vec![
|
||||
identifier.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
controls.into(),
|
||||
]);
|
||||
|
||||
if is_known {
|
||||
has_known = true;
|
||||
known_networks = known_networks.add(widget);
|
||||
} else {
|
||||
has_visible = true;
|
||||
visible_networks = visible_networks.add(widget);
|
||||
}
|
||||
|
||||
(known_networks, visible_networks)
|
||||
},
|
||||
);
|
||||
|
||||
if has_known {
|
||||
view = view.push(known_networks);
|
||||
}
|
||||
|
||||
if has_visible {
|
||||
view = view.push(visible_networks);
|
||||
}
|
||||
};
|
||||
|
||||
view.spacing(spacing.space_l)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::WiFi)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_connected(state: &NetworkManagerState, network: &AccessPoint) -> bool {
|
||||
state.active_conns.iter().any(|active| {
|
||||
if let ActiveConnectionInfo::WiFi { ref name, .. } = active {
|
||||
*name == network.ssid.as_ref()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_button<'a>(message: Message, text: &'a str) -> Element<'a, Message> {
|
||||
widget::text::body(text)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(widget::button)
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(cosmic::theme::Button::MenuItem)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn connection_settings(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
let settings = async move {
|
||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
||||
|
||||
_ = settings.load_connections(&[]).await;
|
||||
|
||||
let settings = settings
|
||||
// Get a list of known connections.
|
||||
.list_connections()
|
||||
.await?
|
||||
// Prepare for wrapping in a concurrent stream.
|
||||
.into_iter()
|
||||
.map(|conn| async move { conn })
|
||||
// Create a concurrent stream for each connection.
|
||||
.apply(futures::stream::FuturesOrdered::from_iter)
|
||||
// Concurrently fetch settings for each connection.
|
||||
.filter_map(|conn| async move {
|
||||
conn.get_settings()
|
||||
.await
|
||||
.map(network_manager::Settings::new)
|
||||
.ok()
|
||||
})
|
||||
// Reduce the settings list into a SSID->UUID map.
|
||||
.fold(BTreeMap::new(), |mut set, settings| async move {
|
||||
if let Some(ref wifi) = settings.wifi {
|
||||
if let Some(ssid) = wifi
|
||||
.ssid
|
||||
.clone()
|
||||
.and_then(|ssid| String::from_utf8(ssid).ok())
|
||||
{
|
||||
if let Some(ref connection) = settings.connection {
|
||||
if let Some(uuid) = connection.uuid.clone() {
|
||||
set.insert(ssid.into(), uuid.into());
|
||||
return set;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok::<_, zbus::Error>(settings)
|
||||
};
|
||||
|
||||
cosmic::command::future(async move {
|
||||
settings
|
||||
.await
|
||||
.context("failed to get connection settings")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
Message::ConnectionSettings,
|
||||
)
|
||||
.apply(crate::pages::Message::WiFi)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_state(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
match NetworkManagerState::new(&conn).await {
|
||||
Ok(state) => Message::UpdateState(state),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_devices(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi);
|
||||
match network_manager::devices::list(&conn, filter).await {
|
||||
Ok(devices) => Message::UpdateDevices(devices),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,10 +1,690 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic_settings_page as page;
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
pub fn info() -> page::Info {
|
||||
page::Info::new("wired", "network-workgroup-symbolic")
|
||||
.title(fl!("wired"))
|
||||
.description(fl!("wired", "desc"))
|
||||
use anyhow::Context;
|
||||
use cosmic::{
|
||||
iced::{alignment, Length},
|
||||
iced_core::text::Wrap,
|
||||
prelude::CollectionWidget,
|
||||
widget::{self, icon},
|
||||
Apply, Command, Element,
|
||||
};
|
||||
use cosmic_settings_page::{self as page, section, Section};
|
||||
use cosmic_settings_subscriptions::network_manager::{
|
||||
self, current_networks::ActiveConnectionInfo, devices::DeviceState, NetworkManagerState,
|
||||
};
|
||||
use slab::Slab;
|
||||
|
||||
pub type ConnectionId = Arc<str>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Activate a connection
|
||||
Activate(ConnectionId),
|
||||
/// Add a network connection with nm-connection-editor
|
||||
AddNetwork,
|
||||
/// Cancels an active dialog.
|
||||
CancelDialog,
|
||||
/// Deactivate a connection.
|
||||
Deactivate(ConnectionId),
|
||||
/// An error occurred.
|
||||
Error(String),
|
||||
/// An update from the network manager daemon
|
||||
NetworkManager(network_manager::Event),
|
||||
/// Successfully connected to the system dbus.
|
||||
NetworkManagerConnect(
|
||||
(
|
||||
zbus::Connection,
|
||||
tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
),
|
||||
),
|
||||
/// Refresh devices and their connection profiles
|
||||
Refresh,
|
||||
/// Create a dialog to ask for confirmation of removal.
|
||||
RemoveProfileRequest(ConnectionId),
|
||||
/// Remove a connection profile
|
||||
RemoveProfile(ConnectionId),
|
||||
/// Selects a device to display connections from
|
||||
SelectDevice(Arc<network_manager::devices::DeviceInfo>),
|
||||
/// Opens settings page for the access point.
|
||||
Settings(ConnectionId),
|
||||
/// Update NetworkManagerState
|
||||
UpdateState(NetworkManagerState),
|
||||
/// Update the devices lists
|
||||
UpdateDevices(Vec<network_manager::devices::DeviceInfo>),
|
||||
/// Display more options for an access point
|
||||
ViewMore(Option<ConnectionId>),
|
||||
}
|
||||
|
||||
impl From<Message> for crate::app::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Wired(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for crate::pages::Message {
|
||||
fn from(message: Message) -> Self {
|
||||
crate::pages::Message::Wired(message)
|
||||
}
|
||||
}
|
||||
|
||||
pub type InterfaceId = String;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum WiredDialog {
|
||||
RemoveProfile(ConnectionId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Page {
|
||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
nm_state: Option<NmState>,
|
||||
dialog: Option<WiredDialog>,
|
||||
/// When defined, displays connections for the specific device.
|
||||
active_device: Option<Arc<network_manager::devices::DeviceInfo>>,
|
||||
/// Tracks which connections are in the act of connecting.
|
||||
connecting: BTreeSet<ConnectionId>,
|
||||
/// Displays a popup when set.
|
||||
view_more_popup: Option<ConnectionId>,
|
||||
/// Withhold device update if the view more popup is shown.
|
||||
withheld_devices: Option<Vec<Arc<network_manager::devices::DeviceInfo>>>,
|
||||
/// Withhold active connections update if the view more popup is shown.
|
||||
withheld_active_conns: Option<Vec<ActiveConnectionInfo>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NmState {
|
||||
conn: zbus::Connection,
|
||||
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
|
||||
active_conns: Vec<ActiveConnectionInfo>,
|
||||
devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
|
||||
}
|
||||
|
||||
impl page::AutoBind<crate::pages::Message> for Page {}
|
||||
|
||||
impl page::Page<crate::pages::Message> for Page {
|
||||
fn info(&self) -> cosmic_settings_page::Info {
|
||||
page::Info::new("wired", "preferences-wired-symbolic")
|
||||
.title(fl!("wired"))
|
||||
.description(fl!("connections-and-profiles", variant = "wired"))
|
||||
}
|
||||
|
||||
fn content(
|
||||
&self,
|
||||
sections: &mut slotmap::SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
Some(vec![sections.insert(devices_view())])
|
||||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<crate::pages::Message>> {
|
||||
self.dialog.as_ref().map(|dialog| match dialog {
|
||||
WiredDialog::RemoveProfile(uuid) => {
|
||||
let primary_action = widget::button::destructive(fl!("remove"))
|
||||
.on_press(Message::RemoveProfile(uuid.clone()));
|
||||
|
||||
let secondary_action =
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::CancelDialog);
|
||||
|
||||
widget::dialog(fl!("remove-connection-dialog"))
|
||||
.icon(icon::from_name("dialog-information").size(64))
|
||||
.body(fl!("remove-connection-dialog", "wired-description"))
|
||||
.primary_action(primary_action)
|
||||
.secondary_action(secondary_action)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Wired)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn header_view(&self) -> Option<cosmic::Element<'_, crate::pages::Message>> {
|
||||
Some(
|
||||
widget::button::standard(fl!("add-network"))
|
||||
.trailing_icon(icon::from_name("window-pop-out-symbolic"))
|
||||
.on_press(Message::AddNetwork)
|
||||
.apply(widget::container)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Wired),
|
||||
)
|
||||
}
|
||||
|
||||
fn on_enter(
|
||||
&mut self,
|
||||
_page: cosmic_settings_page::Entity,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) -> cosmic::Command<crate::pages::Message> {
|
||||
if self.nm_task.is_none() {
|
||||
return cosmic::command::future(async move {
|
||||
zbus::Connection::system()
|
||||
.await
|
||||
.context("failed to create system dbus connection")
|
||||
.map_or_else(
|
||||
|why| Message::Error(why.to_string()),
|
||||
|conn| Message::NetworkManagerConnect((conn, sender.clone())),
|
||||
)
|
||||
.apply(crate::pages::Message::Wired)
|
||||
});
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn on_leave(&mut self) -> Command<crate::pages::Message> {
|
||||
self.active_device = None;
|
||||
self.view_more_popup = None;
|
||||
self.nm_state = None;
|
||||
self.withheld_active_conns = None;
|
||||
self.withheld_devices = None;
|
||||
self.connecting.clear();
|
||||
|
||||
if let Some(cancel) = self.nm_task.take() {
|
||||
_ = cancel.send(());
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn title(&self) -> Option<&str> {
|
||||
self.active_device
|
||||
.as_ref()
|
||||
.map(|device| device.interface.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn update(&mut self, message: Message) -> Command<crate::app::Message> {
|
||||
match message {
|
||||
Message::NetworkManager(network_manager::Event::RequestResponse {
|
||||
req,
|
||||
state,
|
||||
success,
|
||||
}) => {
|
||||
if !success {
|
||||
tracing::error!(request = ?req, "network-manager request failed");
|
||||
}
|
||||
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
let conn = conn.clone();
|
||||
self.update_active_conns(state);
|
||||
return update_devices(conn);
|
||||
}
|
||||
}
|
||||
|
||||
Message::UpdateDevices(devices) => {
|
||||
self.update_devices(devices);
|
||||
}
|
||||
|
||||
Message::UpdateState(state) => {
|
||||
self.update_active_conns(state);
|
||||
}
|
||||
|
||||
Message::SelectDevice(device) => {
|
||||
self.active_device = Some(device);
|
||||
}
|
||||
|
||||
Message::NetworkManager(
|
||||
network_manager::Event::ActiveConns | network_manager::Event::Devices,
|
||||
) => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::NetworkManager(network_manager::Event::Init {
|
||||
conn,
|
||||
sender,
|
||||
state,
|
||||
}) => {
|
||||
self.nm_state = Some(NmState {
|
||||
conn: conn.clone(),
|
||||
sender,
|
||||
devices: Vec::new(),
|
||||
active_conns: state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Wired { .. }))
|
||||
.collect(),
|
||||
});
|
||||
|
||||
return update_devices(conn);
|
||||
}
|
||||
|
||||
Message::NetworkManager(_event) => (),
|
||||
|
||||
Message::AddNetwork => {
|
||||
return cosmic::command::future(async move {
|
||||
_ = super::nm_add_wired().await;
|
||||
// TODO: Update when iced is rebased to use then method.
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
|
||||
Message::Activate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
if let Some(NmState {
|
||||
ref devices,
|
||||
ref sender,
|
||||
..
|
||||
}) = self.nm_state
|
||||
{
|
||||
for device in devices {
|
||||
let device_conn = device
|
||||
.available_connections
|
||||
.iter()
|
||||
.find(|conn| conn.uuid.as_ref() == uuid.as_ref());
|
||||
|
||||
if let Some(device_conn) = device_conn {
|
||||
let device_path = device.path.clone();
|
||||
let conn_path = device_conn.path.clone();
|
||||
|
||||
_ = sender.unbounded_send(network_manager::Request::Activate(
|
||||
device_path,
|
||||
conn_path,
|
||||
));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::Deactivate(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Deactivate(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::RemoveProfileRequest(uuid) => {
|
||||
self.view_more_popup = None;
|
||||
self.dialog = Some(WiredDialog::RemoveProfile(uuid));
|
||||
}
|
||||
|
||||
Message::RemoveProfile(uuid) => {
|
||||
self.dialog = None;
|
||||
self.close_popup_and_apply_updates();
|
||||
if let Some(NmState { ref sender, .. }) = self.nm_state {
|
||||
_ = sender.unbounded_send(network_manager::Request::Remove(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
Message::ViewMore(uuid) => {
|
||||
self.view_more_popup = uuid;
|
||||
if self.view_more_popup.is_none() {
|
||||
self.close_popup_and_apply_updates();
|
||||
}
|
||||
}
|
||||
|
||||
Message::Settings(uuid) => {
|
||||
self.close_popup_and_apply_updates();
|
||||
|
||||
return cosmic::command::future(async move {
|
||||
_ = super::nm_edit_connection(uuid.as_ref()).await;
|
||||
// TODO: Update when iced is rebased to use then method.
|
||||
Message::Refresh
|
||||
});
|
||||
}
|
||||
|
||||
Message::Refresh => {
|
||||
if let Some(NmState { ref conn, .. }) = self.nm_state {
|
||||
return cosmic::command::batch(vec![
|
||||
update_state(conn.clone()),
|
||||
update_devices(conn.clone()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Message::CancelDialog => {
|
||||
self.dialog = None;
|
||||
}
|
||||
|
||||
Message::Error(why) => {
|
||||
tracing::error!(why, "error in wired settings page");
|
||||
}
|
||||
|
||||
Message::NetworkManagerConnect((conn, output)) => {
|
||||
self.connect(conn.clone(), output);
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&mut self,
|
||||
conn: zbus::Connection,
|
||||
sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
|
||||
) {
|
||||
if self.nm_task.is_none() {
|
||||
self.nm_task = Some(crate::utils::forward_event_loop(
|
||||
sender,
|
||||
|event| crate::pages::Message::Wired(Message::NetworkManager(event)),
|
||||
move |tx| async move {
|
||||
futures::join!(
|
||||
network_manager::watch(conn.clone(), tx.clone()),
|
||||
network_manager::active_conns::watch(conn.clone(), tx.clone()),
|
||||
network_manager::devices::watch(conn, true, tx)
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the view more popup and applies any withheld updates.
|
||||
fn close_popup_and_apply_updates(&mut self) {
|
||||
self.view_more_popup = None;
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
if let Some(active_conns) = self.withheld_active_conns.take() {
|
||||
nm_state.active_conns = active_conns;
|
||||
}
|
||||
|
||||
if let Some(devices) = self.withheld_devices.take() {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_active_device();
|
||||
}
|
||||
|
||||
fn update_active_device(&mut self) {
|
||||
if let Some((nm_state, active)) = self.nm_state.as_ref().zip(self.active_device.as_ref()) {
|
||||
self.active_device = nm_state
|
||||
.devices
|
||||
.iter()
|
||||
.find(|device| device.path == active.path)
|
||||
.map(Arc::clone);
|
||||
}
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_devices(&mut self, devices: Vec<network_manager::devices::DeviceInfo>) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
let devices = devices.into_iter().map(Arc::new).collect();
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_devices = Some(devices);
|
||||
} else {
|
||||
nm_state.devices = devices;
|
||||
}
|
||||
}
|
||||
|
||||
self.update_active_device();
|
||||
}
|
||||
|
||||
/// Withholds updates if the view more popup is displayed.
|
||||
fn update_active_conns(&mut self, state: NetworkManagerState) {
|
||||
if let Some(ref mut nm_state) = self.nm_state {
|
||||
let conns = state
|
||||
.active_conns
|
||||
.into_iter()
|
||||
.filter(|info| matches!(info, ActiveConnectionInfo::Wired { .. }))
|
||||
.collect();
|
||||
|
||||
if self.view_more_popup.is_some() {
|
||||
self.withheld_active_conns = Some(conns);
|
||||
} else {
|
||||
nm_state.active_conns = conns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn device_view<'a>(
|
||||
&'a self,
|
||||
spacing: &cosmic::cosmic_theme::Spacing,
|
||||
nm_state: &'a NmState,
|
||||
connect_txt: &'a str,
|
||||
connected_txt: &'a str,
|
||||
disconnect_txt: &'a str,
|
||||
remove_txt: &'a str,
|
||||
settings_txt: &'a str,
|
||||
wired_conns_txt: &'a str,
|
||||
device: &'a network_manager::devices::DeviceInfo,
|
||||
) -> Element<'a, Message> {
|
||||
let has_multiple_connection_profiles = device.available_connections.len() > 1;
|
||||
let header_txt = format!("{}", wired_conns_txt);
|
||||
|
||||
device
|
||||
.available_connections
|
||||
.iter()
|
||||
.fold(
|
||||
widget::settings::view_section(header_txt),
|
||||
|networks, connection| {
|
||||
let is_connected = nm_state.active_conns.iter().any(|conn| match conn {
|
||||
ActiveConnectionInfo::Wired { name, .. } => {
|
||||
name.as_str() == connection.id.as_str()
|
||||
}
|
||||
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let (connect_txt, connect_msg) = if is_connected {
|
||||
(connected_txt, None)
|
||||
} else {
|
||||
(
|
||||
connect_txt,
|
||||
Some(Message::Activate(connection.uuid.clone())),
|
||||
)
|
||||
};
|
||||
|
||||
let identifier = widget::text::body(&connection.id).wrap(Wrap::Glyph);
|
||||
|
||||
let connect: Element<'_, Message> = if let Some(msg) = connect_msg {
|
||||
widget::button::text(connect_txt).on_press(msg).into()
|
||||
} else {
|
||||
widget::text::body(connect_txt)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
let view_more_button =
|
||||
widget::button::icon(widget::icon::from_name("view-more-symbolic"));
|
||||
|
||||
let view_more: Option<Element<_>> = if self
|
||||
.view_more_popup
|
||||
.as_deref()
|
||||
.map_or(false, |id| id == connection.uuid.as_ref())
|
||||
{
|
||||
widget::popover(view_more_button.on_press(Message::ViewMore(None)))
|
||||
.position(widget::popover::Position::Bottom)
|
||||
.on_close(Message::ViewMore(None))
|
||||
.popup({
|
||||
widget::column()
|
||||
.push_maybe(is_connected.then(|| {
|
||||
popup_button(
|
||||
Message::Deactivate(connection.uuid.clone()),
|
||||
&disconnect_txt,
|
||||
)
|
||||
}))
|
||||
.push(popup_button(
|
||||
Message::Settings(connection.uuid.clone()),
|
||||
&settings_txt,
|
||||
))
|
||||
.push_maybe(has_multiple_connection_profiles.then(|| {
|
||||
popup_button(
|
||||
Message::RemoveProfileRequest(connection.uuid.clone()),
|
||||
&remove_txt,
|
||||
)
|
||||
}))
|
||||
.width(Length::Fixed(200.0))
|
||||
.apply(widget::container)
|
||||
.style(cosmic::style::Container::Dialog)
|
||||
})
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
} else {
|
||||
view_more_button
|
||||
.on_press(Message::ViewMore(Some(connection.uuid.clone())))
|
||||
.apply(|e| Some(Element::from(e)))
|
||||
};
|
||||
|
||||
let controls = widget::row::with_capacity(2)
|
||||
.push(connect)
|
||||
.push_maybe(view_more)
|
||||
.align_items(alignment::Alignment::Center)
|
||||
.spacing(spacing.space_xxs);
|
||||
|
||||
let widget = widget::settings::item_row(vec![
|
||||
identifier.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
controls.into(),
|
||||
]);
|
||||
|
||||
networks.add(widget)
|
||||
},
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn device_list_view<'a>(
|
||||
&'a self,
|
||||
_spacing: &cosmic::cosmic_theme::Spacing,
|
||||
nm_state: &'a NmState,
|
||||
devices_txt: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
nm_state
|
||||
.devices
|
||||
.iter()
|
||||
.fold(
|
||||
widget::settings::view_section(devices_txt),
|
||||
|section, device| {
|
||||
let is_unplugged = matches!(device.state, DeviceState::Unavailable);
|
||||
|
||||
let device_list =
|
||||
cosmic::widget::settings::item::builder(device.interface.as_str())
|
||||
.description(match device.state {
|
||||
DeviceState::Activated => fl!("network-device-state", "activated"),
|
||||
DeviceState::Config => fl!("network-device-state", "config"),
|
||||
DeviceState::Deactivating => {
|
||||
fl!("network-device-state", "deactivating")
|
||||
}
|
||||
DeviceState::Disconnected => {
|
||||
fl!("network-device-state", "disconnected")
|
||||
}
|
||||
DeviceState::Failed => fl!("network-device-state", "failed"),
|
||||
DeviceState::IpCheck => fl!("network-device-state", "ip-check"),
|
||||
DeviceState::IpConfig => fl!("network-device-state", "ip-config"),
|
||||
DeviceState::NeedAuth => fl!("network-device-state", "need-auth"),
|
||||
DeviceState::Prepare => fl!("network-device-state", "prepare"),
|
||||
DeviceState::Secondaries => {
|
||||
fl!("network-device-state", "secondaries")
|
||||
}
|
||||
DeviceState::Unavailable => {
|
||||
fl!("network-device-state", "unplugged")
|
||||
}
|
||||
DeviceState::Unknown => fl!("network-device-state", "unknown"),
|
||||
DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"),
|
||||
})
|
||||
.icon(icon::from_name("network-wired-symbolic").size(32))
|
||||
.control(icon::from_name("go-next-symbolic").size(20))
|
||||
.spacing(16)
|
||||
.apply(widget::container)
|
||||
.padding([16, 14])
|
||||
.style(cosmic::theme::Container::List)
|
||||
.apply(widget::button)
|
||||
.padding(0)
|
||||
.style(cosmic::theme::Button::Transparent)
|
||||
.on_press_maybe(if is_unplugged {
|
||||
None
|
||||
} else {
|
||||
Some(Message::SelectDevice(device.clone()))
|
||||
});
|
||||
|
||||
section.add(device_list)
|
||||
},
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn devices_view() -> Section<crate::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
wired_conns_txt = fl!("wired", "connections");
|
||||
wired_devices_txt = fl!("wired", "devices");
|
||||
remove_txt = fl!("wired", "remove");
|
||||
connect_txt = fl!("connect");
|
||||
connected_txt = fl!("connected");
|
||||
settings_txt = fl!("settings");
|
||||
disconnect_txt = fl!("disconnect");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.descriptions(descriptions)
|
||||
.view::<Page>(move |_binder, page, section| {
|
||||
let Some(ref nm_state) = page.nm_state else {
|
||||
return cosmic::widget::column().into();
|
||||
};
|
||||
|
||||
let theme = cosmic::theme::active();
|
||||
let spacing = &theme.cosmic().spacing;
|
||||
|
||||
let mut view = widget::column::with_capacity(4);
|
||||
|
||||
// Displays device connections if a device is selected, or only device exists.
|
||||
let active_device = page
|
||||
.active_device
|
||||
.as_ref()
|
||||
.or_else(|| (nm_state.devices.len() == 1).then(|| nm_state.devices.get(0))?)
|
||||
.filter(|device| !matches!(device.state, DeviceState::Unavailable));
|
||||
|
||||
view = match active_device {
|
||||
Some(device) => view.push(page.device_view(
|
||||
spacing,
|
||||
nm_state,
|
||||
§ion.descriptions[connect_txt],
|
||||
§ion.descriptions[connected_txt],
|
||||
§ion.descriptions[disconnect_txt],
|
||||
§ion.descriptions[remove_txt],
|
||||
§ion.descriptions[settings_txt],
|
||||
§ion.descriptions[wired_conns_txt],
|
||||
device,
|
||||
)),
|
||||
|
||||
None => view.push(page.device_list_view(
|
||||
spacing,
|
||||
nm_state,
|
||||
§ion.descriptions[wired_devices_txt],
|
||||
)),
|
||||
};
|
||||
|
||||
view.spacing(spacing.space_l)
|
||||
.apply(Element::from)
|
||||
.map(crate::pages::Message::Wired)
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_button<'a>(message: Message, text: &'a str) -> Element<'a, Message> {
|
||||
widget::text::body(text)
|
||||
.vertical_alignment(alignment::Vertical::Center)
|
||||
.apply(widget::button)
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(cosmic::theme::Button::MenuItem)
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn update_state(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
match NetworkManagerState::new(&conn).await {
|
||||
Ok(state) => Message::UpdateState(state),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_devices(conn: zbus::Connection) -> Command<crate::app::Message> {
|
||||
cosmic::command::future(async move {
|
||||
let filter =
|
||||
|device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet);
|
||||
|
||||
match network_manager::devices::list(&conn, filter).await {
|
||||
Ok(devices) => Message::UpdateDevices(devices),
|
||||
Err(why) => Message::Error(why.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,3 +46,15 @@ pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + Send + 'st
|
|||
|
||||
cancel_tx
|
||||
}
|
||||
|
||||
/// Creates a slab with predefined items
|
||||
#[macro_export]
|
||||
macro_rules! slab {
|
||||
( $descriptions:ident { $( $txt_id:ident = $txt_expr:expr; )+ } ) => {
|
||||
let mut $descriptions = Slab::new();
|
||||
|
||||
$(
|
||||
let $txt_id = $descriptions.insert($txt_expr);
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
debian/control
vendored
3
debian/control
vendored
|
|
@ -31,6 +31,9 @@ Depends:
|
|||
cosmic-randr,
|
||||
gettext,
|
||||
iso-codes,
|
||||
network-manager-gnome,
|
||||
network-manager-openvpn,
|
||||
network-manager-openvpn-gnome,
|
||||
xkb-data,
|
||||
Recommends: adw-gtk3
|
||||
Description: Settings application for the COSMIC desktop environment
|
||||
|
|
|
|||
3
debian/install
vendored
3
debian/install
vendored
|
|
@ -19,8 +19,11 @@
|
|||
/usr/share/applications/com.system76.CosmicSettings.Time.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Touchpad.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Users.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Vpn.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Wallpaper.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.WindowManagement.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Wired.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Wireless.desktop
|
||||
/usr/share/applications/com.system76.CosmicSettings.Workspaces.desktop
|
||||
/usr/share/metainfo/com.system76.CosmicSettings.metainfo.xml
|
||||
/usr/share/polkit-1/rules.d/cosmic-settings.rules
|
||||
|
|
|
|||
|
|
@ -4,10 +4,73 @@ unknown = Unknown
|
|||
|
||||
number = { $number }
|
||||
|
||||
## Networking: Wired
|
||||
## Network & Wireless
|
||||
|
||||
connections-and-profiles = { $variant ->
|
||||
[wired] Wired
|
||||
[wifi] Wi-Fi
|
||||
[vpn] VPN
|
||||
*[other] Unknown
|
||||
} connections and connection profiles.
|
||||
|
||||
add-network = Add network
|
||||
add-vpn = Add VPN
|
||||
airplane-on = Airplane mode is on.
|
||||
cable-unplugged = Cable unplugged
|
||||
connect = Connect
|
||||
connected = Connected
|
||||
connecting = Connecting…
|
||||
disconnect = Disconnect
|
||||
forget = Forget
|
||||
known-networks = Known Networks
|
||||
network-and-wireless = Network & Wireless
|
||||
no-networks = No networks have been found.
|
||||
no-vpn = No VPN connections available.
|
||||
password = Password
|
||||
remove = Remove
|
||||
settings = Settings
|
||||
username = Username
|
||||
visible-networks = Visible Networks
|
||||
|
||||
auth-dialog = Authentication Required
|
||||
.vpn-description = Enter the username and password required by the VPN service.
|
||||
.wifi-description = Enter the password or encryption key. You can also connect by pressing the “WPS” button on the router.
|
||||
|
||||
forget-dialog = Forget this Wi-Fi network?
|
||||
.description = You'll need to enter a password again to use this Wi-Fi network in the future.
|
||||
|
||||
network-device-state =
|
||||
.activated = Connected to network
|
||||
.config = Connecting to network
|
||||
.deactivating = Disconnecting from network
|
||||
.disconnected = Disconnected
|
||||
.failed = Failed to connect
|
||||
.ip-check = Checking connection
|
||||
.ip-config = Requesting IP and routing information
|
||||
.need-auth = Needs authentication
|
||||
.prepare = Preparing to connect to network
|
||||
.secondaries = Waiting for secondary connection
|
||||
.unavailable = Unavailable
|
||||
.unknown = Unknown state
|
||||
.unmanaged = Unmanaged
|
||||
.unplugged = Cable unplugged
|
||||
|
||||
remove-connection-dialog = Remove Connection Profile?
|
||||
.vpn-description = You'll need to enter a password again to use this network in the future.
|
||||
.wired-description = You'll need to recreate this profile to use it in the future.
|
||||
|
||||
vpn = VPN
|
||||
.connections = VPN Connections
|
||||
.remove = Remove connection profile
|
||||
.select-file = Select a VPN configuration file
|
||||
|
||||
wired = Wired
|
||||
.desc = Wired connections and connection profiles
|
||||
.connections = Wired Connections
|
||||
.devices = Wired Devices
|
||||
.remove = Remove connection profile
|
||||
|
||||
wifi = Wi-Fi
|
||||
.forget = Forget this network
|
||||
|
||||
## Networking: Online Accounts
|
||||
|
||||
|
|
|
|||
9
justfile
9
justfile
|
|
@ -44,8 +44,11 @@ entry-system := appid + '.System.desktop'
|
|||
entry-time := appid + '.Time.desktop'
|
||||
entry-touchpad := appid + '.Touchpad.desktop'
|
||||
entry-users := appid + '.Users.desktop'
|
||||
entry-vpn := appid + '.Vpn.desktop'
|
||||
entry-wallpaper := appid + '.Wallpaper.desktop'
|
||||
entry-window-management := appid + '.WindowManagement.desktop'
|
||||
entry-wired := appid + '.Wired.desktop'
|
||||
entry-wireless := appid + '.Wireless.desktop'
|
||||
entry-workspaces := appid + '.Workspaces.desktop'
|
||||
|
||||
# Build recipes
|
||||
|
|
@ -74,8 +77,11 @@ install-desktop-entries:
|
|||
install -Dm0644 'resources/{{entry-time}}' '{{appdir}}/{{entry-time}}'
|
||||
install -Dm0644 'resources/{{entry-touchpad}}' '{{appdir}}/{{entry-touchpad}}'
|
||||
install -Dm0644 'resources/{{entry-users}}' '{{appdir}}/{{entry-users}}'
|
||||
install -Dm0644 'resources/{{entry-vpn}}' '{{appdir}}/{{entry-vpn}}'
|
||||
install -Dm0644 'resources/{{entry-wallpaper}}' '{{appdir}}/{{entry-wallpaper}}'
|
||||
install -Dm0644 'resources/{{entry-window-management}}' '{{appdir}}/{{entry-window-management}}'
|
||||
install -Dm0644 'resources/{{entry-wired}}' '{{appdir}}/{{entry-wired}}'
|
||||
install -Dm0644 'resources/{{entry-wireless}}' '{{appdir}}/{{entry-wireless}}'
|
||||
install -Dm0644 'resources/{{entry-workspaces}}' '{{appdir}}/{{entry-workspaces}}'
|
||||
|
||||
# Install everything
|
||||
|
|
@ -116,8 +122,11 @@ uninstall:
|
|||
'{{appdir}}/{{entry-time}}' \
|
||||
'{{appdir}}/{{entry-touchpad}}' \
|
||||
'{{appdir}}/{{entry-users}}' \
|
||||
'{{appdir}}/{{entry-vpn}}' \
|
||||
'{{appdir}}/{{entry-wallpaper}}' \
|
||||
'{{appdir}}/{{entry-window-management}}' \
|
||||
'{{appdir}}/{{entry-wired}}' \
|
||||
'{{appdir}}/{{entry-wireless}}' \
|
||||
'{{appdir}}/{{entry-workspaces}}'
|
||||
find 'resources'/'default_schema' -type f -exec echo {} \; | rev | cut -d'/' -f-3 | rev | xargs -d '\n' -I {} rm -rf {{default-schema-target}}/{}
|
||||
find 'resources'/'icons' -type f -exec echo {} \; | rev | cut -d'/' -f-3 | rev | xargs -d '\n' -I {} rm {{iconsdir}}/{}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ pub trait Page<Message: 'static>: Downcast {
|
|||
fn on_leave(&mut self) -> Command<Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// The title to display in the page header.
|
||||
fn title(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl_downcast!(Page<Message>);
|
||||
|
|
|
|||
12
resources/com.system76.CosmicSettings.Vpn.desktop
Normal file
12
resources/com.system76.CosmicSettings.Vpn.desktop
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Desktop Entry]
|
||||
Name=VPN
|
||||
Comment=VPN connections and connection profiles.
|
||||
Type=Settings
|
||||
Exec=cosmic-settings vpn
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
Keywords=COSMIC
|
||||
NoDisplay=true
|
||||
OnlyShowIn=COSMIC
|
||||
Icon=preferences-vpn
|
||||
StartupNotify=true
|
||||
12
resources/com.system76.CosmicSettings.Wired.desktop
Normal file
12
resources/com.system76.CosmicSettings.Wired.desktop
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Desktop Entry]
|
||||
Name=Wired
|
||||
Comment=Wired connections and connection profiles.
|
||||
Type=Settings
|
||||
Exec=cosmic-settings wired
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
Keywords=COSMIC
|
||||
NoDisplay=true
|
||||
OnlyShowIn=COSMIC
|
||||
Icon=preferences-wired
|
||||
StartupNotify=true
|
||||
12
resources/com.system76.CosmicSettings.Wireless.desktop
Normal file
12
resources/com.system76.CosmicSettings.Wireless.desktop
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[Desktop Entry]
|
||||
Name=Wi-Fi
|
||||
Comment=Wi-Fi connections and connection profiles.
|
||||
Type=Settings
|
||||
Exec=cosmic-settings wireless
|
||||
Terminal=false
|
||||
Categories=COSMIC
|
||||
Keywords=COSMIC
|
||||
NoDisplay=true
|
||||
OnlyShowIn=COSMIC
|
||||
Icon=preferences-wireless
|
||||
StartupNotify=true
|
||||
Loading…
Add table
Add a link
Reference in a new issue