feat(networking): add VPN, WiFi, and Wired network pages

This commit is contained in:
Michael Murphy 2024-09-13 21:45:49 +02:00 committed by GitHub
parent d035ba0cf7
commit fa22b556dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 2876 additions and 131 deletions

337
Cargo.lock generated
View file

@ -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",
]

View file

@ -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" }

View file

@ -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"

View file

@ -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),
);

View file

@ -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,
}

View file

@ -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,

View file

@ -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 {

View file

@ -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"))
}

View file

@ -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
}

View 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(&section.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 {
(&section.descriptions[connected_txt], None)
} else {
(
&section.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()),
&section.descriptions[disconnect_txt],
)
}))
.push(popup_button(
Message::Settings(uuid.clone()),
&section.descriptions[settings_txt],
))
.push(popup_button(
Message::RemoveProfileRequest(uuid.clone()),
&section.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,
)
})
}

View 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)
}

View 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(&section.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(&section.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(&section.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(&section.descriptions[known_networks_txt]),
widget::settings::view_section(&section.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 {
(&section.descriptions[connected_txt], None)
} else if page.connecting.contains(&network.ssid) {
(&section.descriptions[connecting_txt], None)
} else {
(
&section.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()),
&section.descriptions[disconnect_txt],
)
}))
.push(popup_button(
Message::Settings(network.ssid.clone()),
&section.descriptions[settings_txt],
))
.push_maybe(is_known.then(|| {
popup_button(
Message::ForgetRequest(network.ssid.clone()),
&section.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()),
}
})
}

View file

@ -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,
&section.descriptions[connect_txt],
&section.descriptions[connected_txt],
&section.descriptions[disconnect_txt],
&section.descriptions[remove_txt],
&section.descriptions[settings_txt],
&section.descriptions[wired_conns_txt],
device,
)),
None => view.push(page.device_list_view(
spacing,
nm_state,
&section.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()),
}
})
}

View file

@ -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
View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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}}/{}

View file

@ -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>);

View 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

View 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

View 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