Merge branch 'master' into tobsch/app-list-filtering
This commit is contained in:
commit
80836cb941
86 changed files with 1849 additions and 2234 deletions
855
Cargo.lock
generated
855
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -54,11 +54,11 @@ rust-embed = "8.9.0"
|
||||||
rust-embed-utils = "8.9.0"
|
rust-embed-utils = "8.9.0"
|
||||||
rustc-hash = "2.1"
|
rustc-hash = "2.1"
|
||||||
rustix = { version = "1.1", features = ["fs", "process"] }
|
rustix = { version = "1.1", features = ["fs", "process"] }
|
||||||
zbus = { version = "5.12.0", default-features = false, features = ["tokio"] }
|
zbus = { version = "5.13.1", default-features = false, features = ["tokio"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
tracing-log = "0.2.0"
|
tracing-log = "0.2.0"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
|
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
|
||||||
|
|
@ -88,3 +88,8 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0" }
|
||||||
[patch."https://github.com/pop-os/cosmic-protocols"]
|
[patch."https://github.com/pop-os/cosmic-protocols"]
|
||||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
||||||
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
||||||
|
|
||||||
|
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
|
||||||
|
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
|
||||||
|
# upower_dbus = { path = "../dbus-settings-bindings/upower" }
|
||||||
|
# nm-secret-agent-manager = { path = "../dbus-settings-bindings/nm-secret-agent-manager" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-app-list"
|
name = "cosmic-app-list"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -24,5 +24,5 @@ tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url = "2.5.7"
|
url = "2.5.8"
|
||||||
zbus.workspace = true
|
zbus.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-app-list-config"
|
name = "cosmic-app-list-config"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[nl]=App-Tray
|
||||||
Name[sk]=Panel aplikácií
|
Name[sk]=Panel aplikácií
|
||||||
Name[sv]=Programfält
|
Name[sv]=Programfält
|
||||||
Name[es]=Bandeja de aplicaciones
|
Name[es]=Bandeja de aplicaciones
|
||||||
|
Name[it]=Area applicazioni
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-app-list
|
Exec=cosmic-app-list
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
0
cosmic-app-list/i18n/kk/cosmic_app_list.ftl
Normal file
0
cosmic-app-list/i18n/kk/cosmic_app_list.ftl
Normal file
|
|
@ -1,8 +1,8 @@
|
||||||
cosmic-app-list = 应用托盘
|
cosmic-app-list = 应用托盘
|
||||||
pin = 固定
|
pin = 固定到应用托盘
|
||||||
quit = 退出
|
quit = 退出
|
||||||
quit-all = 全部退出
|
quit-all = 全部退出
|
||||||
new-window = 新窗口
|
new-window = 新建窗口
|
||||||
run = 运行
|
run = 运行
|
||||||
run-on = 在 {$gpu} 上运行
|
run-on = 在 { $gpu } 上运行
|
||||||
run-on-default = (Default)
|
run-on-default = (默认)
|
||||||
|
|
|
||||||
|
|
@ -203,12 +203,13 @@ impl DockItem {
|
||||||
|
|
||||||
let app_icon = AppletIconData::new(applet);
|
let app_icon = AppletIconData::new(applet);
|
||||||
|
|
||||||
let cosmic_icon = fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default())
|
let cosmic_icon = cosmic::widget::icon(
|
||||||
.as_cosmic_icon()
|
fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()).as_cosmic_icon(),
|
||||||
// sets the preferred icon size variant
|
)
|
||||||
.size(128)
|
// sets the preferred icon size variant
|
||||||
.width(app_icon.icon_size.into())
|
.size(128)
|
||||||
.height(app_icon.icon_size.into());
|
.width(app_icon.icon_size.into())
|
||||||
|
.height(app_icon.icon_size.into());
|
||||||
|
|
||||||
let indicator = {
|
let indicator = {
|
||||||
let container = if toplevel_count <= 1 {
|
let container = if toplevel_count <= 1 {
|
||||||
|
|
@ -2037,10 +2038,12 @@ impl cosmic::Application for CosmicAppList {
|
||||||
let theme = self.core.system_theme();
|
let theme = self.core.system_theme();
|
||||||
|
|
||||||
if let Some((_, item, _, _)) = self.dnd_source.as_ref().filter(|s| s.0 == id) {
|
if let Some((_, item, _, _)) = self.dnd_source.as_ref().filter(|s| s.0 == id) {
|
||||||
fde::IconSource::from_unknown(item.desktop_info.icon().unwrap_or_default())
|
cosmic::widget::icon(
|
||||||
.as_cosmic_icon()
|
fde::IconSource::from_unknown(item.desktop_info.icon().unwrap_or_default())
|
||||||
.size(self.core.applet.suggested_size(false).0)
|
.as_cosmic_icon(),
|
||||||
.into()
|
)
|
||||||
|
.size(self.core.applet.suggested_size(false).0)
|
||||||
|
.into()
|
||||||
} else if let Some(Popup {
|
} else if let Some(Popup {
|
||||||
dock_item: DockItem { id, .. },
|
dock_item: DockItem { id, .. },
|
||||||
popup_type,
|
popup_type,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-a11y"
|
name = "cosmic-applet-a11y"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[de]= Zugänglichkeit
|
||||||
Name[sk]=Zjednodušenie ovládania
|
Name[sk]=Zjednodušenie ovládania
|
||||||
Name[sv]=Tillgänglighet
|
Name[sv]=Tillgänglighet
|
||||||
Name[es]=Accesibilidad
|
Name[es]=Accesibilidad
|
||||||
|
Name[it]=Accessibilità
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-a11y
|
Exec=cosmic-applet-a11y
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
screen-reader = Képernyőolvasó
|
screen-reader = Képernyőolvasó
|
||||||
magnifier = Nagyító
|
magnifier = Nagyító
|
||||||
invert-colors = Színek invertálása
|
invert-colors = Színek invertálása
|
||||||
settings = Akadálymentességi beállítások...
|
settings = Akadálymentességi beállítások…
|
||||||
filter-colors = Színek szűrése
|
filter-colors = Színek szűrése
|
||||||
high-contrast = Magas kontraszt
|
high-contrast = Magas kontraszt
|
||||||
|
|
|
||||||
0
cosmic-applet-a11y/i18n/kk/cosmic_applet_a11y.ftl
Normal file
0
cosmic-applet-a11y/i18n/kk/cosmic_applet_a11y.ftl
Normal file
|
|
@ -1,6 +1,6 @@
|
||||||
screen-reader = 屏幕阅读器
|
screen-reader = 屏幕阅读器
|
||||||
magnifier = 放大镜
|
magnifier = 放大镜
|
||||||
invert-colors = 反转颜色
|
invert-colors = 颜色反转
|
||||||
settings = 无障碍设置...
|
settings = 无障碍设置...
|
||||||
filter-colors = 滤镜颜色
|
filter-colors = 颜色滤镜
|
||||||
high-contrast = 高对比度
|
high-contrast = 高对比度模式
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ use cosmic::{
|
||||||
use cosmic_settings_a11y_manager_subscription::{
|
use cosmic_settings_a11y_manager_subscription::{
|
||||||
self as cosmic_a11y_manager, AccessibilityEvent, AccessibilityRequest, ColorFilter,
|
self as cosmic_a11y_manager, AccessibilityEvent, AccessibilityRequest, ColorFilter,
|
||||||
};
|
};
|
||||||
use cosmic_settings_accessibility_subscription::{self as accessibility, DBusRequest, DBusUpdate};
|
use cosmic_settings_accessibility_subscription::{self as accessibility};
|
||||||
use cosmic_time::{Instant, Timeline, anim, chain, id};
|
use cosmic_time::{Instant, Timeline, anim, chain, id};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
@ -51,7 +51,7 @@ struct CosmicA11yApplet {
|
||||||
magnifier_enabled: bool,
|
magnifier_enabled: bool,
|
||||||
inverted_colors_enabled: bool,
|
inverted_colors_enabled: bool,
|
||||||
popup: Option<window::Id>,
|
popup: Option<window::Id>,
|
||||||
dbus_sender: Option<UnboundedSender<DBusRequest>>,
|
dbus_sender: Option<UnboundedSender<accessibility::Request>>,
|
||||||
wayland_sender: Option<calloop::channel::Sender<AccessibilityRequest>>,
|
wayland_sender: Option<calloop::channel::Sender<AccessibilityRequest>>,
|
||||||
wayland_protocol_version: Option<u32>,
|
wayland_protocol_version: Option<u32>,
|
||||||
timeline: Timeline,
|
timeline: Timeline,
|
||||||
|
|
@ -71,7 +71,7 @@ enum Message {
|
||||||
Frame(Instant),
|
Frame(Instant),
|
||||||
Token(TokenUpdate),
|
Token(TokenUpdate),
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
DBusUpdate(DBusUpdate),
|
DBusUpdate(accessibility::Response),
|
||||||
WaylandUpdate(WaylandUpdate),
|
WaylandUpdate(WaylandUpdate),
|
||||||
Surface(surface::Action),
|
Surface(surface::Action),
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +109,7 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
if let Some(tx) = &self.dbus_sender {
|
if let Some(tx) = &self.dbus_sender {
|
||||||
self.timeline.set_chain(chain).start();
|
self.timeline.set_chain(chain).start();
|
||||||
self.reader_enabled = enabled;
|
self.reader_enabled = enabled;
|
||||||
let _ = tx.send(DBusRequest::Status(enabled));
|
let _ = tx.send(accessibility::Request::ScreenReader(enabled));
|
||||||
} else {
|
} else {
|
||||||
self.reader_enabled = false;
|
self.reader_enabled = false;
|
||||||
}
|
}
|
||||||
|
|
@ -256,18 +256,19 @@ impl cosmic::Application for CosmicA11yApplet {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::DBusUpdate(update) => match update {
|
Message::DBusUpdate(update) => match update {
|
||||||
DBusUpdate::Error(err) => {
|
accessibility::Response::Error(err) => {
|
||||||
tracing::error!("{err}");
|
tracing::error!("{err}");
|
||||||
let _ = self.dbus_sender.take();
|
let _ = self.dbus_sender.take();
|
||||||
self.reader_enabled = false;
|
self.reader_enabled = false;
|
||||||
}
|
}
|
||||||
DBusUpdate::Status(enabled) => {
|
accessibility::Response::ScreenReader(enabled) => {
|
||||||
self.reader_enabled = enabled;
|
self.reader_enabled = enabled;
|
||||||
}
|
}
|
||||||
DBusUpdate::Init(enabled, tx) => {
|
accessibility::Response::Init(enabled, tx) => {
|
||||||
self.reader_enabled = enabled;
|
self.reader_enabled = enabled;
|
||||||
self.dbus_sender = Some(tx);
|
self.dbus_sender = Some(tx);
|
||||||
}
|
}
|
||||||
|
_ => (),
|
||||||
},
|
},
|
||||||
Message::WaylandUpdate(update) => match update {
|
Message::WaylandUpdate(update) => match update {
|
||||||
WaylandUpdate::Errored => {
|
WaylandUpdate::Errored => {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-audio"
|
name = "cosmic-applet-audio"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ Name[de]=Klang
|
||||||
Name[sk]=Zvuk
|
Name[sk]=Zvuk
|
||||||
Name[sv]=Ljud
|
Name[sv]=Ljud
|
||||||
Name[es]=Sonido
|
Name[es]=Sonido
|
||||||
|
Name[it]=Audio
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-audio
|
Exec=cosmic-applet-audio
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
output = Kimenet
|
output = Kimenet
|
||||||
input = Bemenet
|
input = Bemenet
|
||||||
show-media-controls = Médiavezérlők megjelenítése a panelen
|
show-media-controls = Médiavezérlők megjelenítése a panelen
|
||||||
sound-settings = Hangbeállítások...
|
sound-settings = Hangbeállítások…
|
||||||
disconnected = PulseAudio nincs csatlakoztatva
|
disconnected = PulseAudio nincs csatlakozva
|
||||||
no-device = Nincs kiválasztott eszköz
|
no-device = Nincs kiválasztott eszköz
|
||||||
unknown-artist = Ismeretlen
|
unknown-artist = Ismeretlen
|
||||||
|
|
|
||||||
0
cosmic-applet-audio/i18n/kk/cosmic_applet_audio.ftl
Normal file
0
cosmic-applet-audio/i18n/kk/cosmic_applet_audio.ftl
Normal file
|
|
@ -1,6 +1,6 @@
|
||||||
output = 输出
|
output = 输出
|
||||||
input = 输入
|
input = 输入
|
||||||
show-media-controls = 在顶栏显示媒体控制
|
show-media-controls = 在面板上显示媒体控制
|
||||||
sound-settings = 声音设置...
|
sound-settings = 声音设置...
|
||||||
disconnected = PulseAudio 服务器已断开连接
|
disconnected = PulseAudio 服务器已断开连接
|
||||||
no-device = 未选择设备
|
no-device = 未选择设备
|
||||||
|
|
|
||||||
|
|
@ -633,7 +633,7 @@ impl cosmic::Application for Audio {
|
||||||
fl!("output"),
|
fl!("output"),
|
||||||
match sink {
|
match sink {
|
||||||
Some(sink) => sink.to_owned(),
|
Some(sink) => sink.to_owned(),
|
||||||
None => String::from("No device selected"),
|
None => fl!("no-device"),
|
||||||
},
|
},
|
||||||
self.model.sinks(),
|
self.model.sinks(),
|
||||||
Message::OutputToggle,
|
Message::OutputToggle,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-battery"
|
name = "cosmic-applet-battery"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
battery = Akkumulátor
|
battery = Akkumulátor
|
||||||
battery-desc = Csökkentett energiafogyasztás és teljesítmény.
|
battery-desc = Csökkentett energiafogyasztás és teljesítmény
|
||||||
balanced = Kiegyensúlyozott
|
balanced = Kiegyensúlyozott
|
||||||
balanced-desc = Normál teljesítmény és akkumulátorhasználat.
|
balanced-desc = Normál teljesítmény és akkumulátorhasználat
|
||||||
performance = Nagy teljesítmény
|
performance = Nagy teljesítmény
|
||||||
performance-desc = Nagy teljesítmény és energiafogyasztás.
|
performance-desc = Nagy teljesítmény és energiafogyasztás
|
||||||
max-charge = Az akkumulátor élettartamának növelése érdekében állítsa a maximális töltési szintet 80%-ra
|
max-charge = Az akkumulátor élettartamának növelése érdekében állítsa a maximális töltési szintet 80%-ra
|
||||||
seconds = másodperc
|
seconds = másodperc
|
||||||
minutes = perc
|
minutes = perc
|
||||||
hours = óra
|
hours = óra
|
||||||
until-empty = a lemerülésig
|
until-empty = a lemerülésig
|
||||||
power-settings = Energia- és akkumulátorbeállítások...
|
power-settings = Energia- és akkumulátorbeállítások…
|
||||||
dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát
|
dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát
|
||||||
dgpu-applications = A { $gpu_name } dedikált GPU-t használó alkalmazások
|
dgpu-applications = A { $gpu_name } dedikált GPU-t használó alkalmazások
|
||||||
|
|
|
||||||
0
cosmic-applet-battery/i18n/kk/cosmic_applet_battery.ftl
Normal file
0
cosmic-applet-battery/i18n/kk/cosmic_applet_battery.ftl
Normal file
|
|
@ -5,9 +5,9 @@ balanced-desc = 标准性能和电池使用。
|
||||||
performance = 高性能
|
performance = 高性能
|
||||||
performance-desc = 高性能和功耗。
|
performance-desc = 高性能和功耗。
|
||||||
max-charge = 最多充电到 80% 以增加电池寿命
|
max-charge = 最多充电到 80% 以增加电池寿命
|
||||||
seconds = s
|
seconds = 秒
|
||||||
minutes = m
|
minutes = 分钟
|
||||||
hours = h
|
hours = 小时
|
||||||
until-empty = 直到电池耗尽
|
until-empty = 直到电池耗尽
|
||||||
power-settings = 电源和电池设置...
|
power-settings = 电源和电池设置...
|
||||||
dgpu-running = 独立显卡正在运行,可能会降低电池寿命
|
dgpu-running = 独立显卡正在运行,可能会降低电池寿命
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-bluetooth"
|
name = "cosmic-applet-bluetooth"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
bluetooth = Bluetooth
|
bluetooth = Bluetooth
|
||||||
other-devices = Egyéb Bluetooth-eszközök
|
other-devices = Egyéb Bluetooth-eszközök
|
||||||
settings = Bluetooth beállítások...
|
settings = Bluetooth beállítások…
|
||||||
connected = Csatlakoztatva
|
connected = Csatlakozva
|
||||||
confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal
|
confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal
|
||||||
confirm = Megerősítés
|
confirm = Megerősítés
|
||||||
cancel = Mégse
|
cancel = Mégse
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-input-sources"
|
name = "cosmic-applet-input-sources"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[de]=Eigangsquellen
|
||||||
Name[sk]=Vstupné zdroje
|
Name[sk]=Vstupné zdroje
|
||||||
Name[es]=Fuentes de entrada de teclado
|
Name[es]=Fuentes de entrada de teclado
|
||||||
Name[sv]=Inmatningskällor
|
Name[sv]=Inmatningskällor
|
||||||
|
Name[it]=Sorgenti di immissione
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-input-sources
|
Exec=cosmic-applet-input-sources
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
show-keyboard-layout = Billentyűzetkiosztás megjelenítése...
|
show-keyboard-layout = Billentyűzetkiosztás megjelenítése…
|
||||||
keyboard-settings = Billentyűzet beállításai...
|
keyboard-settings = Billentyűzet beállításai…
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-minimize"
|
name = "cosmic-applet-minimize"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[de]=Minimierte Fenster
|
||||||
Name[sk]=Minimalizované okná
|
Name[sk]=Minimalizované okná
|
||||||
Name[es]=Ventanas minimizadas
|
Name[es]=Ventanas minimizadas
|
||||||
Name[sv]=Minimera fönster
|
Name[sv]=Minimera fönster
|
||||||
|
Name[it]=Finestre minimizzate
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-minimize
|
Exec=cosmic-applet-minimize
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ where
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Element::from(
|
Element::from(
|
||||||
icon.as_cosmic_icon()
|
cosmic::widget::icon(icon.as_cosmic_icon())
|
||||||
.width(Length::Fixed((size - border * 2.0).max(0.)))
|
.width(Length::Fixed((size - border * 2.0).max(0.)))
|
||||||
.height(Length::Fixed((size - border * 2.0).max(0.))),
|
.height(Length::Fixed((size - border * 2.0).max(0.))),
|
||||||
)
|
)
|
||||||
|
|
@ -73,8 +73,7 @@ where
|
||||||
.class(Button::AppletIcon)
|
.class(Button::AppletIcon)
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.into(),
|
.into(),
|
||||||
icon: icon
|
icon: cosmic::widget::icon(icon.as_cosmic_icon())
|
||||||
.as_cosmic_icon()
|
|
||||||
.width(Length::Fixed(size / 3.0))
|
.width(Length::Fixed(size / 3.0))
|
||||||
.height(Length::Fixed(size / 3.0))
|
.height(Length::Fixed(size / 3.0))
|
||||||
.into(),
|
.into(),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-network"
|
name = "cosmic-applet-network"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
async-fn-stream = "0.3"
|
||||||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||||
cosmic-time.workspace = true
|
cosmic-time.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|
@ -26,3 +27,15 @@ tracing-log.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
zbus.workspace = true
|
zbus.workspace = true
|
||||||
|
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings/" }
|
||||||
|
indexmap = "2.13.0"
|
||||||
|
secure-string = "0.3.0"
|
||||||
|
uuid = { version = "1.19.0", features = ["v4"] }
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies.cosmic-settings-network-manager-subscription]
|
||||||
|
git = "https://github.com/pop-os/cosmic-settings/"
|
||||||
|
|
||||||
|
[dependencies.cosmic-settings-airplane-mode-subscription]
|
||||||
|
git = "https://github.com/pop-os/cosmic-settings/"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[de]=Netzwerk
|
||||||
Name[sk]=Sieť
|
Name[sk]=Sieť
|
||||||
Name[sv]=Nätverk
|
Name[sv]=Nätverk
|
||||||
Name[es]=Red
|
Name[es]=Red
|
||||||
|
Name[it]=Rete
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-network
|
Exec=cosmic-applet-network
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ ipv4 = IPv4 cím
|
||||||
ipv6 = IPv6 cím
|
ipv6 = IPv6 cím
|
||||||
mac = MAC
|
mac = MAC
|
||||||
megabits-per-second = Mbps
|
megabits-per-second = Mbps
|
||||||
connected = Csatlakoztatva
|
connected = Csatlakozva
|
||||||
connecting = Csatlakozás...
|
connecting = Csatlakozás…
|
||||||
connect = Csatlakozás
|
connect = Csatlakozás
|
||||||
cancel = Mégse
|
cancel = Mégse
|
||||||
settings = Hálózati beállítások...
|
settings = Hálózati beállítások…
|
||||||
visible-wireless-networks = Látható vezeték nélküli hálózatok
|
visible-wireless-networks = Látható vezeték nélküli hálózatok
|
||||||
enter-password = Add meg a jelszót vagy a titkosítási kulcsot
|
enter-password = Add meg a jelszót vagy a titkosítási kulcsot
|
||||||
router-wps-button = A router „WPS” gombjának megnyomásával is csatlakozhatsz
|
router-wps-button = A router „WPS” gombjának megnyomásával is csatlakozhatsz
|
||||||
|
|
|
||||||
0
cosmic-applet-network/i18n/kk/cosmic_applet_network.ftl
Normal file
0
cosmic-applet-network/i18n/kk/cosmic_applet_network.ftl
Normal file
|
|
@ -1,7 +1,7 @@
|
||||||
network = 网络
|
network = 网络
|
||||||
airplane-mode = 飞行模式
|
airplane-mode = 飞行模式
|
||||||
airplane-mode-on = 飞行模式已开启
|
airplane-mode-on = 飞行模式已开启
|
||||||
turn-off-airplane-mode = 关闭飞行模式以启用 Wi-Fi、蓝牙和移动宽带。
|
turn-off-airplane-mode = 关闭飞行模式即可启用 Wi-Fi、蓝牙和移动宽带。
|
||||||
wifi = Wi-Fi
|
wifi = Wi-Fi
|
||||||
identity = 标识
|
identity = 标识
|
||||||
ipv4 = IPv4 地址
|
ipv4 = IPv4 地址
|
||||||
|
|
@ -15,8 +15,10 @@ cancel = 取消
|
||||||
settings = 网络设置...
|
settings = 网络设置...
|
||||||
visible-wireless-networks = 可见的无线网络
|
visible-wireless-networks = 可见的无线网络
|
||||||
enter-password = 输入密码或加密密钥
|
enter-password = 输入密码或加密密钥
|
||||||
router-wps-button = 您也可以通过按路由器上的“WPS”按钮连接
|
router-wps-button = 您也可以通过路由器上的“WPS”按钮连接
|
||||||
unable-to-connect = 无法连接到网络
|
unable-to-connect = 无法连接到网络
|
||||||
check-wifi-connection = 确保 Wi-Fi 已连接到互联网并且密码正确
|
check-wifi-connection = 确保 Wi-Fi 已连接互联网并且密码正确
|
||||||
reset = 重置
|
reset = 重置
|
||||||
vpn-connections = VPN连接
|
vpn-connections = VPN 连接
|
||||||
|
gigabits-per-second = Gbps
|
||||||
|
terabits-per-second = Tbps
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod localize;
|
mod localize;
|
||||||
mod network_manager;
|
mod utils;
|
||||||
|
|
||||||
use crate::localize::localize;
|
use crate::localize::localize;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
use super::{NetworkManagerEvent, NetworkManagerState};
|
|
||||||
use cosmic::{
|
|
||||||
iced::{self, Subscription},
|
|
||||||
iced_futures::stream,
|
|
||||||
};
|
|
||||||
use cosmic_dbus_networkmanager::nm::NetworkManager;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use std::{fmt::Debug, hash::Hash};
|
|
||||||
use zbus::Connection;
|
|
||||||
|
|
||||||
pub fn active_conns_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
|
||||||
id: I,
|
|
||||||
conn: Connection,
|
|
||||||
) -> iced::Subscription<NetworkManagerEvent> {
|
|
||||||
let initial = State::Continue(conn);
|
|
||||||
Subscription::run_with_id(
|
|
||||||
id,
|
|
||||||
stream::channel(50, move |mut output| {
|
|
||||||
let mut state = initial;
|
|
||||||
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
state = start_listening(state, &mut output).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum State {
|
|
||||||
Continue(Connection),
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_listening(
|
|
||||||
state: State,
|
|
||||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
|
||||||
) -> State {
|
|
||||||
let conn = match state {
|
|
||||||
State::Continue(conn) => conn,
|
|
||||||
State::Error => iced::futures::future::pending().await,
|
|
||||||
};
|
|
||||||
let network_manager = match NetworkManager::new(&conn).await {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(why) => {
|
|
||||||
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
|
|
||||||
return State::Error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut active_conns_changed = network_manager.receive_active_connections_changed().await;
|
|
||||||
active_conns_changed.next().await;
|
|
||||||
|
|
||||||
while let (Some(_change), ()) = tokio::join!(
|
|
||||||
active_conns_changed.next(),
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(1))
|
|
||||||
) {
|
|
||||||
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::ActiveConns(new_state))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
State::Continue(conn)
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings};
|
|
||||||
use zbus::Connection;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct VpnConnection {
|
|
||||||
pub name: String,
|
|
||||||
pub uuid: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load all available VPN connections from NetworkManager settings
|
|
||||||
pub async fn load_vpn_connections(conn: &Connection) -> anyhow::Result<Vec<VpnConnection>> {
|
|
||||||
let nm_settings = NetworkManagerSettings::new(conn).await?;
|
|
||||||
let connections = nm_settings.list_connections().await?;
|
|
||||||
|
|
||||||
let mut vpn_connections = Vec::new();
|
|
||||||
|
|
||||||
for connection in connections {
|
|
||||||
let settings_map = match connection.get_settings().await {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let settings = Settings::new(settings_map);
|
|
||||||
|
|
||||||
// Check if this is a VPN connection
|
|
||||||
if let Some(connection_settings) = &settings.connection {
|
|
||||||
if let Some(conn_type) = &connection_settings.type_ {
|
|
||||||
// VPN connections have type "vpn" or "wireguard"
|
|
||||||
if conn_type == "vpn" || conn_type == "wireguard" {
|
|
||||||
let name = connection_settings
|
|
||||||
.id
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "Unknown VPN".to_string());
|
|
||||||
let uuid = connection_settings.uuid.clone().unwrap_or_default();
|
|
||||||
|
|
||||||
vpn_connections.push(VpnConnection { name, uuid });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by name for consistent UI
|
|
||||||
vpn_connections.sort_by(|a, b| a.name.cmp(&b.name));
|
|
||||||
|
|
||||||
Ok(vpn_connections)
|
|
||||||
}
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
use cosmic_dbus_networkmanager::{
|
|
||||||
device::wireless::WirelessDevice,
|
|
||||||
interface::{
|
|
||||||
access_point::AccessPointProxy,
|
|
||||||
enums::{ApFlags, ApSecurityFlags, DeviceState},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use futures_util::StreamExt;
|
|
||||||
use rustc_hash::FxHashMap;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use zbus::zvariant::ObjectPath;
|
|
||||||
|
|
||||||
use super::hw_address::HwAddress;
|
|
||||||
|
|
||||||
pub async fn handle_wireless_device(
|
|
||||||
device: WirelessDevice<'_>,
|
|
||||||
hw_address: Option<String>,
|
|
||||||
) -> zbus::Result<Vec<AccessPoint>> {
|
|
||||||
device.request_scan(HashMap::new()).await?;
|
|
||||||
let mut scan_changed = device.receive_last_scan_changed().await;
|
|
||||||
if let Some(t) = scan_changed.next().await {
|
|
||||||
if let Ok(-1) = t.get().await {
|
|
||||||
eprintln!("scan errored");
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let access_points = device.get_access_points().await?;
|
|
||||||
let state: DeviceState = device
|
|
||||||
.upcast()
|
|
||||||
.await
|
|
||||||
.and_then(|dev| dev.cached_state())
|
|
||||||
.unwrap_or_default()
|
|
||||||
.map_or(DeviceState::Unknown, |s| s.into());
|
|
||||||
// Sort by strength and remove duplicates
|
|
||||||
let mut aps = FxHashMap::<String, AccessPoint>::default();
|
|
||||||
for ap in access_points {
|
|
||||||
let ssid = String::from_utf8_lossy(ap.ssid().await?.as_slice()).into_owned();
|
|
||||||
let wps_push = ap.flags().await?.contains(ApFlags::WPS_PBC);
|
|
||||||
let strength = ap.strength().await?;
|
|
||||||
if let Some(access_point) = aps.get(&ssid) {
|
|
||||||
if access_point.strength > strength {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let proxy: &AccessPointProxy = ≈
|
|
||||||
let Ok(flags) = ap.rsn_flags().await else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let network_type = if flags.intersects(ApSecurityFlags::KEY_MGMT_802_1X) {
|
|
||||||
NetworkType::EAP
|
|
||||||
} else if flags.intersects(ApSecurityFlags::KEY_MGMTPSK) {
|
|
||||||
NetworkType::PSK
|
|
||||||
} else if flags.is_empty() {
|
|
||||||
NetworkType::Open
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
aps.insert(
|
|
||||||
ssid.clone(),
|
|
||||||
AccessPoint {
|
|
||||||
ssid,
|
|
||||||
strength,
|
|
||||||
state,
|
|
||||||
working: false,
|
|
||||||
path: ap.inner().path().to_owned(),
|
|
||||||
hw_address: hw_address
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|str_addr| HwAddress::from_str(str_addr))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
wps_push,
|
|
||||||
network_type,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let mut aps = aps.into_values().collect::<Vec<_>>();
|
|
||||||
aps.sort_unstable_by_key(|ap| ap.strength);
|
|
||||||
Ok(aps)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AccessPoint {
|
|
||||||
pub ssid: String,
|
|
||||||
pub strength: u8,
|
|
||||||
pub state: DeviceState,
|
|
||||||
pub working: bool,
|
|
||||||
pub path: ObjectPath<'static>,
|
|
||||||
pub hw_address: HwAddress,
|
|
||||||
pub wps_push: bool,
|
|
||||||
pub network_type: NetworkType,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO do we want to support eap methods other than peap in the applet?
|
|
||||||
// Then we'd need a dropdown for the eap method,
|
|
||||||
// and tls requires a cert instead of a password
|
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum NetworkType {
|
|
||||||
Open,
|
|
||||||
PSK,
|
|
||||||
EAP,
|
|
||||||
}
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
use cosmic_dbus_networkmanager::{
|
|
||||||
active_connection::ActiveConnection, device::SpecificDevice,
|
|
||||||
interface::enums::ActiveConnectionState,
|
|
||||||
};
|
|
||||||
use std::net::Ipv4Addr;
|
|
||||||
|
|
||||||
use super::hw_address::HwAddress;
|
|
||||||
|
|
||||||
/// Read network interface speed from sysfs
|
|
||||||
/// Returns speed in Mbps, or None if unable to read
|
|
||||||
fn read_speed_from_sysfs(interface: &str) -> Option<u32> {
|
|
||||||
let path = format!("/sys/class/net/{}/speed", interface);
|
|
||||||
std::fs::read_to_string(path)
|
|
||||||
.ok()
|
|
||||||
.and_then(|content| content.trim().parse::<i32>().ok())
|
|
||||||
.and_then(|speed| if speed > 0 { Some(speed as u32) } else { None })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn active_connections(
|
|
||||||
active_connections: Vec<ActiveConnection<'_>>,
|
|
||||||
) -> zbus::Result<Vec<ActiveConnectionInfo>> {
|
|
||||||
let mut info = Vec::<ActiveConnectionInfo>::with_capacity(active_connections.len());
|
|
||||||
for connection in active_connections {
|
|
||||||
let ipv4 = connection
|
|
||||||
.ip4_config()
|
|
||||||
.await?
|
|
||||||
.address_data()
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
let addresses: Vec<_> = ipv4.iter().map(|d| d.address).collect();
|
|
||||||
let state = connection
|
|
||||||
.state()
|
|
||||||
.await
|
|
||||||
.unwrap_or(ActiveConnectionState::Unknown);
|
|
||||||
|
|
||||||
if connection.vpn().await.unwrap_or_default() {
|
|
||||||
info.push(ActiveConnectionInfo::Vpn {
|
|
||||||
name: connection.id().await?,
|
|
||||||
ip_addresses: addresses.clone(),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for device in connection.devices().await.unwrap_or_default() {
|
|
||||||
let interface_name = device.interface().await.ok();
|
|
||||||
|
|
||||||
match device
|
|
||||||
.downcast_to_device()
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|inner| inner)
|
|
||||||
{
|
|
||||||
Some(SpecificDevice::Wired(wired_device)) => {
|
|
||||||
let mut speed = wired_device.speed().await?;
|
|
||||||
|
|
||||||
// If NetworkManager returns 0, try to read from sysfs
|
|
||||||
if speed == 0 {
|
|
||||||
if let Some(interface) = interface_name.as_ref() {
|
|
||||||
speed = read_speed_from_sysfs(interface).unwrap_or(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info.push(ActiveConnectionInfo::Wired {
|
|
||||||
name: connection.id().await?,
|
|
||||||
hw_address: HwAddress::from_str(&wired_device.hw_address().await?)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
speed,
|
|
||||||
ip_addresses: addresses.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some(SpecificDevice::Wireless(wireless_device)) => {
|
|
||||||
if let Ok(access_point) = wireless_device.active_access_point().await {
|
|
||||||
info.push(ActiveConnectionInfo::WiFi {
|
|
||||||
name: String::from_utf8_lossy(&access_point.ssid().await?).into_owned(),
|
|
||||||
ip_addresses: addresses.clone(),
|
|
||||||
hw_address: HwAddress::from_str(&wireless_device.hw_address().await?)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
state,
|
|
||||||
strength: access_point.strength().await.unwrap_or_default(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(SpecificDevice::WireGuard(_)) => {
|
|
||||||
info.push(ActiveConnectionInfo::Vpn {
|
|
||||||
name: connection.id().await?,
|
|
||||||
ip_addresses: addresses.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info.sort_unstable();
|
|
||||||
Ok(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ActiveConnectionInfo {
|
|
||||||
Wired {
|
|
||||||
name: String,
|
|
||||||
hw_address: HwAddress,
|
|
||||||
speed: u32,
|
|
||||||
ip_addresses: Vec<Ipv4Addr>,
|
|
||||||
},
|
|
||||||
WiFi {
|
|
||||||
name: String,
|
|
||||||
ip_addresses: Vec<Ipv4Addr>,
|
|
||||||
hw_address: HwAddress,
|
|
||||||
state: ActiveConnectionState,
|
|
||||||
strength: u8,
|
|
||||||
},
|
|
||||||
Vpn {
|
|
||||||
name: String,
|
|
||||||
ip_addresses: Vec<Ipv4Addr>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveConnectionInfo {
|
|
||||||
pub fn name(&self) -> String {
|
|
||||||
match &self {
|
|
||||||
Self::Wired { name, .. } | Self::WiFi { name, .. } | Self::Vpn { name, .. } => {
|
|
||||||
name.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn hw_address(&self) -> HwAddress {
|
|
||||||
match &self {
|
|
||||||
Self::Wired { hw_address, .. } | Self::WiFi { hw_address, .. } => *hw_address,
|
|
||||||
Self::Vpn { .. } => HwAddress::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::cmp::Ord for ActiveConnectionInfo {
|
|
||||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
||||||
match (self, other) {
|
|
||||||
(Self::Vpn { .. }, Self::Wired { .. } | Self::WiFi { .. })
|
|
||||||
| (Self::Wired { .. }, Self::WiFi { .. }) => std::cmp::Ordering::Less,
|
|
||||||
|
|
||||||
(Self::WiFi { .. }, Self::Wired { .. } | Self::Vpn { .. })
|
|
||||||
| (Self::Wired { .. }, Self::Vpn { .. }) => std::cmp::Ordering::Greater,
|
|
||||||
|
|
||||||
(Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. })
|
|
||||||
| (Self::Wired { name: n1, .. }, Self::Wired { name: n2, .. })
|
|
||||||
| (Self::WiFi { name: n1, .. }, Self::WiFi { name: n2, .. }) => n1.cmp(n2),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::cmp::Eq for ActiveConnectionInfo {}
|
|
||||||
|
|
||||||
impl std::cmp::PartialOrd for ActiveConnectionInfo {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::cmp::PartialEq for ActiveConnectionInfo {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(
|
|
||||||
Self::Wired {
|
|
||||||
name: n1,
|
|
||||||
hw_address: a1,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
Self::Wired {
|
|
||||||
name: n2,
|
|
||||||
hw_address: a2,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
)
|
|
||||||
| (
|
|
||||||
Self::WiFi {
|
|
||||||
name: n1,
|
|
||||||
hw_address: a1,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
Self::WiFi {
|
|
||||||
name: n2,
|
|
||||||
hw_address: a2,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) => n1 == n2 && a1 == a2,
|
|
||||||
|
|
||||||
(Self::Vpn { name: n1, .. }, Self::Vpn { name: n2, .. }) => n1 == n2,
|
|
||||||
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
use super::{NetworkManagerEvent, NetworkManagerState};
|
|
||||||
use cosmic::iced::{self, Subscription, stream};
|
|
||||||
use cosmic_dbus_networkmanager::nm::NetworkManager;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use std::{fmt::Debug, hash::Hash};
|
|
||||||
use zbus::Connection;
|
|
||||||
|
|
||||||
pub fn devices_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
|
||||||
id: I,
|
|
||||||
has_popup: bool,
|
|
||||||
conn: Connection,
|
|
||||||
) -> iced::Subscription<NetworkManagerEvent> {
|
|
||||||
let initial = State::Continue(conn);
|
|
||||||
Subscription::run_with_id(
|
|
||||||
(id, has_popup),
|
|
||||||
stream::channel(50, move |mut output| {
|
|
||||||
let mut state = initial.clone();
|
|
||||||
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
state = start_listening(state, has_popup, &mut output).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum State {
|
|
||||||
Continue(Connection),
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_listening(
|
|
||||||
state: State,
|
|
||||||
has_popup: bool,
|
|
||||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
|
||||||
) -> State {
|
|
||||||
let conn = match state {
|
|
||||||
State::Continue(conn) => conn,
|
|
||||||
State::Error => iced::futures::future::pending().await,
|
|
||||||
};
|
|
||||||
let network_manager = match NetworkManager::new(&conn).await {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(why) => {
|
|
||||||
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
|
|
||||||
return State::Error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut devices_changed = network_manager.receive_devices_changed().await;
|
|
||||||
|
|
||||||
let secs = if has_popup { 4 } else { 60 };
|
|
||||||
while let (Some(_change), ()) = tokio::join!(
|
|
||||||
devices_changed.next(),
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(secs))
|
|
||||||
) {
|
|
||||||
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::WirelessAccessPoints(new_state))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
State::Continue(conn)
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)]
|
|
||||||
pub struct HwAddress {
|
|
||||||
address: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HwAddress {
|
|
||||||
pub fn from_str(arg: &str) -> Option<Self> {
|
|
||||||
let columnless_vec = arg.split(':').collect::<Box<[_]>>();
|
|
||||||
if columnless_vec.len() * 3 - 1 != arg.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
for byte in &columnless_vec {
|
|
||||||
if byte.len() != 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u64::from_str_radix(columnless_vec.join("").as_str(), 16)
|
|
||||||
.ok()
|
|
||||||
.map(|address| HwAddress { address })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for HwAddress {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
for (index, c) in format!("{:x}", self.address).char_indices() {
|
|
||||||
if index != 0 && index % 2 == 0 {
|
|
||||||
f.write_char(':')?;
|
|
||||||
}
|
|
||||||
f.write_char(c)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,822 +0,0 @@
|
||||||
pub mod active_conns;
|
|
||||||
pub mod available_vpns;
|
|
||||||
pub mod available_wifi;
|
|
||||||
pub mod current_networks;
|
|
||||||
pub mod devices;
|
|
||||||
pub mod hw_address;
|
|
||||||
pub mod wireless_enabled;
|
|
||||||
|
|
||||||
use std::{collections::HashMap, fmt::Debug, time::Duration};
|
|
||||||
|
|
||||||
use available_wifi::NetworkType;
|
|
||||||
use cosmic::{
|
|
||||||
iced::{self, Subscription},
|
|
||||||
iced_futures::stream,
|
|
||||||
};
|
|
||||||
use cosmic_dbus_networkmanager::{
|
|
||||||
active_connection::ActiveConnection,
|
|
||||||
device::SpecificDevice,
|
|
||||||
interface::{
|
|
||||||
active_connection::ActiveConnectionProxy,
|
|
||||||
enums::{self, ActiveConnectionState, DeviceType, NmConnectivityState},
|
|
||||||
},
|
|
||||||
nm::NetworkManager,
|
|
||||||
settings::{NetworkManagerSettings, connection::Settings},
|
|
||||||
};
|
|
||||||
use futures::{
|
|
||||||
SinkExt, StreamExt,
|
|
||||||
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
|
|
||||||
};
|
|
||||||
use hw_address::HwAddress;
|
|
||||||
use tokio::process::Command;
|
|
||||||
use zbus::{
|
|
||||||
Connection,
|
|
||||||
zvariant::{self, Value},
|
|
||||||
};
|
|
||||||
|
|
||||||
use self::{
|
|
||||||
available_vpns::{VpnConnection, load_vpn_connections},
|
|
||||||
available_wifi::{AccessPoint, handle_wireless_device},
|
|
||||||
current_networks::{ActiveConnectionInfo, active_connections},
|
|
||||||
};
|
|
||||||
|
|
||||||
// In some distros, rfkill is only in sbin, which isn't normally in PATH
|
|
||||||
// TODO: Directly access `/dev/rfkill`
|
|
||||||
fn rfkill_path_var() -> std::ffi::OsString {
|
|
||||||
let mut path = std::env::var_os("PATH").unwrap_or_default();
|
|
||||||
path.push(":/usr/sbin");
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum State {
|
|
||||||
Ready,
|
|
||||||
Waiting(Connection, UnboundedReceiver<NetworkManagerRequest>),
|
|
||||||
Finished,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn network_manager_subscription<I: Copy + Debug + std::hash::Hash + 'static>(
|
|
||||||
id: I,
|
|
||||||
) -> iced::Subscription<NetworkManagerEvent> {
|
|
||||||
Subscription::run_with_id(
|
|
||||||
id,
|
|
||||||
stream::channel(50, |mut output| async move {
|
|
||||||
let mut state = State::Ready;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
state = start_listening(state, &mut output).await;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_listening(
|
|
||||||
state: State,
|
|
||||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
|
||||||
) -> State {
|
|
||||||
match state {
|
|
||||||
State::Ready => {
|
|
||||||
let Ok(conn) = Connection::system().await else {
|
|
||||||
return State::Finished;
|
|
||||||
};
|
|
||||||
|
|
||||||
let (tx, rx) = unbounded();
|
|
||||||
let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
if output
|
|
||||||
.send(NetworkManagerEvent::Init {
|
|
||||||
conn: conn.clone(),
|
|
||||||
sender: tx,
|
|
||||||
state: nm_state,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
State::Waiting(conn, rx)
|
|
||||||
} else {
|
|
||||||
State::Finished
|
|
||||||
}
|
|
||||||
}
|
|
||||||
State::Waiting(conn, mut rx) => {
|
|
||||||
let Ok(network_manager) = NetworkManager::new(&conn).await else {
|
|
||||||
return State::Finished;
|
|
||||||
};
|
|
||||||
|
|
||||||
match rx.next().await {
|
|
||||||
Some(NetworkManagerRequest::Disconnect(ssid, hw_address)) => {
|
|
||||||
let mut success = false;
|
|
||||||
for c in network_manager
|
|
||||||
.active_connections()
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
if c.id().await.unwrap_or_default() != ssid {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut is_there_device = false;
|
|
||||||
for device in c.devices().await.unwrap_or_default() {
|
|
||||||
if HwAddress::from_str(device.hw_address().await.as_ref().unwrap())
|
|
||||||
== Some(hw_address)
|
|
||||||
{
|
|
||||||
is_there_device = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_there_device
|
|
||||||
&& network_manager.deactivate_connection(&c).await.is_ok()
|
|
||||||
{
|
|
||||||
success = true;
|
|
||||||
if let Ok(ActiveConnectionState::Deactivated) = c.state().await {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
let mut changed = c.receive_state_changed().await;
|
|
||||||
_ = tokio::time::timeout(Duration::from_secs(5), async move {
|
|
||||||
loop {
|
|
||||||
if let Some(next) = changed.next().await {
|
|
||||||
if let Ok(ActiveConnectionState::Deactivated) =
|
|
||||||
next.get().await.map(ActiveConnectionState::from)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::Disconnect(ssid.clone(), hw_address),
|
|
||||||
success,
|
|
||||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::SetAirplaneMode(airplane_mode)) => {
|
|
||||||
// wifi
|
|
||||||
let mut success = network_manager
|
|
||||||
.set_wireless_enabled(!airplane_mode)
|
|
||||||
.await
|
|
||||||
.is_ok();
|
|
||||||
// bluetooth
|
|
||||||
success = success
|
|
||||||
&& Command::new("rfkill")
|
|
||||||
.env("PATH", rfkill_path_var())
|
|
||||||
.arg(if airplane_mode { "block" } else { "unblock" })
|
|
||||||
.arg("bluetooth")
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.is_ok();
|
|
||||||
let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
state.airplane_mode = if success {
|
|
||||||
airplane_mode
|
|
||||||
} else {
|
|
||||||
!airplane_mode
|
|
||||||
};
|
|
||||||
if state.airplane_mode {
|
|
||||||
state.wifi_enabled = false;
|
|
||||||
}
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::SetAirplaneMode(airplane_mode),
|
|
||||||
success,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::SetWiFi(enabled)) => {
|
|
||||||
let success = network_manager.set_wireless_enabled(enabled).await.is_ok();
|
|
||||||
let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
state.wifi_enabled = if success { enabled } else { !enabled };
|
|
||||||
let response = NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::SetWiFi(enabled),
|
|
||||||
success,
|
|
||||||
state,
|
|
||||||
};
|
|
||||||
_ = output.send(response).await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::Authenticate {
|
|
||||||
ssid,
|
|
||||||
identity,
|
|
||||||
password,
|
|
||||||
hw_address,
|
|
||||||
}) => {
|
|
||||||
let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
let mut success = true;
|
|
||||||
let err = nm_state
|
|
||||||
.connect_wifi(
|
|
||||||
&conn,
|
|
||||||
&ssid,
|
|
||||||
identity.as_deref(),
|
|
||||||
Some(&password),
|
|
||||||
hw_address,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Err(err) = err {
|
|
||||||
success = false;
|
|
||||||
tracing::error!("{:?}", &err);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::Authenticate {
|
|
||||||
ssid: ssid.clone(),
|
|
||||||
identity: identity.clone(),
|
|
||||||
password: password.clone(),
|
|
||||||
hw_address,
|
|
||||||
},
|
|
||||||
success,
|
|
||||||
state: NetworkManagerState::new(&conn).await.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type)) => {
|
|
||||||
if matches!(network_type, NetworkType::Open) {
|
|
||||||
attempt_wifi_connection(&conn, ssid, hw_address, network_type, output)
|
|
||||||
.await;
|
|
||||||
} else {
|
|
||||||
// For secured networks, check if we have saved credentials
|
|
||||||
if !has_saved_wifi_credentials(&conn, &ssid).await {
|
|
||||||
return State::Waiting(conn, rx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have saved credentials, attempt connection
|
|
||||||
attempt_wifi_connection(&conn, ssid, hw_address, network_type, output)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::Reload) => {
|
|
||||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::Reload,
|
|
||||||
success: true,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::Forget(ssid, hw_address)) => {
|
|
||||||
let s = NetworkManagerSettings::new(&conn).await.unwrap();
|
|
||||||
let known_conns = s.list_connections().await.unwrap_or_default();
|
|
||||||
let mut success = false;
|
|
||||||
for c in known_conns {
|
|
||||||
let settings = c.get_settings().await.ok().unwrap_or_default();
|
|
||||||
let s = Settings::new(settings);
|
|
||||||
if s.wifi
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|w| w.ssid.as_deref())
|
|
||||||
.is_some_and(|s| std::str::from_utf8(s).is_ok_and(|s| s == ssid))
|
|
||||||
{
|
|
||||||
// todo most likely we can here forget ssid from wrong hw_address
|
|
||||||
_ = c.delete().await;
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::Forget(ssid.clone(), hw_address),
|
|
||||||
success,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::ActivateVpn(uuid)) => {
|
|
||||||
tracing::info!("Activating VPN with UUID: {}", uuid);
|
|
||||||
let network_manager = match NetworkManager::new(&conn).await {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::ActivateVpn(uuid),
|
|
||||||
success: false,
|
|
||||||
state: NetworkManagerState::new(&conn)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
return State::Waiting(conn, rx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut success = false;
|
|
||||||
|
|
||||||
// Find the connection by UUID
|
|
||||||
if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await {
|
|
||||||
if let Ok(connections) = nm_settings.list_connections().await {
|
|
||||||
for connection in connections {
|
|
||||||
if let Ok(settings) = connection.get_settings().await {
|
|
||||||
let settings = Settings::new(settings);
|
|
||||||
if let Some(conn_settings) = &settings.connection {
|
|
||||||
if conn_settings.uuid.as_ref() == Some(&uuid) {
|
|
||||||
// Activate the VPN connection without a specific device
|
|
||||||
// Call the D-Bus method directly since VPNs don't need a device
|
|
||||||
use zbus::zvariant::ObjectPath;
|
|
||||||
let empty_device = ObjectPath::try_from("/").unwrap();
|
|
||||||
|
|
||||||
match network_manager
|
|
||||||
.inner()
|
|
||||||
.call_method(
|
|
||||||
"ActivateConnection",
|
|
||||||
&(
|
|
||||||
connection.inner().path(),
|
|
||||||
empty_device.clone(),
|
|
||||||
empty_device,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::info!(
|
|
||||||
"Successfully activated VPN: {}",
|
|
||||||
uuid
|
|
||||||
);
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to activate VPN {}: {:?}",
|
|
||||||
uuid,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !success {
|
|
||||||
tracing::warn!(
|
|
||||||
"VPN connection with UUID {} not found or failed to activate",
|
|
||||||
uuid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::ActivateVpn(uuid),
|
|
||||||
success,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Some(NetworkManagerRequest::DeactivateVpn(name)) => {
|
|
||||||
tracing::info!("Deactivating VPN: {}", name);
|
|
||||||
let network_manager = match NetworkManager::new(&conn).await {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Failed to connect to NetworkManager: {:?}", e);
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::DeactivateVpn(name),
|
|
||||||
success: false,
|
|
||||||
state: NetworkManagerState::new(&conn)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
return State::Waiting(conn, rx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut success = false;
|
|
||||||
|
|
||||||
// Find and deactivate the active VPN connection by name
|
|
||||||
if let Ok(active_connections) = network_manager.active_connections().await {
|
|
||||||
for active_conn in active_connections {
|
|
||||||
if let Ok(conn_id) = active_conn.id().await {
|
|
||||||
if conn_id == name && active_conn.vpn().await.unwrap_or(false) {
|
|
||||||
match network_manager.deactivate_connection(&active_conn).await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
tracing::info!(
|
|
||||||
"Successfully deactivated VPN: {}",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
success = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to deactivate VPN {}: {:?}",
|
|
||||||
name,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !success {
|
|
||||||
tracing::warn!(
|
|
||||||
"Active VPN connection '{}' not found or failed to deactivate",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::DeactivateVpn(name),
|
|
||||||
success,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return State::Finished;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
State::Waiting(conn, rx)
|
|
||||||
}
|
|
||||||
State::Finished => iced::futures::future::pending().await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn has_saved_wifi_credentials(conn: &Connection, ssid: &str) -> bool {
|
|
||||||
let Ok(nm_settings) = NetworkManagerSettings::new(conn).await else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let known_conns = nm_settings.list_connections().await.unwrap_or_default();
|
|
||||||
|
|
||||||
for connection in known_conns {
|
|
||||||
if let Ok(settings) = connection.get_settings().await {
|
|
||||||
let settings = Settings::new(settings);
|
|
||||||
if let Some(saved_ssid) = settings
|
|
||||||
.wifi
|
|
||||||
.and_then(|w| w.ssid)
|
|
||||||
.and_then(|ssid| String::from_utf8(ssid).ok())
|
|
||||||
{
|
|
||||||
if saved_ssid == ssid {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn attempt_wifi_connection(
|
|
||||||
conn: &Connection,
|
|
||||||
ssid: String,
|
|
||||||
hw_address: HwAddress,
|
|
||||||
network_type: NetworkType,
|
|
||||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
|
||||||
) {
|
|
||||||
let state = NetworkManagerState::new(conn).await.unwrap_or_default();
|
|
||||||
|
|
||||||
let success = if let Err(err) = state
|
|
||||||
.connect_wifi(conn, ssid.as_ref(), None, None, hw_address)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!("Failed to connect to access point: {:?}", err);
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::RequestResponse {
|
|
||||||
req: NetworkManagerRequest::SelectAccessPoint(ssid, hw_address, network_type),
|
|
||||||
success,
|
|
||||||
state: NetworkManagerState::new(conn).await.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum NetworkManagerRequest {
|
|
||||||
SetAirplaneMode(bool),
|
|
||||||
SetWiFi(bool),
|
|
||||||
SelectAccessPoint(String, HwAddress, NetworkType),
|
|
||||||
Disconnect(String, HwAddress),
|
|
||||||
Authenticate {
|
|
||||||
ssid: String,
|
|
||||||
identity: Option<String>,
|
|
||||||
password: String,
|
|
||||||
hw_address: HwAddress,
|
|
||||||
},
|
|
||||||
Forget(String, HwAddress),
|
|
||||||
Reload,
|
|
||||||
ActivateVpn(String), // UUID of VPN connection to activate
|
|
||||||
DeactivateVpn(String), // Name of active VPN connection to deactivate
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum NetworkManagerEvent {
|
|
||||||
RequestResponse {
|
|
||||||
req: NetworkManagerRequest,
|
|
||||||
state: NetworkManagerState,
|
|
||||||
success: bool,
|
|
||||||
},
|
|
||||||
Init {
|
|
||||||
conn: Connection,
|
|
||||||
sender: UnboundedSender<NetworkManagerRequest>,
|
|
||||||
state: NetworkManagerState,
|
|
||||||
},
|
|
||||||
WiFiEnabled(NetworkManagerState),
|
|
||||||
WirelessAccessPoints(NetworkManagerState),
|
|
||||||
ActiveConns(NetworkManagerState),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct NetworkManagerState {
|
|
||||||
pub wireless_access_points: Vec<AccessPoint>,
|
|
||||||
pub active_conns: Vec<ActiveConnectionInfo>,
|
|
||||||
pub known_access_points: Vec<AccessPoint>,
|
|
||||||
pub available_vpns: Vec<VpnConnection>,
|
|
||||||
pub wifi_enabled: bool,
|
|
||||||
pub airplane_mode: bool,
|
|
||||||
pub connectivity: NmConnectivityState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NetworkManagerState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
wireless_access_points: Vec::new(),
|
|
||||||
active_conns: Vec::new(),
|
|
||||||
known_access_points: Vec::new(),
|
|
||||||
available_vpns: Vec::new(),
|
|
||||||
wifi_enabled: false,
|
|
||||||
airplane_mode: false,
|
|
||||||
connectivity: NmConnectivityState::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetworkManagerState {
|
|
||||||
pub async fn new(conn: &Connection) -> anyhow::Result<Self> {
|
|
||||||
let network_manager = NetworkManager::new(conn).await?;
|
|
||||||
let mut self_ = Self::default();
|
|
||||||
// airplane mode
|
|
||||||
let airplaine_mode = Command::new("rfkill")
|
|
||||||
.env("PATH", rfkill_path_var())
|
|
||||||
.arg("list")
|
|
||||||
.arg("bluetooth")
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
let airplane_mode = std::str::from_utf8(&airplaine_mode.stdout).unwrap_or_default();
|
|
||||||
self_.wifi_enabled = network_manager.wireless_enabled().await.unwrap_or_default();
|
|
||||||
self_.airplane_mode = airplane_mode.contains("Soft blocked: yes") && !self_.wifi_enabled;
|
|
||||||
|
|
||||||
let s = NetworkManagerSettings::new(conn).await?;
|
|
||||||
_ = s.load_connections(&[]).await;
|
|
||||||
let known_conns = s.list_connections().await.unwrap_or_default();
|
|
||||||
let active_conns = active_connections(
|
|
||||||
network_manager
|
|
||||||
.active_connections()
|
|
||||||
.await
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
// active_conns.sort(); active_connections should have already sorted the vector
|
|
||||||
let devices = network_manager.devices().await.ok().unwrap_or_default();
|
|
||||||
let wireless_access_point_futures: Vec<_> = devices
|
|
||||||
.into_iter()
|
|
||||||
.map(|device| async move {
|
|
||||||
if let Ok(Some(SpecificDevice::Wireless(wireless_device))) =
|
|
||||||
device.downcast_to_device().await
|
|
||||||
{
|
|
||||||
handle_wireless_device(wireless_device, device.hw_address().await.ok())
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let mut wireless_access_points = Vec::with_capacity(wireless_access_point_futures.len());
|
|
||||||
for f in wireless_access_point_futures {
|
|
||||||
let mut access_points = f.await;
|
|
||||||
wireless_access_points.append(&mut access_points);
|
|
||||||
}
|
|
||||||
let mut known_ssid = Vec::with_capacity(known_conns.len());
|
|
||||||
for c in known_conns {
|
|
||||||
let Ok(s) = c.get_settings().await else {
|
|
||||||
tracing::info!("Failed to get settings for known connection");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let s = Settings::new(s);
|
|
||||||
if let Some(cur_ssid) = s
|
|
||||||
.wifi
|
|
||||||
.clone()
|
|
||||||
.and_then(|w| w.ssid)
|
|
||||||
.and_then(|ssid| String::from_utf8(ssid).ok())
|
|
||||||
{
|
|
||||||
known_ssid.push(cur_ssid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let known_access_points: Vec<_> = wireless_access_points
|
|
||||||
.iter()
|
|
||||||
.filter(|a| {
|
|
||||||
known_ssid.contains(&a.ssid)
|
|
||||||
&& !active_conns
|
|
||||||
.iter()
|
|
||||||
.any(|ac| ac.name() == a.ssid && ac.hw_address() == a.hw_address)
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength));
|
|
||||||
self_.wireless_access_points = wireless_access_points;
|
|
||||||
for ap in &self_.wireless_access_points {
|
|
||||||
tracing::info!(
|
|
||||||
"AP ssid: {},\ttype: {:?},\tworking: {},\tstate: {:?}",
|
|
||||||
ap.ssid,
|
|
||||||
ap.network_type,
|
|
||||||
ap.working,
|
|
||||||
ap.state
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self_.active_conns = active_conns;
|
|
||||||
self_.known_access_points = known_access_points;
|
|
||||||
self_.connectivity = network_manager.connectivity().await?;
|
|
||||||
|
|
||||||
// Load available VPN connections
|
|
||||||
self_.available_vpns = load_vpn_connections(conn).await.unwrap_or_default();
|
|
||||||
|
|
||||||
Ok(self_)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.active_conns = Vec::new();
|
|
||||||
self.known_access_points = Vec::new();
|
|
||||||
self.wireless_access_points = Vec::new();
|
|
||||||
self.available_vpns = Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect_wifi(
|
|
||||||
&self,
|
|
||||||
conn: &Connection,
|
|
||||||
ssid: &str,
|
|
||||||
identity: Option<&str>,
|
|
||||||
password: Option<&str>,
|
|
||||||
hw_address: HwAddress,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let nm = NetworkManager::new(conn).await?;
|
|
||||||
|
|
||||||
for c in nm.active_connections().await.unwrap_or_default() {
|
|
||||||
if self.wireless_access_points.iter().any(|w| {
|
|
||||||
c.cached_id()
|
|
||||||
.is_ok_and(|opt| opt.is_some_and(|id| id == w.ssid))
|
|
||||||
&& w.hw_address == hw_address
|
|
||||||
}) {
|
|
||||||
_ = nm.deactivate_connection(&c).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(ap) = self
|
|
||||||
.wireless_access_points
|
|
||||||
.iter()
|
|
||||||
.find(|ap| ap.ssid == ssid && ap.hw_address == hw_address)
|
|
||||||
else {
|
|
||||||
return Err(anyhow::anyhow!("Access point not found"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([
|
|
||||||
(
|
|
||||||
"802-11-wireless",
|
|
||||||
HashMap::from([("ssid", Value::Array(ssid.as_bytes().into()))]),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"connection",
|
|
||||||
HashMap::from([
|
|
||||||
("id", Value::Str(ssid.into())),
|
|
||||||
("type", Value::Str("802-11-wireless".into())),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if let Some(identity) = identity {
|
|
||||||
conn_settings.insert(
|
|
||||||
"802-1x",
|
|
||||||
HashMap::from([
|
|
||||||
("identity", Value::Str(identity.into())),
|
|
||||||
// most common default
|
|
||||||
("eap", Value::Array(["peap"].as_slice().into())),
|
|
||||||
// most common default
|
|
||||||
("phase2-auth", Value::Str("mschapv2".into())),
|
|
||||||
("password", Value::Str(password.unwrap_or("").into())),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
let wireless = conn_settings.get_mut("802-11-wireless").unwrap();
|
|
||||||
wireless.insert("security", Value::Str("802-11-wireless-security".into()));
|
|
||||||
wireless.insert("mode", Value::Str("infrastructure".into()));
|
|
||||||
conn_settings.insert(
|
|
||||||
"802-11-wireless-security",
|
|
||||||
HashMap::from([("key-mgmt", Value::Str("wpa-eap".into()))]),
|
|
||||||
);
|
|
||||||
} else if let Some(pass) = password {
|
|
||||||
conn_settings.insert(
|
|
||||||
"802-11-wireless-security",
|
|
||||||
HashMap::from([
|
|
||||||
("psk", Value::Str(pass.into())),
|
|
||||||
("key-mgmt", Value::Str("wpa-psk".into())),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let devices = nm.devices().await?;
|
|
||||||
for device in devices {
|
|
||||||
let device_hw_address = device
|
|
||||||
.hw_address()
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|device_address| HwAddress::from_str(&device_address))
|
|
||||||
.unwrap_or_default();
|
|
||||||
if device_hw_address != hw_address {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if !matches!(
|
|
||||||
device.device_type().await.unwrap_or(DeviceType::Other),
|
|
||||||
DeviceType::Wifi
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let s = NetworkManagerSettings::new(conn).await?;
|
|
||||||
let known_conns = s.list_connections().await.unwrap_or_default();
|
|
||||||
let mut known_conn = None;
|
|
||||||
for c in known_conns {
|
|
||||||
let settings = c.get_settings().await.ok().unwrap_or_default();
|
|
||||||
|
|
||||||
let s = Settings::new(settings);
|
|
||||||
// todo try to add hw_address comparing here if it changes anything
|
|
||||||
if s.wifi
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|w| w.ssid.as_deref())
|
|
||||||
.is_some_and(|s| std::str::from_utf8(s).is_ok_and(|cur_ssid| cur_ssid == ssid))
|
|
||||||
{
|
|
||||||
known_conn = Some(c);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let active_conn = if let Some(known_conn) = known_conn.as_ref() {
|
|
||||||
// update settings if needed
|
|
||||||
if password.is_some() {
|
|
||||||
known_conn.update(conn_settings).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
nm.activate_connection(known_conn, &device).await?
|
|
||||||
} else {
|
|
||||||
let (_, active_conn) = nm
|
|
||||||
.add_and_activate_connection(conn_settings, device.inner().path(), &ap.path)
|
|
||||||
.await?;
|
|
||||||
let dummy = ActiveConnectionProxy::new(conn, active_conn).await?;
|
|
||||||
let active = ActiveConnectionProxy::builder(conn)
|
|
||||||
.destination(dummy.inner().destination().to_owned())
|
|
||||||
.unwrap()
|
|
||||||
.interface(dummy.inner().interface().to_owned())
|
|
||||||
.unwrap()
|
|
||||||
.path(dummy.inner().path().to_owned())
|
|
||||||
.unwrap()
|
|
||||||
.build()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
ActiveConnection::from(active)
|
|
||||||
};
|
|
||||||
let mut changes = active_conn.receive_state_changed().await;
|
|
||||||
() = tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
let mut count = 5;
|
|
||||||
loop {
|
|
||||||
let state = active_conn.state().await;
|
|
||||||
if let Ok(enums::ActiveConnectionState::Activated) = state {
|
|
||||||
return Ok(());
|
|
||||||
} else if let Ok(enums::ActiveConnectionState::Deactivated) = state {
|
|
||||||
anyhow::bail!("Failed to activate connection");
|
|
||||||
}
|
|
||||||
if let Ok(Some(s)) =
|
|
||||||
tokio::time::timeout(Duration::from_secs(20), changes.next()).await
|
|
||||||
{
|
|
||||||
let state = s.get().await.unwrap_or_default().into();
|
|
||||||
if matches!(state, enums::ActiveConnectionState::Activated) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
count -= 1;
|
|
||||||
if count <= 0 {
|
|
||||||
anyhow::bail!("Failed to activate connection");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow::anyhow!("No wifi device found"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
use super::{NetworkManagerEvent, NetworkManagerState};
|
|
||||||
use cosmic::{
|
|
||||||
iced::{self, Subscription},
|
|
||||||
iced_futures::stream,
|
|
||||||
};
|
|
||||||
use cosmic_dbus_networkmanager::nm::NetworkManager;
|
|
||||||
use futures::{SinkExt, StreamExt};
|
|
||||||
use std::{fmt::Debug, hash::Hash};
|
|
||||||
use zbus::Connection;
|
|
||||||
|
|
||||||
pub fn wireless_enabled_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
|
||||||
id: I,
|
|
||||||
conn: Connection,
|
|
||||||
) -> iced::Subscription<NetworkManagerEvent> {
|
|
||||||
let initial = State::Continue(conn);
|
|
||||||
Subscription::run_with_id(
|
|
||||||
id,
|
|
||||||
stream::channel(50, move |mut output| {
|
|
||||||
let mut state = initial;
|
|
||||||
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
state = start_listening(state, &mut output).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum State {
|
|
||||||
Continue(Connection),
|
|
||||||
Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_listening(
|
|
||||||
state: State,
|
|
||||||
output: &mut futures::channel::mpsc::Sender<NetworkManagerEvent>,
|
|
||||||
) -> State {
|
|
||||||
let conn = match state {
|
|
||||||
State::Continue(conn) => conn,
|
|
||||||
State::Error => iced::futures::future::pending().await,
|
|
||||||
};
|
|
||||||
|
|
||||||
let network_manager = match NetworkManager::new(&conn).await {
|
|
||||||
Ok(n) => n,
|
|
||||||
Err(why) => {
|
|
||||||
tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager");
|
|
||||||
return State::Error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut wireless_enabled_changed = network_manager.receive_wireless_enabled_changed().await;
|
|
||||||
|
|
||||||
while let Some(_change) = wireless_enabled_changed.next().await {
|
|
||||||
let new_state = NetworkManagerState::new(&conn).await.unwrap_or_default();
|
|
||||||
_ = output
|
|
||||||
.send(NetworkManagerEvent::WiFiEnabled(new_state))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
State::Continue(conn)
|
|
||||||
}
|
|
||||||
18
cosmic-applet-network/src/utils.rs
Normal file
18
cosmic-applet-network/src/utils.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use futures_util::future::select;
|
||||||
|
|
||||||
|
/// Spawn a background tasks and forward its messages
|
||||||
|
pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + Send + 'static>(
|
||||||
|
event_loop: impl FnOnce(async_fn_stream::StreamEmitter<M>) -> T + Send + 'static,
|
||||||
|
) -> (tokio::sync::oneshot::Sender<()>, cosmic::Task<M>) {
|
||||||
|
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||||
|
|
||||||
|
let task = cosmic::Task::stream(async_fn_stream::fn_stream(|emitter| async move {
|
||||||
|
select(
|
||||||
|
std::pin::pin!(cancel_rx),
|
||||||
|
std::pin::pin!(event_loop(emitter)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}));
|
||||||
|
|
||||||
|
(cancel_tx, task)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-notifications"
|
name = "cosmic-applet-notifications"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -25,4 +25,4 @@ i18n-embed = { workspace = true, features = [
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
zbus = { workspace = true, features = ["tokio", "p2p"] }
|
zbus = { workspace = true, features = ["tokio", "p2p"] }
|
||||||
url = "2.5.7"
|
url = "2.5.8"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[de]=Notifizierungszentrum
|
||||||
Name[sk]=Centrum oznámení
|
Name[sk]=Centrum oznámení
|
||||||
Name[sv]=Aviseringscenter
|
Name[sv]=Aviseringscenter
|
||||||
Name[es]=Centro de notificaciones
|
Name[es]=Centro de notificaciones
|
||||||
|
Name[it]=Centro notifiche
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-notifications
|
Exec=cosmic-applet-notifications
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,5 @@ show-more = { $more } további megjelenítése
|
||||||
clear-group = Csoport törlése
|
clear-group = Csoport törlése
|
||||||
clear-all = Minden értesítés törlése
|
clear-all = Minden értesítés törlése
|
||||||
do-not-disturb = Ne zavarjanak
|
do-not-disturb = Ne zavarjanak
|
||||||
notification-settings = Értesítési beállítások...
|
notification-settings = Értesítési beállítások…
|
||||||
no-notifications = Nincsenek értesítések
|
no-notifications = Nincsenek értesítések
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ hours-ago =
|
||||||
*[other] { $duration } horas atrás
|
*[other] { $duration } horas atrás
|
||||||
}
|
}
|
||||||
minutes-ago =
|
minutes-ago =
|
||||||
{ NUMBER($duration) ->
|
{ $duration ->
|
||||||
[1] 1 minuto atrás
|
[0] Agora mesmo
|
||||||
|
[one] 1 minuto atrás
|
||||||
*[other] { $duration } minutos atrás
|
*[other] { $duration } minutos atrás
|
||||||
}
|
}
|
||||||
show-less = Mostrar menos
|
show-less = Mostrar menos
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
hours-ago = { NUMBER($duration) ->
|
hours-ago =
|
||||||
[1] 1 小时前
|
{ $duration ->
|
||||||
*[other] {$duration} 小时前
|
[0] 刚刚
|
||||||
}
|
[one] 1 小时前
|
||||||
minutes-ago = { NUMBER($duration) ->
|
*[other] { $duration } 小时前
|
||||||
[1] 1 分钟前
|
}
|
||||||
*[other] {$duration} 分钟前
|
minutes-ago =
|
||||||
}
|
{ $duration ->
|
||||||
|
[0] 刚刚
|
||||||
|
[one] 1 分钟前
|
||||||
|
*[other] { $duration } 分钟前
|
||||||
|
}
|
||||||
show-less = 收起
|
show-less = 收起
|
||||||
show-more = 显示更多 {$more} 项
|
show-more = 显示剩余 { $more } 项
|
||||||
clear-group = 清除组
|
clear-group = 清除群组
|
||||||
clear-all = 清除所有通知
|
clear-all = 清除所有通知
|
||||||
do-not-disturb = 勿扰模式
|
do-not-disturb = 勿扰模式
|
||||||
notification-settings = 通知设置...
|
notification-settings = 通知设置...
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-power"
|
name = "cosmic-applet-power"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name[nl]=Gebruikerssessie
|
||||||
Name[sk]=Používateľská relácia
|
Name[sk]=Používateľská relácia
|
||||||
Name[sv]=Användarsession
|
Name[sv]=Användarsession
|
||||||
Name[es]=Sesión de usuario
|
Name[es]=Sesión de usuario
|
||||||
|
Name[it]=Sessione utente
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-power
|
Exec=cosmic-applet-power
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
power = Főkapcsoló
|
power = Főkapcsoló
|
||||||
settings = Beállítások...
|
settings = Beállítások…
|
||||||
lock-screen = Képernyő zárolása
|
lock-screen = Képernyő zárolása
|
||||||
lock-screen-shortcut = Super + Escape
|
lock-screen-shortcut = Super + Escape
|
||||||
log-out = Kijelentkezés
|
log-out = Kijelentkezés
|
||||||
|
|
@ -33,4 +33,4 @@ confirm-body =
|
||||||
[lock-screen] zárolni fogja a képernyőt
|
[lock-screen] zárolni fogja a képernyőt
|
||||||
[log-out] kijelentkezik
|
[log-out] kijelentkezik
|
||||||
*[other] alkalmazni fogja a kiválasztott műveletet
|
*[other] alkalmazni fogja a kiválasztott műveletet
|
||||||
} { $countdown } másodperc múlva.
|
} { $countdown } másodperc múlva
|
||||||
|
|
|
||||||
0
cosmic-applet-power/i18n/kk/cosmic_applet_power.ftl
Normal file
0
cosmic-applet-power/i18n/kk/cosmic_applet_power.ftl
Normal file
|
|
@ -1,10 +1,10 @@
|
||||||
power = 电源
|
power = 电源
|
||||||
settings = 设置...
|
settings = 设置...
|
||||||
lock-screen = 锁定屏幕
|
lock-screen = 锁定屏幕
|
||||||
lock-screen-shortcut = Super + Escape
|
lock-screen-shortcut = 超级键 + 退出键
|
||||||
log-out = 登出
|
log-out = 登出
|
||||||
log-out-shortcut = Super + Shift + Escape
|
log-out-shortcut = 超级键 + 换档键 + 退出键
|
||||||
suspend = 挂起
|
suspend = 待机
|
||||||
restart = 重启
|
restart = 重启
|
||||||
shutdown = 关机
|
shutdown = 关机
|
||||||
confirm = 确认
|
confirm = 确认
|
||||||
|
|
@ -18,19 +18,19 @@ confirm-button =
|
||||||
*[other] { confirm }
|
*[other] { confirm }
|
||||||
}
|
}
|
||||||
confirm-title =
|
confirm-title =
|
||||||
确认要{ $action ->
|
立即{ $action ->
|
||||||
[restart] { restart }
|
[restart] { restart }
|
||||||
[suspend] { suspend }
|
[suspend] { suspend }
|
||||||
[shutdown] { shutdown }
|
[shutdown] { shutdown }
|
||||||
[log-out] 退出所有应用并登出
|
[log-out] 退出所有应用并登出
|
||||||
*[other] 应用所选操作
|
*[other] 执行所选操作
|
||||||
}吗?
|
}?
|
||||||
confirm-body =
|
confirm-body =
|
||||||
系统将在 { $countdown } 秒后自动{ $action ->
|
系统将在 { $countdown } 秒后自动{ $action ->
|
||||||
[restart] 重启
|
[restart] 重启
|
||||||
[suspend] 挂起
|
[suspend] 待机
|
||||||
[shutdown] 关机
|
[shutdown] 关机
|
||||||
[lock-screen] 锁定屏幕
|
[lock-screen] 锁定屏幕
|
||||||
[log-out] 登出
|
[log-out] 登出
|
||||||
*[other] 应用所选操作
|
*[other] 执行所选操作
|
||||||
}。
|
}。
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-status-area"
|
name = "cosmic-applet-status-area"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name[pt]=Área de Notificações
|
||||||
Name[sk]=Panel oznámení
|
Name[sk]=Panel oznámení
|
||||||
Name[sv]=Fält för programikoner
|
Name[sv]=Fält för programikoner
|
||||||
Name[es]=Bandeja de notificaciones
|
Name[es]=Bandeja de notificaciones
|
||||||
|
Name[it]=Area notifiche
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-status-area
|
Exec=cosmic-applet-status-area
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use cosmic::{
|
||||||
applet::token::subscription::{TokenRequest, TokenUpdate, activation_token_subscription},
|
applet::token::subscription::{TokenRequest, TokenUpdate, activation_token_subscription},
|
||||||
cctk::sctk::reexports::calloop,
|
cctk::sctk::reexports::calloop,
|
||||||
iced::{
|
iced::{
|
||||||
self, Subscription,
|
self, Length, Subscription,
|
||||||
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
|
|
@ -567,21 +567,41 @@ fn menu_icon_button<'a>(
|
||||||
applet: &'a cosmic::applet::Context,
|
applet: &'a cosmic::applet::Context,
|
||||||
menu: &'a status_menu::State,
|
menu: &'a status_menu::State,
|
||||||
) -> cosmic::widget::Button<'a, Msg> {
|
) -> cosmic::widget::Button<'a, Msg> {
|
||||||
match (menu.icon_pixmap(), menu.icon_name(), menu.icon_theme_path()) {
|
let icon = menu.icon_handle().clone();
|
||||||
(Some(icon), "", _) => applet.icon_button_from_handle(icon.clone().symbolic(true)),
|
|
||||||
(_, name, Some(theme_path)) if name != "" => {
|
let theme = cosmic::theme::active();
|
||||||
let mut path = theme_path.to_owned();
|
let theme = theme.cosmic();
|
||||||
// XXX right way to lookup icon in dir?
|
|
||||||
path.push(name.to_owned() + ".svg");
|
let suggested = applet.suggested_size(true);
|
||||||
if !path.exists() {
|
let padding = applet.suggested_padding(true).1;
|
||||||
path.pop();
|
// let (major_padding, applet_padding_minor_axis) = applet.suggested_padding(true);
|
||||||
path.push(name.to_owned() + ".png");
|
// let (horizontal_padding, vertical_padding) = if applet.is_horizontal() {
|
||||||
}
|
// (major_padding, applet_padding_minor_axis)
|
||||||
let icon = cosmic::widget::icon::from_path(path).symbolic(true);
|
// } else {
|
||||||
applet.icon_button_from_handle(icon)
|
// (applet_padding_minor_axis, major_padding)
|
||||||
}
|
// };
|
||||||
(_, name, _) => applet.icon_button(name),
|
let symbolic = icon.symbolic;
|
||||||
}
|
|
||||||
|
cosmic::widget::button::custom(
|
||||||
|
cosmic::widget::layer_container(
|
||||||
|
cosmic::widget::icon(icon)
|
||||||
|
.class(if symbolic {
|
||||||
|
cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| {
|
||||||
|
cosmic::iced_widget::svg::Style {
|
||||||
|
color: Some(theme.cosmic().background.on.into()),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
cosmic::theme::Svg::default()
|
||||||
|
})
|
||||||
|
.width(Length::Fixed(suggested.0 as f32))
|
||||||
|
.height(Length::Fixed(suggested.1 as f32)),
|
||||||
|
)
|
||||||
|
.center(Length::Fill),
|
||||||
|
)
|
||||||
|
.width(Length::Fixed((suggested.0 + 2 * padding) as f32))
|
||||||
|
.height(Length::Fixed((suggested.1 + 2 * padding) as f32))
|
||||||
|
.class(cosmic::theme::Button::AppletIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() -> iced::Result {
|
pub fn main() -> iced::Result {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use cosmic::{
|
||||||
applet::{menu_button, token::subscription::TokenRequest},
|
applet::{menu_button, token::subscription::TokenRequest},
|
||||||
cctk::sctk::reexports::calloop,
|
cctk::sctk::reexports::calloop,
|
||||||
iced,
|
iced,
|
||||||
widget::icon,
|
widget::icon::{self, IconFallback},
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|
@ -24,10 +24,8 @@ pub struct State {
|
||||||
pub item: StatusNotifierItem,
|
pub item: StatusNotifierItem,
|
||||||
layout: Option<Layout>,
|
layout: Option<Layout>,
|
||||||
expanded: Option<i32>,
|
expanded: Option<i32>,
|
||||||
icon_name: String,
|
|
||||||
// TODO handle icon with multiple sizes?
|
// TODO handle icon with multiple sizes?
|
||||||
icon_pixmap: Option<icon::Handle>,
|
icon_handle: icon::Handle,
|
||||||
icon_theme_path: Option<PathBuf>,
|
|
||||||
click_event: Option<(i32, bool)>,
|
click_event: Option<(i32, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,9 +36,9 @@ impl State {
|
||||||
item,
|
item,
|
||||||
layout: None,
|
layout: None,
|
||||||
expanded: None,
|
expanded: None,
|
||||||
icon_name: String::new(),
|
icon_handle: icon::from_name("application-default")
|
||||||
icon_pixmap: None,
|
.prefer_svg(true)
|
||||||
icon_theme_path: None,
|
.handle(),
|
||||||
click_event: None,
|
click_event: None,
|
||||||
},
|
},
|
||||||
iced::Task::none(),
|
iced::Task::none(),
|
||||||
|
|
@ -64,24 +62,45 @@ impl State {
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
}
|
}
|
||||||
Msg::Icon(update) => {
|
Msg::Icon(update) => {
|
||||||
self.icon_name = update.name.unwrap_or_default();
|
let icon_name = update.name.unwrap_or_default();
|
||||||
self.icon_pixmap = update.pixmap.and_then(|icons| icons
|
|
||||||
.into_iter()
|
// Use the icon pixmap if an icon was not defined by name.
|
||||||
.max_by_key(|i| (i.width, i.height))
|
if icon_name.is_empty() {
|
||||||
.map(|mut i| {
|
let icon_pixmap = update.pixmap.and_then(|icons| icons
|
||||||
if i.width <= 0 || i.height <= 0 || i.bytes.is_empty() {
|
.into_iter()
|
||||||
// App sent invalid icon data during initialization - show placeholder until NewIcon signal
|
.max_by_key(|i| (i.width, i.height))
|
||||||
eprintln!("Skipping invalid icon: {}x{} with {} bytes, app may still be initializing",
|
.map(|mut i| {
|
||||||
i.width, i.height, i.bytes.len());
|
if i.width <= 0 || i.height <= 0 || i.bytes.is_empty() {
|
||||||
return icon::from_name("dialog-question").symbolic(true).handle();
|
// App sent invalid icon data during initialization - show placeholder until NewIcon signal
|
||||||
}
|
tracing::debug!("Skipping invalid icon: {}x{} with {} bytes, app may still be initializing",
|
||||||
// Convert ARGB to RGBA
|
i.width, i.height, i.bytes.len());
|
||||||
for pixel in i.bytes.chunks_exact_mut(4) {
|
return icon::from_name("dialog-question").symbolic(true).handle();
|
||||||
pixel.rotate_left(1);
|
}
|
||||||
}
|
// Convert ARGB to RGBA
|
||||||
icon::from_raster_pixels(i.width as u32, i.height as u32, i.bytes)
|
for pixel in i.bytes.chunks_exact_mut(4) {
|
||||||
}));
|
pixel.rotate_left(1);
|
||||||
self.icon_theme_path = update.theme_path;
|
}
|
||||||
|
icon::from_raster_pixels(i.width as u32, i.height as u32, i.bytes)
|
||||||
|
}));
|
||||||
|
|
||||||
|
if let Some(icon) = icon_pixmap {
|
||||||
|
self.icon_handle = icon.clone();
|
||||||
|
return iced::Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load icon by path if the name is a path.
|
||||||
|
self.icon_handle = if Path::new(&icon_name).exists() {
|
||||||
|
icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true)
|
||||||
|
} else {
|
||||||
|
icon::from_name(icon_name)
|
||||||
|
.prefer_svg(true)
|
||||||
|
.fallback(Some(IconFallback::Names(vec![
|
||||||
|
"application-default".into(),
|
||||||
|
"application-x-executable".into(),
|
||||||
|
])))
|
||||||
|
.handle()
|
||||||
|
};
|
||||||
|
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -134,16 +153,8 @@ impl State {
|
||||||
self.item.name()
|
self.item.name()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon_name(&self) -> &str {
|
pub fn icon_handle(&self) -> &icon::Handle {
|
||||||
&self.icon_name
|
&self.icon_handle
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon_pixmap(&self) -> Option<&icon::Handle> {
|
|
||||||
self.icon_pixmap.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon_theme_path(&self) -> Option<&Path> {
|
|
||||||
self.icon_theme_path.as_deref()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn popup_view(&self) -> cosmic::Element<'_, Msg> {
|
pub fn popup_view(&self) -> cosmic::Element<'_, Msg> {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ pub struct Icon {
|
||||||
pub struct IconUpdate {
|
pub struct IconUpdate {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub pixmap: Option<Vec<Icon>>,
|
pub pixmap: Option<Vec<Icon>>,
|
||||||
pub theme_path: Option<PathBuf>,
|
// pub theme_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusNotifierItem {
|
impl StatusNotifierItem {
|
||||||
|
|
@ -86,8 +86,15 @@ impl StatusNotifierItem {
|
||||||
format!("status-notifier-item-layout-{}", &self.name),
|
format!("status-notifier-item-layout-{}", &self.name),
|
||||||
async move {
|
async move {
|
||||||
let initial = futures::stream::once(get_layout(menu_proxy.clone()));
|
let initial = futures::stream::once(get_layout(menu_proxy.clone()));
|
||||||
let layout_updated_stream = menu_proxy.receive_layout_updated().await.unwrap();
|
|
||||||
let updates = layout_updated_stream.then(move |_| get_layout(menu_proxy.clone()));
|
let layout_updated = menu_proxy.receive_layout_updated().await.unwrap();
|
||||||
|
let props_updated = menu_proxy.receive_items_properties_updated().await.unwrap();
|
||||||
|
|
||||||
|
// Merge both streams - any update triggers a layout refetch
|
||||||
|
let updates =
|
||||||
|
futures::stream_select!(layout_updated.map(|_| ()), props_updated.map(|_| ()))
|
||||||
|
.then(move |()| get_layout(menu_proxy.clone()));
|
||||||
|
|
||||||
initial.chain(updates)
|
initial.chain(updates)
|
||||||
}
|
}
|
||||||
.flatten_stream(),
|
.flatten_stream(),
|
||||||
|
|
@ -98,11 +105,11 @@ impl StatusNotifierItem {
|
||||||
async fn icon_events(item_proxy: StatusNotifierItemProxy<'static>) -> IconUpdate {
|
async fn icon_events(item_proxy: StatusNotifierItemProxy<'static>) -> IconUpdate {
|
||||||
let icon_name = item_proxy.icon_name().await;
|
let icon_name = item_proxy.icon_name().await;
|
||||||
let icon_pixmap = item_proxy.icon_pixmap().await;
|
let icon_pixmap = item_proxy.icon_pixmap().await;
|
||||||
let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from);
|
// let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from);
|
||||||
IconUpdate {
|
IconUpdate {
|
||||||
name: icon_name.ok(),
|
name: icon_name.ok(),
|
||||||
pixmap: icon_pixmap.ok(),
|
pixmap: icon_pixmap.ok(),
|
||||||
theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()),
|
// theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,4 +290,11 @@ pub trait DBusMenu {
|
||||||
|
|
||||||
#[zbus(signal)]
|
#[zbus(signal)]
|
||||||
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
|
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
fn items_properties_updated(
|
||||||
|
&self,
|
||||||
|
updated_props: Vec<(i32, std::collections::HashMap<String, zvariant::OwnedValue>)>,
|
||||||
|
removed_props: Vec<(i32, Vec<String>)>,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-tiling"
|
name = "cosmic-applet-tiling"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ Name[nl]=Vensters tegelen
|
||||||
Name[sk]=Dláždenie okien
|
Name[sk]=Dláždenie okien
|
||||||
Name[sv]=Kakling
|
Name[sv]=Kakling
|
||||||
Name[es]=Ventanas mosaico
|
Name[es]=Ventanas mosaico
|
||||||
|
Name[it]=Tiling
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-tiling
|
Exec=cosmic-applet-tiling
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ shortcuts = Gyorsbillentyűk
|
||||||
navigate-windows = Ablakok navigálása
|
navigate-windows = Ablakok navigálása
|
||||||
move-window = Ablak mozgatása
|
move-window = Ablak mozgatása
|
||||||
toggle-floating-window = Az ablakok lebegtetésének be- és kikapcsolása
|
toggle-floating-window = Az ablakok lebegtetésének be- és kikapcsolása
|
||||||
view-all-shortcuts = Az összes gyorsbillentyű megtekintése...
|
view-all-shortcuts = Az összes gyorsbillentyű megtekintése…
|
||||||
active-hint = Aktív ablak kiemelése
|
active-hint = Aktív ablak kiemelése
|
||||||
gaps = Hézagok
|
gaps = Hézagok
|
||||||
floating-window-exceptions = Lebegő ablak kivételek...
|
floating-window-exceptions = Lebegő ablak kivételek…
|
||||||
window-management-settings = Ablakkezelési beállítások...
|
window-management-settings = Ablakkezelési beállítások…
|
||||||
all-workspaces = Összes munkaterület
|
all-workspaces = Összes munkaterület
|
||||||
per-workspace = Munkaterületenként
|
per-workspace = Munkaterületenként
|
||||||
super = Super
|
super = Super
|
||||||
|
|
|
||||||
0
cosmic-applet-tiling/i18n/kk/cosmic_applet_tiling.ftl
Normal file
0
cosmic-applet-tiling/i18n/kk/cosmic_applet_tiling.ftl
Normal file
|
|
@ -1,5 +1,5 @@
|
||||||
tile-windows = Organizar janelas lado a lado automaticamente
|
tile-windows = Organizar janelas lado a lado automaticamente
|
||||||
tile-current = Modo de janelas lado a lado
|
tile-current = Organizar janelas lado a lado na área de trabalho atual
|
||||||
shortcuts = Atalhos
|
shortcuts = Atalhos
|
||||||
navigate-windows = Navegar pelas janelas
|
navigate-windows = Navegar pelas janelas
|
||||||
move-window = Mover janela
|
move-window = Mover janela
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@ tile-current = 自动平铺当前工作区
|
||||||
shortcuts = 快捷键
|
shortcuts = 快捷键
|
||||||
navigate-windows = 切换窗口
|
navigate-windows = 切换窗口
|
||||||
move-window = 移动窗口
|
move-window = 移动窗口
|
||||||
toggle-floating-window = 切换浮动窗口
|
toggle-floating-window = 切换悬浮窗口
|
||||||
view-all-shortcuts = 显示所有快捷键...
|
view-all-shortcuts = 显示所有快捷键...
|
||||||
active-hint = 聚焦窗口提示
|
active-hint = 聚焦窗口提示
|
||||||
gaps = 窗口间距
|
gaps = 窗口间距
|
||||||
floating-window-exceptions = 浮动窗口例外...
|
floating-window-exceptions = 悬浮窗口例外...
|
||||||
window-management-settings = 窗口管理设置...
|
window-management-settings = 窗口管理设置...
|
||||||
all-workspaces = 所有工作区
|
all-workspaces = 所有工作区
|
||||||
per-workspace = 每个工作区
|
per-workspace = 每个工作区
|
||||||
super = Super
|
super = 超级键
|
||||||
shift = Shift
|
shift = 换档键
|
||||||
arrow-keys = 方向键
|
arrow-keys = 方向键
|
||||||
tiled = 平铺
|
tiled = 平铺
|
||||||
floating = 浮动
|
floating = 悬浮
|
||||||
autotile-behavior = 自动平铺所有工作区的窗口
|
autotile-behavior = 自动平铺所有工作区窗口
|
||||||
new-workspace = 新工作区行为
|
new-workspace = 新工作区行为
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-time"
|
name = "cosmic-applet-time"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name[nl]=Datum, tijd en agenda
|
||||||
Name[sk]=Dátum, čas a kalendár
|
Name[sk]=Dátum, čas a kalendár
|
||||||
Name[es]=Fecha, hora y calendario
|
Name[es]=Fecha, hora y calendario
|
||||||
Name[sv]=Datum, Tid & Kalender
|
Name[sv]=Datum, Tid & Kalender
|
||||||
|
Name[it]=Data, ora e calendario
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-time
|
Exec=cosmic-applet-time
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
datetime-settings = Dátum, idő és naptár beállítások...
|
datetime-settings = Dátum, idő és naptár beállítások…
|
||||||
|
|
|
||||||
0
cosmic-applet-time/i18n/kk/cosmic_applet_time.ftl
Normal file
0
cosmic-applet-time/i18n/kk/cosmic_applet_time.ftl
Normal file
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-workspaces"
|
name = "cosmic-applet-workspaces"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
authors = ["Ashley Wulber <ashley@system76.com>"]
|
authors = ["Ashley Wulber <ashley@system76.com>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name[nl]=Genummerde werkbladen
|
||||||
Name[sk]=Číslované pracovné plochy
|
Name[sk]=Číslované pracovné plochy
|
||||||
Name[sv]=Numrerade arbetsytor
|
Name[sv]=Numrerade arbetsytor
|
||||||
Name[es]=Espacios de trabajo numerados
|
Name[es]=Espacios de trabajo numerados
|
||||||
|
Name[it]=Spazi di lavoro numerati
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-applet-workspaces
|
Exec=cosmic-applet-workspaces
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applets-config"
|
name = "cosmic-applets-config"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applets"
|
name = "cosmic-applets"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-panel-button"
|
name = "cosmic-panel-button"
|
||||||
version = "1.0.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name[nl]=Knop snelstarter
|
||||||
Name[sk]=Tlačidlo spúšťača
|
Name[sk]=Tlačidlo spúšťača
|
||||||
Name[es]=Botón del lanzador
|
Name[es]=Botón del lanzador
|
||||||
Name[sv]=Knapp för programstartare
|
Name[sv]=Knapp för programstartare
|
||||||
|
Name[it]=Tasto Launcher
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-panel-button com.system76.CosmicLauncher
|
Exec=cosmic-panel-button com.system76.CosmicLauncher
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Name[nl]=Knop werkbladenoverzicht
|
||||||
Name[sk]=Tlačidlo pracovných plôch
|
Name[sk]=Tlačidlo pracovných plôch
|
||||||
Name[es]=Botón de espacios de trabajo
|
Name[es]=Botón de espacios de trabajo
|
||||||
Name[sv]=Knapp för arbetsytor
|
Name[sv]=Knapp för arbetsytor
|
||||||
|
Name[it]=Tasto Spazi di lavoro
|
||||||
Type=Application
|
Type=Application
|
||||||
Exec=cosmic-panel-button com.system76.CosmicWorkspaces
|
Exec=cosmic-panel-button com.system76.CosmicWorkspaces
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|
|
||||||
5
debian/changelog
vendored
5
debian/changelog
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
cosmic-applets (0.1.0) UNRELEASED; urgency=medium
|
cosmic-applets (1.0.2) noble; urgency=medium
|
||||||
|
|
||||||
|
[ Ashley Wulber ]
|
||||||
* Initial release.
|
* Initial release.
|
||||||
|
|
||||||
-- Ashley Wulber <ashley@system76.com> Thu, 07 Apr 2022 09:39:19 -0700
|
-- Michael Murphy <michael@mmurphy.dev> Tue, 13 Jan 2026 17:20:30 +0100
|
||||||
|
|
|
||||||
8
justfile
8
justfile
|
|
@ -73,3 +73,11 @@ vendor:
|
||||||
vendor-extract:
|
vendor-extract:
|
||||||
rm -rf vendor
|
rm -rf vendor
|
||||||
tar pxf vendor.tar
|
tar pxf vendor.tar
|
||||||
|
|
||||||
|
# Bump cargo version, create git commit, and create tag
|
||||||
|
tag version:
|
||||||
|
find -type f -name Cargo.toml -exec sed -i '0,/^version/s/^version.*/version = "{{version}}"/' '{}' \; -exec git add '{}' \;
|
||||||
|
cargo check
|
||||||
|
cargo clean
|
||||||
|
dch -D noble -v {{version}}
|
||||||
|
git add Cargo.lock debian/changelog
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue