Compare commits
36 commits
master
...
yoda-dock-
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df503977d | |||
| 43415d98b1 | |||
| c38dba6a52 | |||
|
|
090503ab2b | ||
|
|
658c38e913 | ||
|
|
3f530fd31f | ||
|
|
9c4299429e | ||
| da53a9f45f | |||
| 339ac4e3e4 | |||
| cc501c7637 | |||
| 93ab0f391d | |||
| 4416a5e9ea | |||
| 6c7f31e1ae | |||
| f2f53fc4d2 | |||
| 82e00a3e16 | |||
|
|
bcc8072a3b | ||
|
|
57a435e830 | ||
|
|
6fe087f4fd | ||
| 0fa93ba21f | |||
| 8fc11581ad | |||
| d090e60370 | |||
|
|
03c302d138 | ||
|
|
2362e7ce40 | ||
|
|
8a39826623 | ||
|
|
8d84396e57 | ||
|
|
65a9e142b5 | ||
|
|
666f0110d6 | ||
|
|
78a6f78621 | ||
|
|
8b2ff3df73 | ||
|
|
89a149034d | ||
|
|
737aaff4b0 | ||
|
|
c003924f08 | ||
|
|
0932bf4edf | ||
|
|
11d99c5df3 | ||
|
|
b7b768a998 | ||
|
|
ce51b784b7 |
109 changed files with 3023 additions and 1106 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -19,11 +19,11 @@ jobs:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: dtolnay/rust-toolchain@master
|
- uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: 1.90.0
|
toolchain: 1.93.1
|
||||||
components: clippy
|
components: clippy
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: sudo apt update && sudo apt install -y libxkbcommon-dev libwayland-dev libdbus-1-dev libpulse-dev libpipewire-0.3-dev libinput-dev
|
run: sudo apt update && sudo apt install -y libxkbcommon-dev libwayland-dev libdbus-1-dev libpulse-dev libpipewire-0.3-dev libinput-dev
|
||||||
- uses: actions-rs-plus/clippy-check@v2
|
- uses: actions-rs-plus/clippy-check@v2
|
||||||
with:
|
with:
|
||||||
toolchain: 1.90.0
|
toolchain: 1.93.1
|
||||||
args: --all --all-targets --all-features
|
args: --all --all-targets --all-features
|
||||||
|
|
|
||||||
15
.zed/settings.json
Normal file
15
.zed/settings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"format_on_save": "on",
|
||||||
|
"lsp": {
|
||||||
|
"rust-analyzer": {
|
||||||
|
"initialization_options": {
|
||||||
|
"check": {
|
||||||
|
"command": "clippy",
|
||||||
|
},
|
||||||
|
"rustfmt": {
|
||||||
|
"extraArgs": ["+nightly"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
831
Cargo.lock
generated
831
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
61
Cargo.toml
61
Cargo.toml
|
|
@ -25,11 +25,11 @@ resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be" }
|
cctk = { package = "cosmic-client-toolkit", path = "../cosmic-protocols/client-toolkit" }
|
||||||
cosmic-applets-config = { path = "cosmic-applets-config" }
|
cosmic-applets-config = { path = "cosmic-applets-config" }
|
||||||
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = [
|
cosmic-protocols = { path = "../cosmic-protocols", default-features = false, features = [
|
||||||
"client",
|
"client",
|
||||||
], rev = "d0e95be" }
|
]}
|
||||||
|
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
|
@ -38,7 +38,7 @@ i18n-embed = { version = "0.16.0", features = [
|
||||||
"desktop-requester",
|
"desktop-requester",
|
||||||
] }
|
] }
|
||||||
i18n-embed-fl = "0.10"
|
i18n-embed-fl = "0.10"
|
||||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = [
|
cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features = false, features = [
|
||||||
"applet",
|
"applet",
|
||||||
"applet-token",
|
"applet-token",
|
||||||
"dbus-config",
|
"dbus-config",
|
||||||
|
|
@ -48,6 +48,7 @@ libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = fa
|
||||||
"desktop-systemd-scope",
|
"desktop-systemd-scope",
|
||||||
"winit",
|
"winit",
|
||||||
] }
|
] }
|
||||||
|
cosmic-comp-config = { path = "../cosmic-comp/cosmic-comp-config" }
|
||||||
rust-embed = "8.11.0"
|
rust-embed = "8.11.0"
|
||||||
rust-embed-utils = "8.11.0"
|
rust-embed-utils = "8.11.0"
|
||||||
rustc-hash = "2.1"
|
rustc-hash = "2.1"
|
||||||
|
|
@ -58,14 +59,13 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
|
||||||
tracing-log = "0.2.0"
|
tracing-log = "0.2.0"
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||||
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
|
cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# opt-level = 3
|
opt-level = 3
|
||||||
# panic = "abort"
|
panic = "abort"
|
||||||
# lto = "thin"
|
lto = "thin"
|
||||||
opt-level = 1
|
|
||||||
|
|
||||||
[workspace.metadata.cargo-machete]
|
[workspace.metadata.cargo-machete]
|
||||||
ignored = ["libcosmic"]
|
ignored = ["libcosmic"]
|
||||||
|
|
@ -82,12 +82,51 @@ ignored = ["libcosmic"]
|
||||||
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
|
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
|
||||||
# winit = { path = "../winit" }
|
# winit = { path = "../winit" }
|
||||||
|
|
||||||
|
[patch."https://github.com/pop-os/libcosmic"]
|
||||||
|
cosmic-config = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" }
|
||||||
|
cosmic-theme = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-theme" }
|
||||||
|
iced = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced" }
|
||||||
|
iced_accessibility = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/accessibility" }
|
||||||
|
iced_core = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/core" }
|
||||||
|
iced_futures = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/futures" }
|
||||||
|
iced_graphics = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/graphics" }
|
||||||
|
iced_renderer = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/renderer" }
|
||||||
|
iced_runtime = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/runtime" }
|
||||||
|
iced_tiny_skia = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/tiny_skia" }
|
||||||
|
iced_wgpu = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/wgpu" }
|
||||||
|
iced_widget = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/widget" }
|
||||||
|
iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit" }
|
||||||
|
|
||||||
[patch."https://github.com/smithay/client-toolkit.git"]
|
[patch."https://github.com/smithay/client-toolkit.git"]
|
||||||
sctk = { package = "smithay-client-toolkit", version = "0.20.0" }
|
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 = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols" }
|
||||||
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
cosmic-client-toolkit = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols/client-toolkit" }
|
||||||
|
|
||||||
|
[patch."https://github.com/pop-os/cosmic-panel"]
|
||||||
|
cosmic-panel-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/cosmic-panel-config" }
|
||||||
|
xdg-shell-wrapper-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/xdg-shell-wrapper-config" }
|
||||||
|
|
||||||
|
[patch."https://github.com/pop-os/cosmic-notifications"]
|
||||||
|
cosmic-notifications-config = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-config" }
|
||||||
|
cosmic-notifications-util = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-util" }
|
||||||
|
|
||||||
|
[patch."https://github.com/pop-os/cosmic-settings"]
|
||||||
|
cosmic-settings-a11y-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/a11y-manager" }
|
||||||
|
cosmic-settings-accessibility-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/accessibility" }
|
||||||
|
cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" }
|
||||||
|
cosmic-settings-daemon-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/settings-daemon" }
|
||||||
|
cosmic-settings-network-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/network-manager" }
|
||||||
|
cosmic-settings-sound-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/sound" }
|
||||||
|
cosmic-settings-upower-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/upower" }
|
||||||
|
|
||||||
|
[patch."https://github.com/pop-os/cosmic-settings/"]
|
||||||
|
cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" }
|
||||||
|
cosmic-settings-network-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/network-manager" }
|
||||||
|
|
||||||
|
[patch."https://github.com/pop-os/cosmic-text.git"]
|
||||||
|
cosmic-text = { path = "../cosmic-text" }
|
||||||
|
|
||||||
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
|
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
|
||||||
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
|
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ futures.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
image = { version = "0.25.9", default-features = false }
|
image = { version = "0.25.9", default-features = false }
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
memmap2 = "0.9.10"
|
memmap2 = "0.9.10"
|
||||||
fastrand = "2.3.0"
|
fastrand = "2.3.0"
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
rustix.workspace = true
|
rustix.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@ 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
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
new-window = Nova Finestra
|
||||||
|
quit = Surt
|
||||||
|
run = Executa
|
||||||
|
run-on = Executa a { $gpu }
|
||||||
|
quit-all = Surt de totes
|
||||||
|
run-on-default = (Per defecte)
|
||||||
|
cosmic-app-list = Safata d'Aplicacions
|
||||||
|
pin = Ancora a la safata
|
||||||
|
|
@ -1 +1,8 @@
|
||||||
new-window = Νέο παράθυρο
|
new-window = Νέο παράθυρο
|
||||||
|
quit = Έξοδος
|
||||||
|
cosmic-app-list = Περιοχή εφαρμογών
|
||||||
|
run = Εκτέλεση
|
||||||
|
run-on = Εκτέλεση με { $gpu }
|
||||||
|
quit-all = Έξοδος από όλα
|
||||||
|
run-on-default = (Προεπιλογή)
|
||||||
|
pin = Καρφίτσωμα στην περιοχή εφαρμογών
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,14 @@ new-window = New Window
|
||||||
run = Run
|
run = Run
|
||||||
run-on = Run on {$gpu}
|
run-on = Run on {$gpu}
|
||||||
run-on-default = (Default)
|
run-on-default = (Default)
|
||||||
|
edit-launcher = Edit launcher
|
||||||
|
launcher-name = Name
|
||||||
|
launcher-command = Command
|
||||||
|
launcher-icon = Icon
|
||||||
|
launcher-icon-theme = Theme
|
||||||
|
launcher-icon-search = Search icons
|
||||||
|
launcher-icon-catalog-loading = Loading icons
|
||||||
|
launcher-icon-catalog-empty = No icons
|
||||||
|
launcher-icons = icons
|
||||||
|
save = Save
|
||||||
|
cancel = Cancel
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,17 @@ pin = Épingler à la barre d'applis
|
||||||
quit = Quitter
|
quit = Quitter
|
||||||
quit-all = Tout quitter
|
quit-all = Tout quitter
|
||||||
new-window = Nouvelle fenêtre
|
new-window = Nouvelle fenêtre
|
||||||
run = Lancer
|
run = Exécuter
|
||||||
run-on = Lancer avec { $gpu }
|
run-on = Lancer avec { $gpu }
|
||||||
run-on-default = (Défaut)
|
run-on-default = (Défaut)
|
||||||
|
edit-launcher = Modifier le lanceur
|
||||||
|
launcher-name = Nom
|
||||||
|
launcher-command = Commande
|
||||||
|
launcher-icon = Icône
|
||||||
|
launcher-icon-theme = Thème
|
||||||
|
launcher-icon-search = Rechercher une icône
|
||||||
|
launcher-icon-catalog-loading = Chargement des icônes
|
||||||
|
launcher-icon-catalog-empty = Aucune icône
|
||||||
|
launcher-icons = icônes
|
||||||
|
save = Enregistrer
|
||||||
|
cancel = Annuler
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
quit = Шығу
|
quit = Шығу
|
||||||
run = Іске қосу
|
run = Орындау
|
||||||
run-on = { $gpu } арқылы іске қосу
|
run-on = { $gpu } арқылы іске қосу
|
||||||
run-on-default = (Әдепкі)
|
run-on-default = (Әдепкі)
|
||||||
cosmic-app-list = Қолданбалар сөресі
|
cosmic-app-list = Қолданбалар сөресі
|
||||||
|
|
|
||||||
0
cosmic-app-list/i18n/lo/cosmic_app_list.ftl
Normal file
0
cosmic-app-list/i18n/lo/cosmic_app_list.ftl
Normal file
|
|
@ -3,6 +3,6 @@ pin = Закрепить на панели приложений
|
||||||
quit = Выйти
|
quit = Выйти
|
||||||
quit-all = Завершить все
|
quit-all = Завершить все
|
||||||
new-window = Новое окно
|
new-window = Новое окно
|
||||||
run = Запустить
|
run = Выполнить
|
||||||
run-on = Запустить на { $gpu }
|
run-on = Запустить на { $gpu }
|
||||||
run-on-default = (По умолчанию)
|
run-on-default = (По умолчанию)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
new-window = Нови прозор
|
||||||
|
quit = Изађи
|
||||||
|
run = Покрени
|
||||||
|
run-on = Покрени на { $gpu }
|
||||||
|
quit-all = Изађи из свега
|
||||||
|
run-on-default = (подразумевано)
|
||||||
|
cosmic-app-list = Системска касета
|
||||||
|
pin = Закачи у системску касету
|
||||||
|
|
@ -3,6 +3,6 @@ pin = Закріпити
|
||||||
quit = Вийти
|
quit = Вийти
|
||||||
quit-all = Закрити всі
|
quit-all = Закрити всі
|
||||||
new-window = Нове вікно
|
new-window = Нове вікно
|
||||||
run = Запустити
|
run = Виконати
|
||||||
run-on = Запустити на { $gpu }
|
run-on = Запустити на { $gpu }
|
||||||
run-on-default = (Основна)
|
run-on-default = (Основна)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
fl,
|
fl, icon_catalog,
|
||||||
|
launcher_edit::{self, LauncherEditRequest},
|
||||||
wayland_subscription::{
|
wayland_subscription::{
|
||||||
OutputUpdate, ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
|
OutputUpdate, ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
|
||||||
wayland_subscription,
|
wayland_subscription,
|
||||||
|
|
@ -43,11 +44,11 @@ use cosmic::{
|
||||||
surface,
|
surface,
|
||||||
theme::{self, Button, Container},
|
theme::{self, Button, Container},
|
||||||
widget::{
|
widget::{
|
||||||
DndDestination, Image, button, container, divider, dnd_source,
|
DndDestination, Image, button, container, divider, dnd_source, grid,
|
||||||
icon::{self, from_name},
|
icon::{self, from_name},
|
||||||
image::Handle,
|
image::Handle,
|
||||||
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
|
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
|
||||||
svg, text,
|
scrollable, svg, text, text_input,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
|
|
@ -64,6 +65,8 @@ use tokio::time::sleep;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
static MIME_TYPE: &str = "text/uri-list";
|
static MIME_TYPE: &str = "text/uri-list";
|
||||||
|
const MAX_VISIBLE_ICON_CHOICES: usize = 120;
|
||||||
|
const ICON_CATALOG_COLUMNS: usize = 6;
|
||||||
|
|
||||||
pub fn run() -> cosmic::iced::Result {
|
pub fn run() -> cosmic::iced::Result {
|
||||||
cosmic::applet::run::<CosmicAppList>(())
|
cosmic::applet::run::<CosmicAppList>(())
|
||||||
|
|
@ -187,6 +190,11 @@ impl DockItem {
|
||||||
dot_border_radius: [f32; 4],
|
dot_border_radius: [f32; 4],
|
||||||
window_id: window::Id,
|
window_id: window::Id,
|
||||||
filter: Option<&dyn Fn(&ToplevelInfo) -> bool>,
|
filter: Option<&dyn Fn(&ToplevelInfo) -> bool>,
|
||||||
|
// Yoda: multiplier on the computed icon size (1.0 = default,
|
||||||
|
// >1.0 = magnified e.g. on hover for the macOS Tahoe effect).
|
||||||
|
// Applied to the icon's rendered width/height only — indicator
|
||||||
|
// dot and surrounding layout stay at base size.
|
||||||
|
icon_scale: f32,
|
||||||
) -> Element<'_, Message> {
|
) -> Element<'_, Message> {
|
||||||
let Self {
|
let Self {
|
||||||
toplevels,
|
toplevels,
|
||||||
|
|
@ -205,17 +213,35 @@ impl DockItem {
|
||||||
};
|
};
|
||||||
let toplevel_count = filtered_toplevels.len();
|
let toplevel_count = filtered_toplevels.len();
|
||||||
|
|
||||||
|
// Cairo-like : pastille plus petite + atténuée quand toutes les fenêtres
|
||||||
|
// de cette app sont minimisées.
|
||||||
|
let all_minimized = toplevel_count > 0
|
||||||
|
&& filtered_toplevels
|
||||||
|
.iter()
|
||||||
|
.all(|(info, _)| info.state.contains(&State::Minimized));
|
||||||
|
|
||||||
let app_icon = AppletIconData::new(applet);
|
let app_icon = AppletIconData::new(applet);
|
||||||
|
|
||||||
|
// Yoda: scaled icon size for hover magnification. Clamped so
|
||||||
|
// tiny floats don't round to 0 and huge ones stay within u16.
|
||||||
|
let scaled_icon_size = ((f32::from(app_icon.icon_size) * icon_scale).round() as i32)
|
||||||
|
.clamp(1, u16::MAX as i32) as u16;
|
||||||
let cosmic_icon = cosmic::widget::icon(
|
let cosmic_icon = cosmic::widget::icon(
|
||||||
fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()).as_cosmic_icon(),
|
fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()).as_cosmic_icon(),
|
||||||
)
|
)
|
||||||
// sets the preferred icon size variant
|
// sets the preferred icon size variant
|
||||||
.size(128)
|
.size(128)
|
||||||
.width(app_icon.icon_size.into())
|
.width(scaled_icon_size.into())
|
||||||
.height(app_icon.icon_size.into());
|
.height(scaled_icon_size.into());
|
||||||
|
|
||||||
let indicator = {
|
let indicator = {
|
||||||
|
// Padding réduit quand minimisée → pastille plus petite.
|
||||||
|
let effective_radius = if all_minimized {
|
||||||
|
(app_icon.dot_radius * 0.55).max(1.0)
|
||||||
|
} else {
|
||||||
|
app_icon.dot_radius
|
||||||
|
};
|
||||||
|
|
||||||
let container = if toplevel_count <= 1 {
|
let container = if toplevel_count <= 1 {
|
||||||
vertical_space().height(Length::Fixed(0.0))
|
vertical_space().height(Length::Fixed(0.0))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -229,22 +255,34 @@ impl DockItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.apply(container)
|
.apply(container)
|
||||||
.padding(app_icon.dot_radius);
|
.padding(effective_radius);
|
||||||
|
|
||||||
if toplevel_count == 0 {
|
if toplevel_count == 0 {
|
||||||
container
|
container
|
||||||
} else {
|
} else {
|
||||||
container.class(theme::Container::custom(move |theme| container::Style {
|
container.class(theme::Container::custom(move |theme| {
|
||||||
background: if is_focused {
|
let cosmic = theme.cosmic();
|
||||||
Some(Background::Color(theme.cosmic().accent_color().into()))
|
let accent: iced::Color = cosmic.accent_color().into();
|
||||||
|
let on_bg: iced::Color = cosmic.on_bg_color().into();
|
||||||
|
// Teinte neutre atténuée quand toutes les fenêtres sont minimisées.
|
||||||
|
let muted = iced::Color { a: 0.45, ..on_bg };
|
||||||
|
|
||||||
|
let fill = if all_minimized {
|
||||||
|
muted
|
||||||
|
} else if is_focused {
|
||||||
|
accent
|
||||||
} else {
|
} else {
|
||||||
Some(Background::Color(theme.cosmic().on_bg_color().into()))
|
on_bg
|
||||||
},
|
};
|
||||||
|
|
||||||
|
container::Style {
|
||||||
|
background: Some(Background::Color(fill)),
|
||||||
border: Border {
|
border: Border {
|
||||||
radius: dot_border_radius.into(),
|
radius: dot_border_radius.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -352,10 +390,35 @@ pub struct Popup {
|
||||||
popup_type: PopupType,
|
popup_type: PopupType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct LauncherEditState {
|
||||||
|
original_app_id: String,
|
||||||
|
source_path: PathBuf,
|
||||||
|
original_name: String,
|
||||||
|
original_exec: String,
|
||||||
|
name: String,
|
||||||
|
exec: String,
|
||||||
|
icon: String,
|
||||||
|
terminal: bool,
|
||||||
|
saving: bool,
|
||||||
|
error: Option<String>,
|
||||||
|
icon_catalog: IconCatalogState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct IconCatalogState {
|
||||||
|
theme: String,
|
||||||
|
query: String,
|
||||||
|
entries: Vec<icon_catalog::IconCatalogEntry>,
|
||||||
|
loading: bool,
|
||||||
|
truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
struct CosmicAppList {
|
struct CosmicAppList {
|
||||||
core: cosmic::app::Core,
|
core: cosmic::app::Core,
|
||||||
popup: Option<Popup>,
|
popup: Option<Popup>,
|
||||||
|
launcher_edit: Option<LauncherEditState>,
|
||||||
subscription_ctr: u32,
|
subscription_ctr: u32,
|
||||||
item_ctr: u32,
|
item_ctr: u32,
|
||||||
desktop_entries: Vec<DesktopEntry>,
|
desktop_entries: Vec<DesktopEntry>,
|
||||||
|
|
@ -374,6 +437,21 @@ struct CosmicAppList {
|
||||||
output_list: FxHashMap<WlOutput, OutputInfo>,
|
output_list: FxHashMap<WlOutput, OutputInfo>,
|
||||||
locales: Vec<String>,
|
locales: Vec<String>,
|
||||||
hovered_toplevel: Option<ExtForeignToplevelHandleV1>,
|
hovered_toplevel: Option<ExtForeignToplevelHandleV1>,
|
||||||
|
/// Yoda: which dock icon the pointer is currently over (for hover
|
||||||
|
/// magnification). None = no dock icon hovered.
|
||||||
|
hovered_dock_item: Option<DockItemId>,
|
||||||
|
/// Yoda: animated "virtual cursor" center used by the fisheye
|
||||||
|
/// formula — lerps toward the real hovered icon's center on each
|
||||||
|
/// AnimTick, so the bell curve slides smoothly from one icon to the
|
||||||
|
/// next instead of snapping.
|
||||||
|
anim_hover_center: Option<(f32, f32)>,
|
||||||
|
/// Yoda: fade-in/out intensity of the magnification effect
|
||||||
|
/// (0.0 = icons flat, 1.0 = full fisheye). Targets 1.0 while the
|
||||||
|
/// pointer is over any dock icon, 0.0 otherwise. Lerped on AnimTick.
|
||||||
|
anim_hover_intensity: f32,
|
||||||
|
/// Yoda: timestamp of the last AnimTick, for dt-based exponential
|
||||||
|
/// smoothing. `None` on first tick.
|
||||||
|
anim_last_tick: Option<std::time::Instant>,
|
||||||
overflow_favorites_popup: Option<window::Id>,
|
overflow_favorites_popup: Option<window::Id>,
|
||||||
overflow_active_popup: Option<window::Id>,
|
overflow_active_popup: Option<window::Id>,
|
||||||
}
|
}
|
||||||
|
|
@ -382,13 +460,33 @@ struct CosmicAppList {
|
||||||
pub enum PopupType {
|
pub enum PopupType {
|
||||||
RightClickMenu,
|
RightClickMenu,
|
||||||
ToplevelList,
|
ToplevelList,
|
||||||
|
LauncherEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Message {
|
enum Message {
|
||||||
Wayland(WaylandUpdate),
|
Wayland(WaylandUpdate),
|
||||||
PinApp(u32),
|
PinApp(u32),
|
||||||
UnpinApp(u32),
|
UnpinApp(u32),
|
||||||
|
EditLauncher(u32),
|
||||||
|
LauncherNameChanged(String),
|
||||||
|
LauncherExecChanged(String),
|
||||||
|
LauncherIconChanged(String),
|
||||||
|
LauncherIconSearchChanged(String),
|
||||||
|
LauncherIconSelected(String),
|
||||||
|
ReloadLauncherIconCatalog,
|
||||||
|
LauncherIconCatalogLoaded(icon_catalog::IconCatalog),
|
||||||
|
SaveLauncherEdit,
|
||||||
|
CancelLauncherEdit,
|
||||||
|
LauncherEditSaved(Result<launcher_edit::LauncherEditResult, String>),
|
||||||
|
/// Yoda: pointer entered (Some) or left (None) a dock icon — drives
|
||||||
|
/// the macOS Tahoe-style hover magnification effect.
|
||||||
|
DockItemHover(Option<DockItemId>),
|
||||||
|
/// Yoda: ticked at ~60fps by the animation subscription. Advances
|
||||||
|
/// anim_hover_center + anim_hover_intensity toward their targets so
|
||||||
|
/// the fisheye effect transitions smoothly instead of snapping.
|
||||||
|
AnimTick(std::time::Instant),
|
||||||
Popup(u32, window::Id),
|
Popup(u32, window::Id),
|
||||||
Pressed(window::Id),
|
Pressed(window::Id),
|
||||||
ToplevelListPopup(u32, window::Id),
|
ToplevelListPopup(u32, window::Id),
|
||||||
|
|
@ -626,6 +724,143 @@ pub fn menu_control_padding() -> Padding {
|
||||||
[spacing.space_xxs, spacing.space_s].into()
|
[spacing.space_xxs, spacing.space_s].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn launcher_icon_editor(edit: &LauncherEditState) -> Element<'_, Message> {
|
||||||
|
let spacing = theme::spacing();
|
||||||
|
let selected_icon = edit.icon.trim();
|
||||||
|
let query = edit.icon_catalog.query.trim().to_ascii_lowercase();
|
||||||
|
let mut total_matches = 0usize;
|
||||||
|
let mut visible_count = 0usize;
|
||||||
|
let mut icon_grid = grid()
|
||||||
|
.width(Length::Fill)
|
||||||
|
.column_spacing(spacing.space_xxs)
|
||||||
|
.row_spacing(spacing.space_xxs);
|
||||||
|
|
||||||
|
for entry in edit.icon_catalog.entries.iter().filter(|entry| {
|
||||||
|
query.is_empty() || entry.name.to_ascii_lowercase().contains(query.as_str())
|
||||||
|
}) {
|
||||||
|
total_matches += 1;
|
||||||
|
if visible_count >= MAX_VISIBLE_ICON_CHOICES {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if visible_count > 0 && visible_count % ICON_CATALOG_COLUMNS == 0 {
|
||||||
|
icon_grid = icon_grid.insert_row();
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = selected_icon == entry.name;
|
||||||
|
let icon_preview = cosmic::widget::icon(
|
||||||
|
fde::IconSource::from_unknown(entry.name.as_str()).as_cosmic_icon(),
|
||||||
|
)
|
||||||
|
.size(32)
|
||||||
|
.width(Length::Fixed(32.0))
|
||||||
|
.height(Length::Fixed(32.0));
|
||||||
|
|
||||||
|
let label = text::caption(entry.name.as_str())
|
||||||
|
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.center();
|
||||||
|
|
||||||
|
let tile = column![icon_preview, label]
|
||||||
|
.align_x(Alignment::Center)
|
||||||
|
.spacing(4)
|
||||||
|
.width(Length::Fixed(70.0));
|
||||||
|
|
||||||
|
let tile_button = button::custom(tile)
|
||||||
|
.class(if selected {
|
||||||
|
Button::Suggested
|
||||||
|
} else {
|
||||||
|
Button::Image
|
||||||
|
})
|
||||||
|
.selected(selected)
|
||||||
|
.on_press(Message::LauncherIconSelected(entry.name.clone()))
|
||||||
|
.padding(6)
|
||||||
|
.width(Length::Fixed(74.0))
|
||||||
|
.height(Length::Fixed(76.0));
|
||||||
|
|
||||||
|
icon_grid = icon_grid.push(tile_button);
|
||||||
|
visible_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_icon =
|
||||||
|
cosmic::widget::icon(fde::IconSource::from_unknown(edit.icon.as_str()).as_cosmic_icon())
|
||||||
|
.size(32)
|
||||||
|
.width(Length::Fixed(36.0))
|
||||||
|
.height(Length::Fixed(36.0));
|
||||||
|
|
||||||
|
let icon_value = row![
|
||||||
|
current_icon,
|
||||||
|
text_input("", edit.icon.as_str())
|
||||||
|
.label(fl!("launcher-icon"))
|
||||||
|
.on_input(Message::LauncherIconChanged)
|
||||||
|
.on_submit(|_| Message::SaveLauncherEdit)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(14),
|
||||||
|
button::icon(from_name("view-refresh-symbolic"))
|
||||||
|
.on_press(Message::ReloadLauncherIconCatalog)
|
||||||
|
.padding(spacing.space_xxs),
|
||||||
|
]
|
||||||
|
.spacing(spacing.space_xs)
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
|
let visible_total = if edit.icon_catalog.truncated {
|
||||||
|
format!(
|
||||||
|
"{}/{}+ {}",
|
||||||
|
visible_count,
|
||||||
|
total_matches,
|
||||||
|
fl!("launcher-icons")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}/{} {}",
|
||||||
|
visible_count,
|
||||||
|
total_matches,
|
||||||
|
fl!("launcher-icons")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let catalog_header = row![
|
||||||
|
text::caption(format!(
|
||||||
|
"{}: {}",
|
||||||
|
fl!("launcher-icon-theme"),
|
||||||
|
edit.icon_catalog.theme
|
||||||
|
)),
|
||||||
|
horizontal_space(),
|
||||||
|
text::caption(visible_total),
|
||||||
|
]
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
|
let catalog_body: Element<_> = if edit.icon_catalog.loading {
|
||||||
|
container(text::body(fl!("launcher-icon-catalog-loading")))
|
||||||
|
.center(Length::Fill)
|
||||||
|
.height(Length::Fixed(220.0))
|
||||||
|
.into()
|
||||||
|
} else if total_matches == 0 {
|
||||||
|
container(text::body(fl!("launcher-icon-catalog-empty")))
|
||||||
|
.center(Length::Fill)
|
||||||
|
.height(Length::Fixed(220.0))
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
scrollable(icon_grid)
|
||||||
|
.height(Length::Fixed(240.0))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
column![
|
||||||
|
icon_value,
|
||||||
|
catalog_header,
|
||||||
|
text_input("", edit.icon_catalog.query.as_str())
|
||||||
|
.label(fl!("launcher-icon-search"))
|
||||||
|
.on_input(Message::LauncherIconSearchChanged)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(14),
|
||||||
|
catalog_body,
|
||||||
|
]
|
||||||
|
.spacing(spacing.space_s)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
fn find_desktop_entries<'a>(
|
fn find_desktop_entries<'a>(
|
||||||
desktop_entries: &'a [fde::DesktopEntry],
|
desktop_entries: &'a [fde::DesktopEntry],
|
||||||
app_ids: &'a [String],
|
app_ids: &'a [String],
|
||||||
|
|
@ -647,6 +882,79 @@ impl CosmicAppList {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yoda: macOS-Tahoe fisheye-style magnification. Returns the
|
||||||
|
/// per-icon size multiplier based on the distance (in pixels) from
|
||||||
|
/// the currently hovered icon's center to this icon's center.
|
||||||
|
///
|
||||||
|
/// Uses a gaussian bell curve so the hovered icon peaks at
|
||||||
|
/// 1.0 + PEAK, immediate neighbors still bulge noticeably, and icons
|
||||||
|
/// further away relax back to 1.0× — that's the smooth neighbour
|
||||||
|
/// deformation people associate with the macOS Dock.
|
||||||
|
///
|
||||||
|
/// Falls back to binary 1.3×/1.0× when the rectangle tracker hasn't
|
||||||
|
/// populated yet (first render, or just after layout changes).
|
||||||
|
///
|
||||||
|
/// Uses the animated hover center (anim_hover_center) and intensity
|
||||||
|
/// (anim_hover_intensity) so inter-icon transitions slide smoothly
|
||||||
|
/// and the whole effect fades in/out at the dock's edges.
|
||||||
|
fn icon_scale_for(&self, id: &DockItemId) -> f32 {
|
||||||
|
const PEAK: f32 = 0.35;
|
||||||
|
// sigma expressed in multiples of the hovered icon's size —
|
||||||
|
// 1.4 means the ±1 neighbors sit ~0.7σ away and still bulge
|
||||||
|
// visibly, while ±3+ has collapsed to ~1.0× (fisheye footprint
|
||||||
|
// close to 5 icons wide, Tahoe-ish).
|
||||||
|
const SIGMA_FACTOR: f32 = 1.4;
|
||||||
|
|
||||||
|
// No intensity at all → skip the rest.
|
||||||
|
if self.anim_hover_intensity < 0.001 {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the animated center (smooth); fall back to the real
|
||||||
|
// hovered icon's center (first render, before tick fires).
|
||||||
|
let hover_center = self.anim_hover_center.or_else(|| {
|
||||||
|
let hovered_id = self.hovered_dock_item.as_ref()?;
|
||||||
|
let r = self.rectangles.get(hovered_id)?;
|
||||||
|
Some((r.x + r.width / 2.0, r.y + r.height / 2.0))
|
||||||
|
});
|
||||||
|
let Some(hover_center) = hover_center else {
|
||||||
|
// No coords yet — visibly peak on the exact hovered id so
|
||||||
|
// the very first frame still responds.
|
||||||
|
return if self.hovered_dock_item.as_ref() == Some(id) {
|
||||||
|
1.0 + PEAK * self.anim_hover_intensity
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let this_rect = match self.rectangles.get(id) {
|
||||||
|
Some(r) => r,
|
||||||
|
None => return 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_horizontal = matches!(
|
||||||
|
self.core.applet.anchor,
|
||||||
|
PanelAnchor::Top | PanelAnchor::Bottom
|
||||||
|
);
|
||||||
|
let this_center = if is_horizontal {
|
||||||
|
this_rect.x + this_rect.width / 2.0
|
||||||
|
} else {
|
||||||
|
this_rect.y + this_rect.height / 2.0
|
||||||
|
};
|
||||||
|
let hover_axis = if is_horizontal { hover_center.0 } else { hover_center.1 };
|
||||||
|
let distance = (this_center - hover_axis).abs();
|
||||||
|
let icon_extent = if is_horizontal {
|
||||||
|
this_rect.width
|
||||||
|
} else {
|
||||||
|
this_rect.height
|
||||||
|
}
|
||||||
|
.max(1.0);
|
||||||
|
let sigma = icon_extent * SIGMA_FACTOR;
|
||||||
|
// exp(-t²) bell curve, t = distance / sigma
|
||||||
|
let t = distance / sigma;
|
||||||
|
1.0 + PEAK * self.anim_hover_intensity * (-t * t).exp()
|
||||||
|
}
|
||||||
|
|
||||||
fn is_on_current_monitor_and_workspace(&self, toplevel_info: &ToplevelInfo) -> bool {
|
fn is_on_current_monitor_and_workspace(&self, toplevel_info: &ToplevelInfo) -> bool {
|
||||||
use cosmic_app_list_config::ToplevelFilter;
|
use cosmic_app_list_config::ToplevelFilter;
|
||||||
|
|
||||||
|
|
@ -691,10 +999,45 @@ impl CosmicAppList {
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_pinned_list_from_config(&mut self) {
|
||||||
|
for item in self.pinned_list.drain(..) {
|
||||||
|
if !item.toplevels.is_empty() {
|
||||||
|
self.active_list.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pinned_list = find_desktop_entries(&self.desktop_entries, &self.config.favorites)
|
||||||
|
.zip(&self.config.favorites)
|
||||||
|
.map(|(de, original_id)| {
|
||||||
|
if let Some(p) = self
|
||||||
|
.active_list
|
||||||
|
.iter()
|
||||||
|
.position(|dock_item| dock_item.desktop_info.id() == de.id())
|
||||||
|
{
|
||||||
|
let mut d = self.active_list.remove(p);
|
||||||
|
d.desktop_info = de.clone();
|
||||||
|
d.original_app_id.clone_from(original_id);
|
||||||
|
d
|
||||||
|
} else {
|
||||||
|
self.item_ctr += 1;
|
||||||
|
DockItem {
|
||||||
|
id: self.item_ctr,
|
||||||
|
toplevels: Vec::new(),
|
||||||
|
desktop_info: de.clone(),
|
||||||
|
original_app_id: original_id.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
/// Close any open popups.
|
/// Close any open popups.
|
||||||
fn close_popups(&mut self) -> Task<cosmic::Action<Message>> {
|
fn close_popups(&mut self) -> Task<cosmic::Action<Message>> {
|
||||||
let mut commands = Vec::new();
|
let mut commands = Vec::new();
|
||||||
if let Some(popup) = self.popup.take() {
|
if let Some(popup) = self.popup.take() {
|
||||||
|
if popup.popup_type == PopupType::LauncherEditor {
|
||||||
|
self.launcher_edit = None;
|
||||||
|
}
|
||||||
commands.push(destroy_popup(popup.id));
|
commands.push(destroy_popup(popup.id));
|
||||||
}
|
}
|
||||||
if let Some(popup) = self.overflow_active_popup.take() {
|
if let Some(popup) = self.overflow_active_popup.take() {
|
||||||
|
|
@ -1068,6 +1411,198 @@ impl cosmic::Application for CosmicAppList {
|
||||||
return destroy_popup(popup_id);
|
return destroy_popup(popup_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::EditLauncher(id) => {
|
||||||
|
let Some(dock_item) = self.pinned_list.iter().find(|t| t.id == id).cloned() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(exec) = dock_item.desktop_info.exec() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(existing_popup) = self.popup.as_mut() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
let original_name = dock_item
|
||||||
|
.desktop_info
|
||||||
|
.desktop_entry("Name")
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.or_else(|| {
|
||||||
|
dock_item
|
||||||
|
.desktop_info
|
||||||
|
.name(&self.locales)
|
||||||
|
.map(Cow::into_owned)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| dock_item.original_app_id.clone());
|
||||||
|
let original_exec = exec.to_string();
|
||||||
|
let original_icon = dock_item
|
||||||
|
.desktop_info
|
||||||
|
.icon()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
let icon_theme = cosmic::icon_theme::default();
|
||||||
|
|
||||||
|
self.launcher_edit = Some(LauncherEditState {
|
||||||
|
original_app_id: dock_item.original_app_id.clone(),
|
||||||
|
source_path: dock_item.desktop_info.path.clone(),
|
||||||
|
original_name: original_name.clone(),
|
||||||
|
original_exec: original_exec.clone(),
|
||||||
|
name: original_name,
|
||||||
|
exec: original_exec,
|
||||||
|
icon: original_icon.clone(),
|
||||||
|
terminal: dock_item.desktop_info.terminal(),
|
||||||
|
saving: false,
|
||||||
|
error: None,
|
||||||
|
icon_catalog: IconCatalogState {
|
||||||
|
theme: icon_theme.clone(),
|
||||||
|
query: String::new(),
|
||||||
|
entries: Vec::new(),
|
||||||
|
loading: true,
|
||||||
|
truncated: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
existing_popup.dock_item = dock_item;
|
||||||
|
existing_popup.popup_type = PopupType::LauncherEditor;
|
||||||
|
|
||||||
|
return Task::perform(
|
||||||
|
icon_catalog::load_icon_catalog(icon_theme, original_icon),
|
||||||
|
|catalog| cosmic::Action::App(Message::LauncherIconCatalogLoaded(catalog)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Message::LauncherNameChanged(name) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut()
|
||||||
|
&& !edit.saving
|
||||||
|
{
|
||||||
|
edit.name = name;
|
||||||
|
edit.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::LauncherExecChanged(exec) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut()
|
||||||
|
&& !edit.saving
|
||||||
|
{
|
||||||
|
edit.exec = exec;
|
||||||
|
edit.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::LauncherIconChanged(icon) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut()
|
||||||
|
&& !edit.saving
|
||||||
|
{
|
||||||
|
edit.icon = icon;
|
||||||
|
edit.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::LauncherIconSearchChanged(query) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut() {
|
||||||
|
edit.icon_catalog.query = query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::LauncherIconSelected(icon) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut()
|
||||||
|
&& !edit.saving
|
||||||
|
{
|
||||||
|
edit.icon = icon;
|
||||||
|
edit.error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::ReloadLauncherIconCatalog => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut() {
|
||||||
|
edit.icon_catalog.theme = cosmic::icon_theme::default();
|
||||||
|
edit.icon_catalog.entries.clear();
|
||||||
|
edit.icon_catalog.loading = true;
|
||||||
|
edit.icon_catalog.truncated = false;
|
||||||
|
|
||||||
|
return Task::perform(
|
||||||
|
icon_catalog::load_icon_catalog(
|
||||||
|
edit.icon_catalog.theme.clone(),
|
||||||
|
edit.icon.clone(),
|
||||||
|
),
|
||||||
|
|catalog| cosmic::Action::App(Message::LauncherIconCatalogLoaded(catalog)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::LauncherIconCatalogLoaded(catalog) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut()
|
||||||
|
&& edit.icon_catalog.theme == catalog.theme
|
||||||
|
{
|
||||||
|
edit.icon_catalog.entries = catalog.entries;
|
||||||
|
edit.icon_catalog.loading = false;
|
||||||
|
edit.icon_catalog.truncated = catalog.truncated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::SaveLauncherEdit => {
|
||||||
|
let Some(edit) = self.launcher_edit.as_mut() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if edit.saving {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if let Err(error) =
|
||||||
|
launcher_edit::validate_launcher_fields(&edit.name, &edit.exec, &edit.icon)
|
||||||
|
{
|
||||||
|
edit.error = Some(error);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = LauncherEditRequest {
|
||||||
|
current_app_id: edit.original_app_id.clone(),
|
||||||
|
source_path: edit.source_path.clone(),
|
||||||
|
name: edit.name.clone(),
|
||||||
|
exec: edit.exec.clone(),
|
||||||
|
icon: edit.icon.clone(),
|
||||||
|
terminal: edit.terminal,
|
||||||
|
replace_localized_name: edit.name.trim() != edit.original_name.trim(),
|
||||||
|
disable_dbus_activation: edit.exec.trim() != edit.original_exec.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
edit.saving = true;
|
||||||
|
edit.error = None;
|
||||||
|
|
||||||
|
return Task::perform(launcher_edit::save_launcher_edit(request), |result| {
|
||||||
|
cosmic::Action::App(Message::LauncherEditSaved(result))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Message::CancelLauncherEdit => {
|
||||||
|
return self.close_popups();
|
||||||
|
}
|
||||||
|
Message::LauncherEditSaved(result) => match result {
|
||||||
|
Ok(result) => {
|
||||||
|
tracing::info!(
|
||||||
|
app_id = result.new_app_id,
|
||||||
|
path = ?result.path,
|
||||||
|
"saved editable launcher"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut favorites = self.config.favorites.clone();
|
||||||
|
let mut favorites_changed = false;
|
||||||
|
for favorite in &mut favorites {
|
||||||
|
if *favorite == result.old_app_id && *favorite != result.new_app_id {
|
||||||
|
*favorite = result.new_app_id.clone();
|
||||||
|
favorites_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if favorites_changed {
|
||||||
|
self.config.update_pinned(
|
||||||
|
favorites.clone(),
|
||||||
|
&Config::new(APP_ID, AppListConfig::VERSION).unwrap(),
|
||||||
|
);
|
||||||
|
self.config.favorites = favorites;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_desktop_entries();
|
||||||
|
self.sync_pinned_list_from_config();
|
||||||
|
self.launcher_edit = None;
|
||||||
|
return self.close_popups();
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if let Some(edit) = self.launcher_edit.as_mut() {
|
||||||
|
edit.saving = false;
|
||||||
|
edit.error = Some(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Message::Activate(handle) => {
|
Message::Activate(handle) => {
|
||||||
if let Some(tx) = self.wayland_sender.as_ref() {
|
if let Some(tx) = self.wayland_sender.as_ref() {
|
||||||
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
|
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
|
||||||
|
|
@ -1520,6 +2055,9 @@ impl cosmic::Application for CosmicAppList {
|
||||||
},
|
},
|
||||||
Message::ClosePopup => {
|
Message::ClosePopup => {
|
||||||
if let Some(p) = self.popup.take() {
|
if let Some(p) = self.popup.take() {
|
||||||
|
if p.popup_type == PopupType::LauncherEditor {
|
||||||
|
self.launcher_edit = None;
|
||||||
|
}
|
||||||
return destroy_popup(p.id);
|
return destroy_popup(p.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1534,42 +2072,16 @@ impl cosmic::Application for CosmicAppList {
|
||||||
}
|
}
|
||||||
Message::ConfigUpdated(config) => {
|
Message::ConfigUpdated(config) => {
|
||||||
self.config = config;
|
self.config = config;
|
||||||
// drain to active list
|
self.update_desktop_entries();
|
||||||
for item in self.pinned_list.drain(..) {
|
self.sync_pinned_list_from_config();
|
||||||
if !item.toplevels.is_empty() {
|
|
||||||
self.active_list.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pull back configured items into the favorites list
|
|
||||||
self.pinned_list =
|
|
||||||
find_desktop_entries(&self.desktop_entries, &self.config.favorites)
|
|
||||||
.zip(&self.config.favorites)
|
|
||||||
.map(|(de, original_id)| {
|
|
||||||
if let Some(p) = self
|
|
||||||
.active_list
|
|
||||||
.iter()
|
|
||||||
// match using heuristic id
|
|
||||||
.position(|dock_item| dock_item.desktop_info.id() == de.id())
|
|
||||||
{
|
|
||||||
let mut d = self.active_list.remove(p);
|
|
||||||
// but use the id from the config
|
|
||||||
d.original_app_id.clone_from(original_id);
|
|
||||||
d
|
|
||||||
} else {
|
|
||||||
self.item_ctr += 1;
|
|
||||||
DockItem {
|
|
||||||
id: self.item_ctr,
|
|
||||||
toplevels: Vec::new(),
|
|
||||||
desktop_info: de.clone(),
|
|
||||||
original_app_id: original_id.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
}
|
||||||
Message::CloseRequested(id) => {
|
Message::CloseRequested(id) => {
|
||||||
if Some(id) == self.popup.as_ref().map(|p| p.id) {
|
if let Some(popup) = &self.popup
|
||||||
|
&& popup.id == id
|
||||||
|
{
|
||||||
|
if popup.popup_type == PopupType::LauncherEditor {
|
||||||
|
self.launcher_edit = None;
|
||||||
|
}
|
||||||
self.popup = None;
|
self.popup = None;
|
||||||
}
|
}
|
||||||
if self.overflow_active_popup.is_some_and(|p| p == id) {
|
if self.overflow_active_popup.is_some_and(|p| p == id) {
|
||||||
|
|
@ -1582,6 +2094,52 @@ impl cosmic::Application for CosmicAppList {
|
||||||
Message::GpuRequest(gpus) => {
|
Message::GpuRequest(gpus) => {
|
||||||
self.gpus = gpus;
|
self.gpus = gpus;
|
||||||
}
|
}
|
||||||
|
Message::DockItemHover(id) => {
|
||||||
|
self.hovered_dock_item = id;
|
||||||
|
// Seed the animated center on the very first hover so
|
||||||
|
// the bell doesn't "fly in" from (0,0).
|
||||||
|
if self.anim_hover_center.is_none()
|
||||||
|
&& let Some(hovered_id) = self.hovered_dock_item.as_ref()
|
||||||
|
&& let Some(r) = self.rectangles.get(hovered_id)
|
||||||
|
{
|
||||||
|
self.anim_hover_center = Some((
|
||||||
|
r.x + r.width / 2.0,
|
||||||
|
r.y + r.height / 2.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::AnimTick(now) => {
|
||||||
|
// dt-based exponential smoothing: reach ~99% of the
|
||||||
|
// target in ~120ms at 60fps (about 7 ticks).
|
||||||
|
let dt = self
|
||||||
|
.anim_last_tick
|
||||||
|
.map(|prev| now.saturating_duration_since(prev).as_secs_f32())
|
||||||
|
.unwrap_or(0.016)
|
||||||
|
.min(0.1); // clamp so a long pause doesn't snap
|
||||||
|
self.anim_last_tick = Some(now);
|
||||||
|
|
||||||
|
// Intensity: target 1.0 when any icon is hovered, 0.0 else.
|
||||||
|
let intensity_target = if self.hovered_dock_item.is_some() { 1.0 } else { 0.0 };
|
||||||
|
let tau = 0.060_f32; // time-constant (s); smaller = snappier
|
||||||
|
let alpha = 1.0 - (-dt / tau).exp();
|
||||||
|
self.anim_hover_intensity += (intensity_target - self.anim_hover_intensity) * alpha;
|
||||||
|
|
||||||
|
// Hovered-center smoothing: chase the real rect's center.
|
||||||
|
if let Some(hovered_id) = self.hovered_dock_item.as_ref()
|
||||||
|
&& let Some(r) = self.rectangles.get(hovered_id)
|
||||||
|
{
|
||||||
|
let target = (r.x + r.width / 2.0, r.y + r.height / 2.0);
|
||||||
|
let current = self.anim_hover_center.unwrap_or(target);
|
||||||
|
self.anim_hover_center = Some((
|
||||||
|
current.0 + (target.0 - current.0) * alpha,
|
||||||
|
current.1 + (target.1 - current.1) * alpha,
|
||||||
|
));
|
||||||
|
} else if self.anim_hover_intensity < 0.01 {
|
||||||
|
// Nothing hovered + intensity faded out → forget the
|
||||||
|
// animated center so the next hover seeds fresh.
|
||||||
|
self.anim_hover_center = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::OpenActive => {
|
Message::OpenActive => {
|
||||||
let create_new = self.overflow_active_popup.is_none();
|
let create_new = self.overflow_active_popup.is_none();
|
||||||
let mut cmds = vec![self.close_popups()];
|
let mut cmds = vec![self.close_popups()];
|
||||||
|
|
@ -1763,7 +2321,9 @@ impl cosmic::Application for CosmicAppList {
|
||||||
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
|
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
|
||||||
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
|
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
|
||||||
|
|
||||||
self.core
|
let dock_id = dock_item.id;
|
||||||
|
let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id));
|
||||||
|
let tooltip = self.core
|
||||||
.applet
|
.applet
|
||||||
.applet_tooltip::<Message>(
|
.applet_tooltip::<Message>(
|
||||||
dock_item.as_icon(
|
dock_item.as_icon(
|
||||||
|
|
@ -1776,6 +2336,7 @@ impl cosmic::Application for CosmicAppList {
|
||||||
dot_radius,
|
dot_radius,
|
||||||
self.core.main_window_id().unwrap(),
|
self.core.main_window_id().unwrap(),
|
||||||
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
||||||
|
icon_scale,
|
||||||
),
|
),
|
||||||
dock_item
|
dock_item
|
||||||
.desktop_info
|
.desktop_info
|
||||||
|
|
@ -1785,7 +2346,10 @@ impl cosmic::Application for CosmicAppList {
|
||||||
self.popup.is_some(),
|
self.popup.is_some(),
|
||||||
Message::Surface,
|
Message::Surface,
|
||||||
None,
|
None,
|
||||||
)
|
);
|
||||||
|
cosmic::widget::mouse_area(tooltip)
|
||||||
|
.on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id))))
|
||||||
|
.on_exit(Message::DockItemHover(None))
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -1836,6 +2400,10 @@ impl cosmic::Application for CosmicAppList {
|
||||||
dot_radius,
|
dot_radius,
|
||||||
self.core.main_window_id().unwrap(),
|
self.core.main_window_id().unwrap(),
|
||||||
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
||||||
|
// Yoda: no magnification on DnD-preview icons — these
|
||||||
|
// float around as the user drags, so static 1.0 is
|
||||||
|
// less visually confusing.
|
||||||
|
1.0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else if self.is_listening_for_dnd && self.pinned_list.is_empty() {
|
} else if self.is_listening_for_dnd && self.pinned_list.is_empty() {
|
||||||
|
|
@ -1875,8 +2443,10 @@ impl cosmic::Application for CosmicAppList {
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
|
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
|
||||||
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
|
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
|
||||||
|
let dock_id = dock_item.id;
|
||||||
|
let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id));
|
||||||
|
|
||||||
self.core
|
let tooltip = self.core
|
||||||
.applet
|
.applet
|
||||||
.applet_tooltip(
|
.applet_tooltip(
|
||||||
dock_item.as_icon(
|
dock_item.as_icon(
|
||||||
|
|
@ -1889,6 +2459,7 @@ impl cosmic::Application for CosmicAppList {
|
||||||
dot_radius,
|
dot_radius,
|
||||||
self.core.main_window_id().unwrap(),
|
self.core.main_window_id().unwrap(),
|
||||||
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
||||||
|
icon_scale,
|
||||||
),
|
),
|
||||||
dock_item
|
dock_item
|
||||||
.desktop_info
|
.desktop_info
|
||||||
|
|
@ -1898,7 +2469,10 @@ impl cosmic::Application for CosmicAppList {
|
||||||
self.popup.is_some(),
|
self.popup.is_some(),
|
||||||
Message::Surface,
|
Message::Surface,
|
||||||
None,
|
None,
|
||||||
)
|
);
|
||||||
|
cosmic::widget::mouse_area(tooltip)
|
||||||
|
.on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id))))
|
||||||
|
.on_exit(Message::DockItemHover(None))
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -2179,6 +2753,19 @@ impl cosmic::Application for CosmicAppList {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if is_pinned && desktop_info.exec().is_some() {
|
||||||
|
content = content.push(
|
||||||
|
menu_button(
|
||||||
|
row![
|
||||||
|
icon::icon(from_name("edit-symbolic").into()).size(16),
|
||||||
|
text::body(fl!("edit-launcher"))
|
||||||
|
]
|
||||||
|
.spacing(8),
|
||||||
|
)
|
||||||
|
.on_press(Message::EditLauncher(*id)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if !toplevels.is_empty() {
|
if !toplevels.is_empty() {
|
||||||
content = content.push(divider::horizontal::light());
|
content = content.push(divider::horizontal::light());
|
||||||
content = match toplevels.len() {
|
content = match toplevels.len() {
|
||||||
|
|
@ -2225,6 +2812,84 @@ impl cosmic::Application for CosmicAppList {
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
PopupType::LauncherEditor => {
|
||||||
|
let Some(edit) = self.launcher_edit.as_ref() else {
|
||||||
|
return text::body("").into();
|
||||||
|
};
|
||||||
|
|
||||||
|
let spacing = theme::spacing();
|
||||||
|
let can_save = !edit.saving
|
||||||
|
&& launcher_edit::validate_launcher_fields(
|
||||||
|
&edit.name, &edit.exec, &edit.icon,
|
||||||
|
)
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
let mut form = column![
|
||||||
|
text::title4(fl!("edit-launcher")),
|
||||||
|
text_input("", edit.name.as_str())
|
||||||
|
.label(fl!("launcher-name"))
|
||||||
|
.on_input(Message::LauncherNameChanged)
|
||||||
|
.on_submit(|_| Message::SaveLauncherEdit)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(14),
|
||||||
|
text_input("", edit.exec.as_str())
|
||||||
|
.label(fl!("launcher-command"))
|
||||||
|
.on_input(Message::LauncherExecChanged)
|
||||||
|
.on_submit(|_| Message::SaveLauncherEdit)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.size(14),
|
||||||
|
launcher_icon_editor(edit),
|
||||||
|
]
|
||||||
|
.spacing(spacing.space_s)
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
if let Some(error) = edit.error.as_ref() {
|
||||||
|
form = form.push(text::caption(error.as_str()).class(
|
||||||
|
cosmic::theme::Text::Color(theme.cosmic().destructive_color().into()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancel =
|
||||||
|
button::custom(text::body(fl!("cancel")).center().width(Length::Fill))
|
||||||
|
.on_press_maybe(if edit.saving {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Message::CancelLauncherEdit)
|
||||||
|
})
|
||||||
|
.padding([spacing.space_xxs, spacing.space_s])
|
||||||
|
.width(142);
|
||||||
|
|
||||||
|
let save = button::custom(text::body(fl!("save")).center().width(Length::Fill))
|
||||||
|
.class(Button::Suggested)
|
||||||
|
.on_press_maybe(if can_save {
|
||||||
|
Some(Message::SaveLauncherEdit)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.padding([spacing.space_xxs, spacing.space_s])
|
||||||
|
.width(142);
|
||||||
|
|
||||||
|
let actions = row![horizontal_space(), cancel, save]
|
||||||
|
.spacing(spacing.space_xxs)
|
||||||
|
.align_y(Alignment::Center);
|
||||||
|
|
||||||
|
let content = column![form, actions]
|
||||||
|
.spacing(spacing.space_m)
|
||||||
|
.padding(spacing.space_m)
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
self.core
|
||||||
|
.applet
|
||||||
|
.popup_container(container(content).width(Length::Fill))
|
||||||
|
.limits(
|
||||||
|
Limits::NONE
|
||||||
|
.min_width(480.)
|
||||||
|
.min_height(1.)
|
||||||
|
.max_width(520.)
|
||||||
|
.max_height(1000.),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
PopupType::ToplevelList => match self.core.applet.anchor {
|
PopupType::ToplevelList => match self.core.applet.anchor {
|
||||||
PanelAnchor::Left | PanelAnchor::Right => {
|
PanelAnchor::Left | PanelAnchor::Right => {
|
||||||
let mut content =
|
let mut content =
|
||||||
|
|
@ -2313,6 +2978,10 @@ impl cosmic::Application for CosmicAppList {
|
||||||
dot_radius,
|
dot_radius,
|
||||||
id,
|
id,
|
||||||
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
||||||
|
// Yoda: icons in the overflow popup are
|
||||||
|
// already smaller-grid — keep them at 1.0
|
||||||
|
// so the popup doesn't reshuffle on hover.
|
||||||
|
1.0,
|
||||||
),
|
),
|
||||||
dock_item
|
dock_item
|
||||||
.desktop_info
|
.desktop_info
|
||||||
|
|
@ -2421,6 +3090,10 @@ impl cosmic::Application for CosmicAppList {
|
||||||
dot_radius,
|
dot_radius,
|
||||||
id,
|
id,
|
||||||
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
|
||||||
|
// Yoda: popup icons stay at 1.0 (hover
|
||||||
|
// magnification is applied to the main
|
||||||
|
// dock row only).
|
||||||
|
1.0,
|
||||||
),
|
),
|
||||||
dock_item
|
dock_item
|
||||||
.desktop_info
|
.desktop_info
|
||||||
|
|
@ -2479,7 +3152,21 @@ impl cosmic::Application for CosmicAppList {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
|
// Yoda: ~60fps animation ticks for the fisheye magnification.
|
||||||
|
// Only emitted when an animation is actually in progress (hover
|
||||||
|
// intensity >0 OR still fading out) — keeps the panel idle when
|
||||||
|
// the pointer is nowhere near the dock.
|
||||||
|
let anim_active = self.hovered_dock_item.is_some()
|
||||||
|
|| self.anim_hover_intensity > 0.001;
|
||||||
|
let anim_subscription = if anim_active {
|
||||||
|
cosmic::iced::time::every(std::time::Duration::from_millis(16))
|
||||||
|
.map(Message::AnimTick)
|
||||||
|
} else {
|
||||||
|
Subscription::none()
|
||||||
|
};
|
||||||
|
|
||||||
Subscription::batch([
|
Subscription::batch([
|
||||||
|
anim_subscription,
|
||||||
wayland_subscription().map(Message::Wayland),
|
wayland_subscription().map(Message::Wayland),
|
||||||
listen_with(|e, _, id| match e {
|
listen_with(|e, _, id| match e {
|
||||||
cosmic::iced::core::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
cosmic::iced::core::Event::PlatformSpecific(event::PlatformSpecific::Wayland(
|
||||||
|
|
|
||||||
440
cosmic-app-list/src/icon_catalog.rs
Normal file
440
cosmic-app-list/src/icon_catalog.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
|
fs,
|
||||||
|
path::{Component, Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
const FALLBACK_THEMES: &[&str] = &["Cosmic", "hicolor", "gnome", "Yaru"];
|
||||||
|
const MAX_THEME_CHAIN: usize = 24;
|
||||||
|
const MAX_SCAN_DEPTH: usize = 5;
|
||||||
|
const MAX_CATALOG_ENTRIES: usize = 2_500;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IconCatalog {
|
||||||
|
pub theme: String,
|
||||||
|
pub entries: Vec<IconCatalogEntry>,
|
||||||
|
pub truncated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IconCatalogEntry {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
struct CandidateRank {
|
||||||
|
preferred: u8,
|
||||||
|
category: u8,
|
||||||
|
symbolic: u8,
|
||||||
|
theme_depth: usize,
|
||||||
|
extension: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for CandidateRank {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
(
|
||||||
|
self.preferred,
|
||||||
|
self.category,
|
||||||
|
self.symbolic,
|
||||||
|
self.theme_depth,
|
||||||
|
self.extension,
|
||||||
|
)
|
||||||
|
.cmp(&(
|
||||||
|
other.preferred,
|
||||||
|
other.category,
|
||||||
|
other.symbolic,
|
||||||
|
other.theme_depth,
|
||||||
|
other.extension,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for CandidateRank {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Candidate {
|
||||||
|
name: String,
|
||||||
|
rank: CandidateRank,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_icon_catalog(theme: String, preferred_icon: String) -> IconCatalog {
|
||||||
|
build_icon_catalog(theme, preferred_icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_icon_catalog(theme: String, preferred_icon: String) -> IconCatalog {
|
||||||
|
let theme = if theme.trim().is_empty() {
|
||||||
|
"Cosmic".to_string()
|
||||||
|
} else {
|
||||||
|
theme
|
||||||
|
};
|
||||||
|
let preferred_name = icon_name_from_value(preferred_icon.trim());
|
||||||
|
let theme_chain = theme_chain(&theme);
|
||||||
|
let mut candidates = HashMap::new();
|
||||||
|
|
||||||
|
for (theme_depth, theme_name) in theme_chain.iter().enumerate() {
|
||||||
|
for theme_dir in theme_dirs(theme_name) {
|
||||||
|
scan_icon_tree(
|
||||||
|
&theme_dir,
|
||||||
|
theme_depth,
|
||||||
|
preferred_name.as_deref(),
|
||||||
|
&mut candidates,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for pixmap_dir in pixmap_dirs() {
|
||||||
|
scan_icon_tree(
|
||||||
|
&pixmap_dir,
|
||||||
|
theme_chain.len() + 1,
|
||||||
|
preferred_name.as_deref(),
|
||||||
|
&mut candidates,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = candidates.into_values().collect::<Vec<_>>();
|
||||||
|
entries.sort_by(|a, b| a.rank.cmp(&b.rank).then_with(|| a.name.cmp(&b.name)));
|
||||||
|
|
||||||
|
let truncated = entries.len() > MAX_CATALOG_ENTRIES;
|
||||||
|
entries.truncate(MAX_CATALOG_ENTRIES);
|
||||||
|
|
||||||
|
IconCatalog {
|
||||||
|
theme,
|
||||||
|
entries: entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|candidate| IconCatalogEntry {
|
||||||
|
name: candidate.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
truncated,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme_chain(theme: &str) -> Vec<String> {
|
||||||
|
let mut queue = VecDeque::from([theme.to_string()]);
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut chain = Vec::new();
|
||||||
|
|
||||||
|
while let Some(theme_name) = queue.pop_front() {
|
||||||
|
let key = theme_name.to_ascii_lowercase();
|
||||||
|
if !seen.insert(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.push(theme_name.clone());
|
||||||
|
if chain.len() >= MAX_THEME_CHAIN {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for parent in read_theme_inherits(&theme_name) {
|
||||||
|
queue.push_back(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for fallback in FALLBACK_THEMES {
|
||||||
|
if chain.len() >= MAX_THEME_CHAIN {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let key = fallback.to_ascii_lowercase();
|
||||||
|
if seen.insert(key) {
|
||||||
|
chain.push((*fallback).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_theme_inherits(theme: &str) -> Vec<String> {
|
||||||
|
theme_dirs(theme)
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|dir| fs::read_to_string(dir.join("index.theme")).ok())
|
||||||
|
.map(|contents| parse_inherits(&contents))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inherits(contents: &str) -> Vec<String> {
|
||||||
|
let mut in_icon_theme = false;
|
||||||
|
|
||||||
|
for raw_line in contents.lines() {
|
||||||
|
let line = raw_line.trim();
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
in_icon_theme = line == "[Icon Theme]";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_icon_theme && let Some(value) = line.strip_prefix("Inherits=") {
|
||||||
|
return value
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scan_icon_tree(
|
||||||
|
root: &Path,
|
||||||
|
theme_depth: usize,
|
||||||
|
preferred_name: Option<&str>,
|
||||||
|
candidates: &mut HashMap<String, Candidate>,
|
||||||
|
) {
|
||||||
|
let mut stack = vec![(root.to_path_buf(), 0usize)];
|
||||||
|
|
||||||
|
while let Some((dir, depth)) = stack.pop() {
|
||||||
|
if depth > MAX_SCAN_DEPTH {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(entries) = fs::read_dir(&dir) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in entries.filter_map(Result::ok) {
|
||||||
|
let path = entry.path();
|
||||||
|
let Ok(file_type) = entry.file_type() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
stack.push((path, depth + 1));
|
||||||
|
} else if (file_type.is_file() || file_type.is_symlink())
|
||||||
|
&& let Some(candidate) = candidate_from_path(&path, theme_depth, preferred_name)
|
||||||
|
{
|
||||||
|
insert_candidate(candidates, candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_candidate(candidates: &mut HashMap<String, Candidate>, candidate: Candidate) {
|
||||||
|
let key = candidate.name.to_ascii_lowercase();
|
||||||
|
match candidates.get_mut(&key) {
|
||||||
|
Some(existing) if candidate.rank < existing.rank => {
|
||||||
|
*existing = candidate;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
candidates.insert(key, candidate);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn candidate_from_path(
|
||||||
|
path: &Path,
|
||||||
|
theme_depth: usize,
|
||||||
|
preferred_name: Option<&str>,
|
||||||
|
) -> Option<Candidate> {
|
||||||
|
let extension = extension_rank(path)?;
|
||||||
|
let name = path.file_stem()?.to_str()?.trim();
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let symbolic = u8::from(name.ends_with("-symbolic") || path_has_component(path, "symbolic"));
|
||||||
|
let preferred = u8::from(preferred_name != Some(name));
|
||||||
|
|
||||||
|
Some(Candidate {
|
||||||
|
name: name.to_string(),
|
||||||
|
rank: CandidateRank {
|
||||||
|
preferred,
|
||||||
|
category: category_rank(path),
|
||||||
|
symbolic,
|
||||||
|
theme_depth,
|
||||||
|
extension,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extension_rank(path: &Path) -> Option<u8> {
|
||||||
|
match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
|
||||||
|
"svg" => Some(0),
|
||||||
|
"png" => Some(1),
|
||||||
|
"xpm" => Some(2),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn category_rank(path: &Path) -> u8 {
|
||||||
|
for component in path.components().filter_map(component_str) {
|
||||||
|
match component {
|
||||||
|
"apps" | "applications" => return 0,
|
||||||
|
"categories" => return 1,
|
||||||
|
"places" => return 2,
|
||||||
|
"devices" => return 3,
|
||||||
|
"mimetypes" => return 4,
|
||||||
|
"actions" => return 5,
|
||||||
|
"status" => return 6,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
7
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_has_component(path: &Path, needle: &str) -> bool {
|
||||||
|
path.components()
|
||||||
|
.filter_map(component_str)
|
||||||
|
.any(|component| component == needle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn component_str(component: Component<'_>) -> Option<&str> {
|
||||||
|
component.as_os_str().to_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_name_from_value(value: &str) -> Option<String> {
|
||||||
|
if value.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = Path::new(value);
|
||||||
|
if value.contains('/') {
|
||||||
|
return path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
}
|
||||||
|
|
||||||
|
path.file_stem()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.or_else(|| Some(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn theme_dirs(theme: &str) -> Vec<PathBuf> {
|
||||||
|
icon_base_dirs()
|
||||||
|
.into_iter()
|
||||||
|
.map(|base| base.join(theme))
|
||||||
|
.filter(|path| path.is_dir())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn icon_base_dirs() -> Vec<PathBuf> {
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
|
if let Some(home) = std::env::home_dir() {
|
||||||
|
push_existing_unique(&mut dirs, home.join(".icons"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data_home) = xdg_data_home() {
|
||||||
|
push_existing_unique(&mut dirs, data_home.join("icons"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for data_dir in xdg_data_dirs() {
|
||||||
|
push_existing_unique(&mut dirs, data_dir.join("icons"));
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixmap_dirs() -> Vec<PathBuf> {
|
||||||
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
|
if let Some(data_home) = xdg_data_home() {
|
||||||
|
push_existing_unique(&mut dirs, data_home.join("pixmaps"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for data_dir in xdg_data_dirs() {
|
||||||
|
push_existing_unique(&mut dirs, data_dir.join("pixmaps"));
|
||||||
|
}
|
||||||
|
|
||||||
|
push_existing_unique(&mut dirs, PathBuf::from("/usr/share/pixmaps"));
|
||||||
|
dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xdg_data_home() -> Option<PathBuf> {
|
||||||
|
std::env::var_os("XDG_DATA_HOME")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| std::env::home_dir().map(|home| home.join(".local/share")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xdg_data_dirs() -> Vec<PathBuf> {
|
||||||
|
std::env::var_os("XDG_DATA_DIRS")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(|value| std::env::split_paths(&value).collect())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
vec![
|
||||||
|
PathBuf::from("/usr/local/share"),
|
||||||
|
PathBuf::from("/usr/share"),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_existing_unique(dirs: &mut Vec<PathBuf>, path: PathBuf) {
|
||||||
|
if path.exists() && !dirs.iter().any(|existing| existing == &path) {
|
||||||
|
dirs.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_inherits_only_from_icon_theme_section() {
|
||||||
|
let contents = r#"
|
||||||
|
[Other]
|
||||||
|
Inherits=Wrong
|
||||||
|
|
||||||
|
[Icon Theme]
|
||||||
|
Name=Demo
|
||||||
|
Inherits=Cosmic, hicolor , Adwaita
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert_eq!(parse_inherits(contents), ["Cosmic", "hicolor", "Adwaita"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalizes_icon_names_from_plain_names_and_paths() {
|
||||||
|
assert_eq!(icon_name_from_value("firefox").as_deref(), Some("firefox"));
|
||||||
|
assert_eq!(
|
||||||
|
icon_name_from_value("/usr/share/icons/hicolor/scalable/apps/firefox.svg").as_deref(),
|
||||||
|
Some("firefox")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_the_best_duplicate_candidate() {
|
||||||
|
let mut candidates = HashMap::new();
|
||||||
|
insert_candidate(
|
||||||
|
&mut candidates,
|
||||||
|
Candidate {
|
||||||
|
name: "demo".to_string(),
|
||||||
|
rank: CandidateRank {
|
||||||
|
preferred: 1,
|
||||||
|
category: 7,
|
||||||
|
symbolic: 1,
|
||||||
|
theme_depth: 4,
|
||||||
|
extension: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
insert_candidate(
|
||||||
|
&mut candidates,
|
||||||
|
Candidate {
|
||||||
|
name: "demo".to_string(),
|
||||||
|
rank: CandidateRank {
|
||||||
|
preferred: 0,
|
||||||
|
category: 0,
|
||||||
|
symbolic: 0,
|
||||||
|
theme_depth: 0,
|
||||||
|
extension: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(candidates["demo"].rank.preferred, 0);
|
||||||
|
assert_eq!(candidates["demo"].rank.category, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
378
cosmic-app-list/src/launcher_edit.rs
Normal file
378
cosmic-app-list/src/launcher_edit.rs
Normal file
|
|
@ -0,0 +1,378 @@
|
||||||
|
// Copyright 2026 System76 <info@system76.com>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
ffi::OsString,
|
||||||
|
io::ErrorKind,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LauncherEditRequest {
|
||||||
|
pub current_app_id: String,
|
||||||
|
pub source_path: PathBuf,
|
||||||
|
pub name: String,
|
||||||
|
pub exec: String,
|
||||||
|
pub icon: String,
|
||||||
|
pub terminal: bool,
|
||||||
|
pub replace_localized_name: bool,
|
||||||
|
pub disable_dbus_activation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LauncherEditResult {
|
||||||
|
pub old_app_id: String,
|
||||||
|
pub new_app_id: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ValidLauncherEdit {
|
||||||
|
current_app_id: String,
|
||||||
|
source_path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
exec: String,
|
||||||
|
icon: String,
|
||||||
|
terminal: bool,
|
||||||
|
replace_localized_name: bool,
|
||||||
|
disable_dbus_activation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_launcher_fields(name: &str, exec: &str, icon: &str) -> Result<(), String> {
|
||||||
|
validate_required("Name", name)?;
|
||||||
|
validate_required("Command", exec)?;
|
||||||
|
validate_optional("Icon", icon)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_launcher_edit(
|
||||||
|
request: LauncherEditRequest,
|
||||||
|
) -> Result<LauncherEditResult, String> {
|
||||||
|
let request = validate_request(request)?;
|
||||||
|
let (new_app_id, target_path) =
|
||||||
|
target_launcher_path(&request.current_app_id, &request.source_path)?;
|
||||||
|
|
||||||
|
let source = read_source_desktop_entry(&request).await?;
|
||||||
|
let rendered = render_editable_desktop_entry(&source, &request);
|
||||||
|
|
||||||
|
let Some(parent) = target_path.parent() else {
|
||||||
|
return Err("Desktop file target has no parent directory".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Could not create applications directory: {err}"))?;
|
||||||
|
|
||||||
|
let tmp_path = temporary_path(&target_path)?;
|
||||||
|
fs::write(&tmp_path, rendered)
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Could not write temporary desktop file: {err}"))?;
|
||||||
|
|
||||||
|
if let Err(err) = fs::rename(&tmp_path, &target_path).await {
|
||||||
|
let _ = fs::remove_file(&tmp_path).await;
|
||||||
|
return Err(format!("Could not install desktop file: {err}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LauncherEditResult {
|
||||||
|
old_app_id: request.current_app_id,
|
||||||
|
new_app_id,
|
||||||
|
path: target_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_request(request: LauncherEditRequest) -> Result<ValidLauncherEdit, String> {
|
||||||
|
let name = validate_required("Name", &request.name)?;
|
||||||
|
let exec = validate_required("Command", &request.exec)?;
|
||||||
|
let icon = validate_optional("Icon", &request.icon)?;
|
||||||
|
|
||||||
|
Ok(ValidLauncherEdit {
|
||||||
|
current_app_id: request.current_app_id,
|
||||||
|
source_path: request.source_path,
|
||||||
|
name,
|
||||||
|
exec,
|
||||||
|
icon,
|
||||||
|
terminal: request.terminal,
|
||||||
|
replace_localized_name: request.replace_localized_name,
|
||||||
|
disable_dbus_activation: request.disable_dbus_activation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_required(label: &str, value: &str) -> Result<String, String> {
|
||||||
|
let value = validate_optional(label, value)?;
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(format!("{label} cannot be empty"));
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_optional(label: &str, value: &str) -> Result<String, String> {
|
||||||
|
if value.contains('\0') || value.contains('\n') || value.contains('\r') {
|
||||||
|
return Err(format!("{label} cannot contain line breaks"));
|
||||||
|
}
|
||||||
|
Ok(value.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_source_desktop_entry(request: &ValidLauncherEdit) -> Result<String, String> {
|
||||||
|
if request.source_path.as_os_str().is_empty() {
|
||||||
|
return Ok(minimal_desktop_entry());
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::read_to_string(&request.source_path).await {
|
||||||
|
Ok(source) => Ok(source),
|
||||||
|
Err(err) if err.kind() == ErrorKind::NotFound => Ok(minimal_desktop_entry()),
|
||||||
|
Err(err) => Err(format!("Could not read source desktop file: {err}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minimal_desktop_entry() -> String {
|
||||||
|
"[Desktop Entry]\nType=Application\n".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_applications_dir() -> Result<PathBuf, String> {
|
||||||
|
if let Some(xdg_data_home) = env::var_os("XDG_DATA_HOME").filter(|value| !value.is_empty()) {
|
||||||
|
return Ok(PathBuf::from(xdg_data_home).join("applications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(home) = env::var_os("HOME").filter(|value| !value.is_empty()) else {
|
||||||
|
return Err("Neither XDG_DATA_HOME nor HOME is set".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PathBuf::from(home).join(".local/share/applications"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_launcher_path(app_id: &str, source_path: &Path) -> Result<(String, PathBuf), String> {
|
||||||
|
let applications_dir = user_applications_dir()?;
|
||||||
|
if source_path.starts_with(&applications_dir) {
|
||||||
|
return Ok((app_id.to_string(), source_path.to_path_buf()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let desktop_id = normalize_desktop_id(app_id)?;
|
||||||
|
Ok((
|
||||||
|
desktop_id.clone(),
|
||||||
|
applications_dir.join(format!("{desktop_id}.desktop")),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_desktop_id(app_id: &str) -> Result<String, String> {
|
||||||
|
if app_id.contains('\0') || app_id.contains('\n') || app_id.contains('\r') {
|
||||||
|
return Err("Desktop ID cannot contain line breaks".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let desktop_id = app_id
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c == '/' { '-' } else { c })
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
if desktop_id.trim().is_empty() {
|
||||||
|
return Err("Desktop ID cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(desktop_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temporary_path(target_path: &Path) -> Result<PathBuf, String> {
|
||||||
|
let Some(file_name) = target_path.file_name() else {
|
||||||
|
return Err("Desktop file target has no filename".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut tmp_file_name = OsString::from(".");
|
||||||
|
tmp_file_name.push(file_name);
|
||||||
|
tmp_file_name.push(format!(".tmp-{}", std::process::id()));
|
||||||
|
|
||||||
|
Ok(target_path.with_file_name(tmp_file_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_editable_desktop_entry(source: &str, request: &ValidLauncherEdit) -> String {
|
||||||
|
let mut updates = vec![
|
||||||
|
("Type", "Application".to_string()),
|
||||||
|
("Name", request.name.clone()),
|
||||||
|
("Exec", request.exec.clone()),
|
||||||
|
("Icon", request.icon.clone()),
|
||||||
|
(
|
||||||
|
"Terminal",
|
||||||
|
if request.terminal { "true" } else { "false" }.to_string(),
|
||||||
|
),
|
||||||
|
("X-COSMIC-UserEditable", "true".to_string()),
|
||||||
|
("X-COSMIC-SourceDesktopId", request.current_app_id.clone()),
|
||||||
|
];
|
||||||
|
|
||||||
|
if !request.source_path.as_os_str().is_empty() {
|
||||||
|
updates.push((
|
||||||
|
"X-COSMIC-SourceDesktopPath",
|
||||||
|
request.source_path.to_string_lossy().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.disable_dbus_activation {
|
||||||
|
updates.push(("DBusActivatable", "false".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
set_desktop_entry_keys(
|
||||||
|
source,
|
||||||
|
&updates,
|
||||||
|
request.replace_localized_name,
|
||||||
|
request.disable_dbus_activation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_desktop_entry_keys(
|
||||||
|
source: &str,
|
||||||
|
updates: &[(&str, String)],
|
||||||
|
replace_localized_name: bool,
|
||||||
|
remove_try_exec: bool,
|
||||||
|
) -> String {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut seen = vec![false; updates.len()];
|
||||||
|
let mut in_desktop_entry = false;
|
||||||
|
let mut saw_desktop_entry = false;
|
||||||
|
|
||||||
|
for line in source.lines() {
|
||||||
|
if let Some(section) = section_name(line) {
|
||||||
|
if in_desktop_entry {
|
||||||
|
insert_missing_keys(&mut out, updates, &seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
in_desktop_entry = section == "Desktop Entry";
|
||||||
|
saw_desktop_entry |= in_desktop_entry;
|
||||||
|
if in_desktop_entry {
|
||||||
|
seen.fill(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(line.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_desktop_entry && let Some(key) = desktop_entry_key(line) {
|
||||||
|
if replace_localized_name && key.starts_with("Name[") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if remove_try_exec && key == "TryExec" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(index) = updates
|
||||||
|
.iter()
|
||||||
|
.position(|(update_key, _)| *update_key == key)
|
||||||
|
{
|
||||||
|
out.push(format!("{}={}", updates[index].0, updates[index].1));
|
||||||
|
seen[index] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(line.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_desktop_entry {
|
||||||
|
insert_missing_keys(&mut out, updates, &seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !saw_desktop_entry {
|
||||||
|
let mut with_desktop_entry = Vec::with_capacity(out.len() + updates.len() + 2);
|
||||||
|
with_desktop_entry.push("[Desktop Entry]".to_string());
|
||||||
|
insert_missing_keys(
|
||||||
|
&mut with_desktop_entry,
|
||||||
|
updates,
|
||||||
|
&vec![false; updates.len()],
|
||||||
|
);
|
||||||
|
if !out.is_empty() {
|
||||||
|
with_desktop_entry.push(String::new());
|
||||||
|
with_desktop_entry.extend(out);
|
||||||
|
}
|
||||||
|
out = with_desktop_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rendered = out.join("\n");
|
||||||
|
rendered.push('\n');
|
||||||
|
rendered
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_missing_keys(out: &mut Vec<String>, updates: &[(&str, String)], seen: &[bool]) {
|
||||||
|
for (index, (key, value)) in updates.iter().enumerate() {
|
||||||
|
if !seen[index] {
|
||||||
|
out.push(format!("{key}={value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_name(line: &str) -> Option<&str> {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
trimmed
|
||||||
|
.strip_prefix('[')
|
||||||
|
.and_then(|value| value.strip_suffix(']'))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn desktop_entry_key(line: &str) -> Option<&str> {
|
||||||
|
let line = line.trim_start();
|
||||||
|
if line.starts_with('#') || line.starts_with(';') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
line.split_once('=').map(|(key, _)| key.trim_end())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn request() -> ValidLauncherEdit {
|
||||||
|
ValidLauncherEdit {
|
||||||
|
current_app_id: "org.example.App".to_string(),
|
||||||
|
source_path: PathBuf::from("/usr/share/applications/org.example.App.desktop"),
|
||||||
|
name: "Example".to_string(),
|
||||||
|
exec: "example --new".to_string(),
|
||||||
|
icon: "example-custom".to_string(),
|
||||||
|
terminal: false,
|
||||||
|
replace_localized_name: true,
|
||||||
|
disable_dbus_activation: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn updates_desktop_entry_without_dropping_action_groups() {
|
||||||
|
let source = "\
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Old
|
||||||
|
Name[fr]=Ancien
|
||||||
|
Exec=old
|
||||||
|
TryExec=old
|
||||||
|
Icon=old
|
||||||
|
DBusActivatable=true
|
||||||
|
Actions=new-window;
|
||||||
|
|
||||||
|
[Desktop Action new-window]
|
||||||
|
Name=New Window
|
||||||
|
Exec=old --new-window
|
||||||
|
";
|
||||||
|
|
||||||
|
let rendered = render_editable_desktop_entry(source, &request());
|
||||||
|
|
||||||
|
assert!(rendered.contains("Name=Example\n"));
|
||||||
|
assert!(!rendered.contains("Name[fr]="));
|
||||||
|
assert!(rendered.contains("Exec=example --new\n"));
|
||||||
|
assert!(rendered.contains("Icon=example-custom\n"));
|
||||||
|
assert!(rendered.contains("DBusActivatable=false\n"));
|
||||||
|
assert!(!rendered.contains("TryExec="));
|
||||||
|
assert!(rendered.contains("[Desktop Action new-window]\n"));
|
||||||
|
assert!(rendered.contains("Exec=old --new-window\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_localized_names_when_name_is_unchanged() {
|
||||||
|
let mut edit = request();
|
||||||
|
edit.replace_localized_name = false;
|
||||||
|
|
||||||
|
let rendered = render_editable_desktop_entry(
|
||||||
|
"[Desktop Entry]\nName=Example\nName[fr]=Exemple\nExec=old\n",
|
||||||
|
&edit,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(rendered.contains("Name=Example\n"));
|
||||||
|
assert!(rendered.contains("Name[fr]=Exemple\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod icon_catalog;
|
||||||
|
mod launcher_edit;
|
||||||
mod localize;
|
mod localize;
|
||||||
mod wayland_handler;
|
mod wayland_handler;
|
||||||
mod wayland_subscription;
|
mod wayland_subscription;
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,10 @@ impl CaptureData {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.conn.flush().unwrap();
|
if let Err(err) = self.conn.flush() {
|
||||||
|
tracing::error!("Wayland flush failed during screencopy session create: {err}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let formats = session
|
let formats = session
|
||||||
.wait_while(|data| data.formats.is_none())
|
.wait_while(|data| data.formats.is_none())
|
||||||
|
|
@ -437,7 +440,10 @@ impl CaptureData {
|
||||||
session: capture_session.clone(),
|
session: capture_session.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.conn.flush().unwrap();
|
if let Err(err) = self.conn.flush() {
|
||||||
|
tracing::error!("Wayland flush failed during screencopy capture: {err}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: wait for server to release buffer?
|
// TODO: wait for server to release buffer?
|
||||||
let res = session
|
let res = session
|
||||||
|
|
@ -709,7 +715,10 @@ pub(crate) fn wayland_handler(
|
||||||
if app_data.exit {
|
if app_data.exit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
event_loop.dispatch(None, &mut app_data).unwrap();
|
if let Err(err) = event_loop.dispatch(None, &mut app_data) {
|
||||||
|
tracing::error!("Wayland event loop terminated: {err}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ cctk.workspace = true
|
||||||
cosmic-protocols.workspace = true
|
cosmic-protocols.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
|
|
@ -17,9 +17,7 @@ tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
||||||
[dependencies.cosmic-settings-a11y-manager-subscription]
|
[dependencies.cosmic-settings-a11y-manager-subscription]
|
||||||
git = "https://github.com/pop-os/cosmic-settings"
|
path = "../../cosmic-settings/subscriptions/a11y-manager"
|
||||||
# path = "../../cosmic-settings/subscriptions/a11y-manager"
|
|
||||||
|
|
||||||
[dependencies.cosmic-settings-accessibility-subscription]
|
[dependencies.cosmic-settings-accessibility-subscription]
|
||||||
git = "https://github.com/pop-os/cosmic-settings"
|
path = "../../cosmic-settings/subscriptions/accessibility"
|
||||||
# path = "../../cosmic-settings/subscriptions/accessibility"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
screen-reader = Ανάγνωση οθόνης
|
||||||
|
invert-colors = Αναστροφή χρωμάτων
|
||||||
|
high-contrast = Υψηλή αντίθεση
|
||||||
|
settings = Ρυθμίσεις προσβασιμότητας...
|
||||||
|
magnifier = Μεγεθυντικός φακός
|
||||||
|
filter-colors = Φίλτρο χρωμάτων
|
||||||
0
cosmic-applet-a11y/i18n/lo/cosmic_applet_a11y.ftl
Normal file
0
cosmic-applet-a11y/i18n/lo/cosmic_applet_a11y.ftl
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
screen-reader = Читач екрана
|
||||||
|
invert-colors = Обрни боје
|
||||||
|
high-contrast = Високи контраст
|
||||||
|
filter-colors = Филтрирај боје
|
||||||
|
settings = Подешавања приступачности...
|
||||||
|
magnifier = Увеличавач
|
||||||
|
|
@ -26,10 +26,9 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use cosmic_settings_a11y_manager_subscription::{
|
use cosmic_settings_a11y_manager_subscription::{
|
||||||
self as cosmic_a11y_manager, AccessibilityEvent, AccessibilityRequest, ColorFilter,
|
AccessibilityEvent, AccessibilityRequest, ColorFilter,
|
||||||
};
|
};
|
||||||
use cosmic_settings_accessibility_subscription::{self as accessibility};
|
use cosmic_settings_accessibility_subscription::{self as accessibility};
|
||||||
use std::sync::LazyLock;
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
pub fn run() -> cosmic::iced::Result {
|
pub fn run() -> cosmic::iced::Result {
|
||||||
|
|
@ -52,6 +51,7 @@ struct CosmicA11yApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use anyhow;
|
use anyhow;
|
||||||
use cctk::sctk::reexports::calloop::{self, channel::SyncSender};
|
use cctk::sctk::reexports::calloop::{self};
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
self, Subscription,
|
self, Subscription,
|
||||||
futures::{self, SinkExt, StreamExt, channel::mpsc},
|
futures::{self, SinkExt},
|
||||||
stream,
|
stream,
|
||||||
};
|
};
|
||||||
use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1::Filter;
|
|
||||||
use cosmic_settings_a11y_manager_subscription::{
|
use cosmic_settings_a11y_manager_subscription::{
|
||||||
self as thread, AccessibilityEvent, AccessibilityRequest,
|
self as thread, AccessibilityEvent, AccessibilityRequest,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,8 @@ license = "GPL-3.0-only"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
mpris2-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
|
||||||
# mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
|
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
@ -21,5 +20,4 @@ urlencoding = "2.1.3"
|
||||||
zbus.workspace = true
|
zbus.workspace = true
|
||||||
|
|
||||||
[dependencies.cosmic-settings-sound-subscription]
|
[dependencies.cosmic-settings-sound-subscription]
|
||||||
git = "https://github.com/pop-os/cosmic-settings"
|
path = "../../cosmic-settings/subscriptions/sound"
|
||||||
# path = "../../cosmic-settings/subscriptions/sound"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
output = Έξοδος
|
||||||
|
show-media-controls = Εμφάνιση στοιχείων ελέγχου πολυμέσων στη γραμμή συστήματος
|
||||||
|
disconnected = Το PulseAudio αποσυνδέθηκε
|
||||||
|
no-device = Καμία επιλεγμένη συσκευή
|
||||||
|
input = Είσοδος
|
||||||
|
unknown-artist = Άγνωστος
|
||||||
|
sound-settings = Ρυθμίσεις ήχου...
|
||||||
|
|
@ -2,6 +2,6 @@ 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 csatlakozva
|
disconnected = A PulseAudio-kapcsolat megszakadt
|
||||||
no-device = Nincs kiválasztott eszköz
|
no-device = Nincs eszköz kiválasztva
|
||||||
unknown-artist = Ismeretlen
|
unknown-artist = Ismeretlen
|
||||||
|
|
|
||||||
0
cosmic-applet-audio/i18n/lo/cosmic_applet_audio.ftl
Normal file
0
cosmic-applet-audio/i18n/lo/cosmic_applet_audio.ftl
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
show-media-controls = Прикажи управљања медијима на траци
|
||||||
|
disconnected = Пулс-аудио откачен
|
||||||
|
no-device = Ниједан уређај није изабран
|
||||||
|
input = Улаз
|
||||||
|
output = Излаз
|
||||||
|
unknown-artist = Непознато
|
||||||
|
sound-settings = Подешавања звука...
|
||||||
|
|
@ -487,11 +487,31 @@ impl cosmic::Application for Audio {
|
||||||
.icon_button(self.output_icon_name())
|
.icon_button(self.output_icon_name())
|
||||||
.on_press_down(Message::TogglePopup);
|
.on_press_down(Message::TogglePopup);
|
||||||
|
|
||||||
const WHEEL_STEP: f32 = 5.0; // 5% per wheel event
|
const WHEEL_STEP: f32 = 5.0; // 5% par cran logique
|
||||||
|
// Wayland axis_v120 envoie un cran physique en rafale de plusieurs
|
||||||
|
// ScrollDelta::Pixels (5–8 events ~15–20px), pour ~120px par cran. On
|
||||||
|
// accumule ces sub-events dans un thread_local et on n'émet qu'au
|
||||||
|
// passage d'un seuil — sinon `signum()` faisait croire que chaque
|
||||||
|
// sub-event = un cran, et un seul cran physique faisait chuter le
|
||||||
|
// volume jusqu'à 0 ("coupe le son").
|
||||||
|
const PIXEL_THRESHOLD: f32 = 15.0; // px par cran logique
|
||||||
|
std::thread_local! {
|
||||||
|
static PIXEL_ACC: std::cell::Cell<f32> = const { std::cell::Cell::new(0.0) };
|
||||||
|
}
|
||||||
let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
|
let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
|
||||||
let scroll_vector = match delta {
|
let scroll_vector = match delta {
|
||||||
iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1
|
iced::mouse::ScrollDelta::Lines { y, .. } => {
|
||||||
iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1
|
PIXEL_ACC.with(|a| a.set(0.0));
|
||||||
|
y * WHEEL_STEP
|
||||||
|
}
|
||||||
|
iced::mouse::ScrollDelta::Pixels { y, .. } => {
|
||||||
|
PIXEL_ACC.with(|acc_cell| {
|
||||||
|
let acc = acc_cell.get() + y;
|
||||||
|
let steps = (acc / PIXEL_THRESHOLD).trunc();
|
||||||
|
acc_cell.set(acc - steps * PIXEL_THRESHOLD);
|
||||||
|
steps * WHEEL_STEP
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if scroll_vector == 0.0 {
|
if scroll_vector == 0.0 {
|
||||||
return Message::Ignore;
|
return Message::Ignore;
|
||||||
|
|
@ -499,7 +519,7 @@ impl cosmic::Application for Audio {
|
||||||
|
|
||||||
let new_volume = (self.model.sink_volume as f64 + (scroll_vector as f64))
|
let new_volume = (self.model.sink_volume as f64 + (scroll_vector as f64))
|
||||||
.clamp(0.0, self.max_sink_volume as f64);
|
.clamp(0.0, self.max_sink_volume as f64);
|
||||||
Message::SetSinkVolume(new_volume as u32)
|
Message::SetSinkVolume(new_volume.round() as u32)
|
||||||
});
|
});
|
||||||
|
|
||||||
let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
|
let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use cosmic::iced::core::Point;
|
||||||
|
|
||||||
use cosmic::iced::core::{
|
use cosmic::iced::core::{
|
||||||
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
|
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
|
||||||
event::{self, Event},
|
event::Event,
|
||||||
layout, mouse, overlay, renderer, touch,
|
layout, mouse, overlay, renderer, touch,
|
||||||
widget::{Operation, Tree, tree},
|
widget::{Operation, Tree, tree},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ drm = "0.14.1"
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
@ -24,9 +24,7 @@ zbus.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
[dependencies.cosmic-settings-upower-subscription]
|
[dependencies.cosmic-settings-upower-subscription]
|
||||||
git = "https://github.com/pop-os/cosmic-settings"
|
path = "../../cosmic-settings/subscriptions/upower"
|
||||||
# path = "../../cosmic-settings/subscriptions/upower"
|
|
||||||
|
|
||||||
[dependencies.cosmic-settings-daemon-subscription]
|
[dependencies.cosmic-settings-daemon-subscription]
|
||||||
git = "https://github.com/pop-os/cosmic-settings"
|
path = "../../cosmic-settings/subscriptions/settings-daemon"
|
||||||
# path = "../../cosmic-settings/subscriptions/settings-daemon"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
battery = Μπαταρία
|
||||||
|
power-settings = Ρυθμίσεις ενέργειας και μπαταρίας...
|
||||||
|
hours = ώ
|
||||||
|
until-empty = μέχρι την αποφόρτιση
|
||||||
|
battery-desc = Μειωμένη χρήση ενέργειας και επιδόσεις.
|
||||||
|
max-charge = Επεκτείνετε τη διάρκεια ζωής της μπαταρίας σας ορίζοντας ως μέγιστη τιμή φόρτισης το 80%
|
||||||
|
balanced = Ισορροπημένη
|
||||||
|
seconds = δ
|
||||||
|
dgpu-running = Η ανεξάρτητη GPU είναι ενεργή και μπορεί να μειώσει τη διάρκεια ζωής της μπαταρίας
|
||||||
|
performance = Υψηλές επιδόσεις
|
||||||
|
performance-desc = Υψηλές επιδόσεις και χρήση ενέργειας.
|
||||||
|
minutes = λ
|
||||||
|
balanced-desc = Τυπικές επιδόσεις και χρήση μπαταρίας.
|
||||||
|
dgpu-applications = Εφαρμογές που χρησιμοποιούν την ανεξάρτητη GPU { $gpu_name }
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
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ítsd a maximális töltési szintet 80%-ra
|
||||||
seconds = másodperc
|
seconds = mp
|
||||||
minutes = perc
|
minutes = p
|
||||||
hours = óra
|
hours = ó
|
||||||
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
|
||||||
|
|
|
||||||
0
cosmic-applet-battery/i18n/lo/cosmic_applet_battery.ftl
Normal file
0
cosmic-applet-battery/i18n/lo/cosmic_applet_battery.ftl
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
power-settings = Подешавања напајања и батерије...
|
||||||
|
hours = ч
|
||||||
|
until-empty = до празне
|
||||||
|
battery-desc = Смањен учинак и употреба струје.
|
||||||
|
balanced = Уравнотежено
|
||||||
|
battery = Батерија
|
||||||
|
seconds = с
|
||||||
|
performance = Најбрже
|
||||||
|
performance-desc = Висок учинак и употреба струје.
|
||||||
|
minutes = м
|
||||||
|
balanced-desc = Уобичајени учинак и употреба батерије.
|
||||||
|
max-charge = Повећајте век трајања батерије подешавањем највише вредности пуњења на 80%
|
||||||
|
dgpu-running = Засебна графичка је покренута и може умањити век трајања батерије
|
||||||
|
dgpu-applications = Програми који користе засебну графичку { $gpu_name }
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
battery = 省電
|
battery = 電池
|
||||||
battery-desc = 降低效能與耗電量。
|
battery-desc = 降低效能與耗電量。
|
||||||
balanced = 平衡
|
balanced = 平衡
|
||||||
balanced-desc = 標準效能與耗電量。
|
balanced-desc = 標準效能與耗電量。
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ use cosmic_settings_upower_subscription::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{path::PathBuf, sync::LazyLock, time::Duration};
|
use std::{path::PathBuf, time::Duration};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
// XXX improve
|
// XXX improve
|
||||||
|
|
@ -172,6 +172,7 @@ impl CosmicBatteryApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
@ -368,6 +369,10 @@ impl cosmic::Application for CosmicBatteryApplet {
|
||||||
let _ = tx.send(());
|
let _ = tx.send(());
|
||||||
}
|
}
|
||||||
let mut tasks = vec![get_popup(popup_settings)];
|
let mut tasks = vec![get_popup(popup_settings)];
|
||||||
|
if let Some(tx) = &self.settings_daemon_sender {
|
||||||
|
let _ = tx.send(settings_daemon::Request::GetDisplayBrightness);
|
||||||
|
let _ = tx.send(settings_daemon::Request::GetMaxDisplayBrightness);
|
||||||
|
}
|
||||||
// Try again every time a popup is opened
|
// Try again every time a popup is opened
|
||||||
if self.charging_limit.is_none() {
|
if self.charging_limit.is_none() {
|
||||||
tasks.push(Task::perform(get_charging_limit(), |limit| {
|
tasks.push(Task::perform(get_charging_limit(), |limit| {
|
||||||
|
|
@ -507,7 +512,7 @@ impl cosmic::Application for CosmicBatteryApplet {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self) -> Element<Message> {
|
fn view(&self) -> Element<'_, Message> {
|
||||||
let is_horizontal = match self.core.applet.anchor {
|
let is_horizontal = match self.core.applet.anchor {
|
||||||
PanelAnchor::Top | PanelAnchor::Bottom => true,
|
PanelAnchor::Top | PanelAnchor::Bottom => true,
|
||||||
PanelAnchor::Left | PanelAnchor::Right => false,
|
PanelAnchor::Left | PanelAnchor::Right => false,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ bluer = { version = "0.17", features = ["bluetoothd", "id"] }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
fastrand = "2.3.0"
|
fastrand = "2.3.0"
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
confirm-pin = Si us plau, verifiqueu que el següent PIN coincideix amb el mostrat a { $deviceName }
|
||||||
|
bluetooth = Bluetooth
|
||||||
|
unsuccessful = Error d'Emparellament
|
||||||
|
pairable = Emparellable
|
||||||
|
try-again = Reintentar
|
||||||
|
check-device = Si us plau, verifiqueu que { $deviceName } estigui encès, a l'abast i llest per emparellar-se.
|
||||||
|
other-devices = Altres dispositius Bluetooth
|
||||||
|
cancel = Cancel·lar
|
||||||
|
connected = Connectat
|
||||||
|
confirm = Confirmar
|
||||||
|
settings = Configuració del Bluetooth...
|
||||||
|
discoverable = Descobrible
|
||||||
|
|
@ -1,2 +1,12 @@
|
||||||
cancel = Ακύρωση
|
cancel = Ακύρωση
|
||||||
confirm = Επιβεβαίωση
|
confirm = Επιβεβαίωση
|
||||||
|
bluetooth = Bluetooth
|
||||||
|
confirm-pin = Επιβεβαιώστε ότι το ακόλουθο PIN είναι ίδιο με αυτό που εμφανίζεται στο { $deviceName }
|
||||||
|
unsuccessful = Ανεπιτυχής σύζευξη
|
||||||
|
pairable = Συνδέσιμο
|
||||||
|
try-again = Δοκιμή ξανά
|
||||||
|
check-device = Βεβαιωθείτε ότι το { $deviceName } είναι ενεργοποιημένο, βρίσκεται εντός εμβέλειας και είναι έτοιμο για σύζευξη.
|
||||||
|
other-devices = Άλλες συσκευές Bluetooth
|
||||||
|
connected = Συνδέθηκε
|
||||||
|
settings = Ρυθμίσεις Bluetooth...
|
||||||
|
discoverable = Ορατό
|
||||||
|
|
|
||||||
|
|
@ -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 = Kapcsolódva
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
confirm-pin = Потврдите да се овај ПИН подудара са оним приказаним на { $deviceName }
|
||||||
|
bluetooth = Блутут
|
||||||
|
unsuccessful = Неуспешно упаривање
|
||||||
|
pairable = Упарив
|
||||||
|
try-again = Покушај поново
|
||||||
|
check-device = Постарајте се да је { $deviceName } укључен, у домету и спреман за упаривање.
|
||||||
|
other-devices = Други блутут уређаји
|
||||||
|
cancel = Откажи
|
||||||
|
connected = Повезано
|
||||||
|
confirm = Потврди
|
||||||
|
settings = Подешавања блутута...
|
||||||
|
discoverable = Откривљив
|
||||||
|
|
@ -24,7 +24,7 @@ use cosmic::{
|
||||||
widget::{button, divider, icon, scrollable, text},
|
widget::{button, divider, icon, scrollable, text},
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use std::{collections::HashMap, sync::LazyLock, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -63,6 +63,7 @@ impl CosmicBluetoothApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
@ -145,7 +146,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
||||||
}
|
}
|
||||||
Message::BluetoothEvent(e) => match e {
|
Message::BluetoothEvent(e) => match e {
|
||||||
BluerEvent::RequestResponse {
|
BluerEvent::RequestResponse {
|
||||||
req,
|
req: _,
|
||||||
state,
|
state,
|
||||||
err_msg,
|
err_msg,
|
||||||
} => {
|
} => {
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,6 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
|
||||||
},
|
},
|
||||||
|
|
||||||
BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
|
BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
|
||||||
|
|
||||||
_ => return,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_ = output.send(message).await;
|
_ = output.send(message).await;
|
||||||
|
|
@ -199,6 +197,7 @@ pub enum BluerRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum BluerEvent {
|
pub enum BluerEvent {
|
||||||
RequestResponse {
|
RequestResponse {
|
||||||
req: BluerRequest,
|
req: BluerRequest,
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
|
cosmic-comp-config.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
show-keyboard-layout = Εμφάνιση διάταξης πληκτρολογίου...
|
||||||
|
keyboard-settings = Ρυθμίσεις πληκτρολογίου...
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
show-keyboard-layout = Прикажи распоред тастатуре...
|
||||||
|
keyboard-settings = Подешавања тастатуре...
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
mod localize;
|
mod localize;
|
||||||
|
|
||||||
use cosmic::iced::{Alignment, Length};
|
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
app,
|
app,
|
||||||
app::Core,
|
app::Core,
|
||||||
|
|
@ -14,16 +13,14 @@ use cosmic::{
|
||||||
iced::{
|
iced::{
|
||||||
Rectangle, Task,
|
Rectangle, Task,
|
||||||
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
||||||
widget::{column, row},
|
|
||||||
window::Id,
|
window::Id,
|
||||||
},
|
},
|
||||||
iced::{core::window, runtime::Appearance},
|
iced::core::window,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
surface, theme,
|
surface, theme,
|
||||||
widget::{
|
widget::{
|
||||||
self, autosize,
|
self, autosize,
|
||||||
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
|
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
|
||||||
space,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use cosmic_comp_config::CosmicCompConfig;
|
use cosmic_comp_config::CosmicCompConfig;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ anyhow.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
image = { version = "0.25.9", default-features = false }
|
image = { version = "0.25.9", default-features = false }
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
memmap2 = "0.9.10"
|
memmap2 = "0.9.10"
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
rustix.workspace = true
|
rustix.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ impl cosmic::Application for Minimize {
|
||||||
} else {
|
} else {
|
||||||
(cross_padding, major_padding)
|
(cross_padding, major_padding)
|
||||||
};
|
};
|
||||||
let theme = self.core.system_theme().cosmic();
|
let _theme = self.core.system_theme().cosmic();
|
||||||
let icon_buttons = self.apps[..max_icon_count].iter().map(|app| {
|
let icon_buttons = self.apps[..max_icon_count].iter().map(|app| {
|
||||||
self.core
|
self.core
|
||||||
.applet
|
.applet
|
||||||
|
|
@ -420,7 +420,7 @@ impl cosmic::Application for Minimize {
|
||||||
(cross_padding, major_padding)
|
(cross_padding, major_padding)
|
||||||
};
|
};
|
||||||
let theme = self.core.system_theme().cosmic();
|
let theme = self.core.system_theme().cosmic();
|
||||||
let space_xxs = theme.space_xxs();
|
let _space_xxs = theme.space_xxs();
|
||||||
let icon_buttons = self.apps[max_icon_count..].iter().map(|app| {
|
let icon_buttons = self.apps[max_icon_count..].iter().map(|app| {
|
||||||
tooltip(
|
tooltip(
|
||||||
Element::from(crate::window_image::WindowImage::new(
|
Element::from(crate::window_image::WindowImage::new(
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ license = "GPL-3.0-or-later"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-fn-stream = "0.3"
|
async-fn-stream = "0.3"
|
||||||
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
cosmic-dbus-networkmanager = { path = "../../dbus-settings-bindings/networkmanager" }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
futures-util.workspace = true
|
futures-util.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic = { workspace = true, features = [
|
cosmic = { workspace = true, features = [
|
||||||
"applet",
|
"applet",
|
||||||
"applet-token",
|
"applet-token",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -26,16 +26,12 @@ 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"
|
indexmap = "2.13.0"
|
||||||
secure-string = "0.3.0"
|
secure-string = "0.3.0"
|
||||||
uuid = { version = "1.21.0", features = ["v4"] }
|
uuid = { version = "1.21.0", features = ["v4"] }
|
||||||
|
nmrs = "3.1.3"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies.cosmic-settings-network-manager-subscription]
|
[dependencies.cosmic-settings-network-manager-subscription]
|
||||||
git = "https://github.com/pop-os/cosmic-settings/"
|
path = "../../cosmic-settings/subscriptions/network-manager"
|
||||||
# path = "../../cosmic-settings/subscriptions/network-manager"
|
|
||||||
|
|
||||||
[dependencies.cosmic-settings-airplane-mode-subscription]
|
|
||||||
git = "https://github.com/pop-os/cosmic-settings/"
|
|
||||||
# path = "../../cosmic-settings/subscriptions/airplane-mode"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
cancel = Cancel·lar
|
||||||
|
connected = Connectat
|
||||||
|
wifi = Wi-Fi
|
||||||
|
identity = Identitat
|
||||||
|
|
@ -1 +1,24 @@
|
||||||
cancel = Ακύρωση
|
cancel = Ακύρωση
|
||||||
|
connect = Σύνδεση
|
||||||
|
network = Δίκτυο
|
||||||
|
check-wifi-connection = Βεβαιωθείτε ότι το Wi-Fi είναι συνδεδεμένο στο διαδίκτυο και ότι ο κωδικός πρόσβασης είναι σωστός
|
||||||
|
reset = Επαναφορά
|
||||||
|
visible-wireless-networks = Ορατά ασύρματα δίκτυα
|
||||||
|
enter-password = Εισαγάγετε τον κωδικό πρόσβασης ή το κλειδί κρυπτογράφησης
|
||||||
|
ipv6 = Διεύθυνση IPv6
|
||||||
|
airplane-mode-on = Η λειτουργία πτήσης είναι ενεργή
|
||||||
|
connecting = Σύνδεση
|
||||||
|
airplane-mode = Λειτουργία πτήσης
|
||||||
|
wifi = Wi-Fi
|
||||||
|
ipv4 = Διεύθυνση IPv4
|
||||||
|
identity = Ταυτότητα
|
||||||
|
unable-to-connect = Δεν είναι δυνατή η σύνδεση στο δίκτυο
|
||||||
|
turn-off-airplane-mode = Απενεργοποιήστε τη για ενεργοποίηση του Wi-Fi, του Bluetooth και της κινητής ευρυζωνικής σύνδεσης.
|
||||||
|
connected = Συνδέθηκε
|
||||||
|
vpn-connections = Συνδέσεις VPN
|
||||||
|
mac = MAC
|
||||||
|
router-wps-button = Μπορείτε επίσης να συνδεθείτε πατώντας το κουμπί «WPS» του δρομολογητή
|
||||||
|
settings = Ρυθμίσεις δικτύου...
|
||||||
|
megabits-per-second = Mbps
|
||||||
|
gigabits-per-second = Gbps
|
||||||
|
terabits-per-second = Tbps
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ 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 = Kapcsolódva
|
||||||
connecting = Csatlakozás…
|
connecting = Kapcsolódás
|
||||||
connect = Csatlakozás
|
connect = Kapcsolódá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
|
||||||
|
|
|
||||||
0
cosmic-applet-network/i18n/lo/cosmic_applet_network.ftl
Normal file
0
cosmic-applet-network/i18n/lo/cosmic_applet_network.ftl
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
cancel = Откажи
|
||||||
|
connected = Повезано
|
||||||
|
connect = Повежи
|
||||||
|
identity = Идентитет
|
||||||
|
wifi = Бежична
|
||||||
|
check-wifi-connection = Уверите се да је бежична мрежа повезана са интернетом и да је лозинка тачна
|
||||||
|
reset = Врати
|
||||||
|
visible-wireless-networks = Видљиве бежичне мреже
|
||||||
|
enter-password = Унесите лозинку или кључ за шифровање
|
||||||
|
ipv6 = ИПв6 адреса
|
||||||
|
airplane-mode-on = Авионски режим је укључен
|
||||||
|
connecting = Повезује се
|
||||||
|
airplane-mode = Авио режим
|
||||||
|
ipv4 = ИПв4 адреса
|
||||||
|
unable-to-connect = Немогуће повезати се са мрежом
|
||||||
|
turn-off-airplane-mode = Искључите да бисте омогућили Блутут, бежичну и мобилни интернет.
|
||||||
|
network = Мрежа
|
||||||
|
vpn-connections = ВПН везе
|
||||||
|
mac = МАК
|
||||||
|
router-wps-button = Такође се можете повезати притиском на дугме „WPS“ на рутеру
|
||||||
|
settings = Подешавања мреже...
|
||||||
|
megabits-per-second = Mbps
|
||||||
|
gigabits-per-second = Gbps
|
||||||
|
terabits-per-second = Tbps
|
||||||
|
|
@ -8,15 +8,15 @@ ipv6 = IPv6-адреса
|
||||||
mac = MAC
|
mac = MAC
|
||||||
megabits-per-second = Мбіт/с
|
megabits-per-second = Мбіт/с
|
||||||
connected = З'єднано
|
connected = З'єднано
|
||||||
connecting = Підключення
|
connecting = З’єднання
|
||||||
connect = З'єднати
|
connect = З’єднати
|
||||||
cancel = Скасувати
|
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 = Скинути
|
||||||
identity = Ідентичність
|
identity = Ідентичність
|
||||||
vpn-connections = VPN-з'єднання
|
vpn-connections = VPN-з'єднання
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use cosmic_dbus_networkmanager::settings::{NetworkManagerSettings, connection::Settings};
|
|
||||||
use cosmic_settings_network_manager_subscription::{
|
use cosmic_settings_network_manager_subscription::{
|
||||||
self as network_manager, NetworkManagerState, UUID,
|
self as network_manager, NetworkManagerState, UUID,
|
||||||
active_conns::active_conns_subscription,
|
|
||||||
available_wifi::{AccessPoint, NetworkType},
|
available_wifi::{AccessPoint, NetworkType},
|
||||||
current_networks::ActiveConnectionInfo,
|
current_networks::ActiveConnectionInfo,
|
||||||
hw_address::HwAddress,
|
hw_address::HwAddress,
|
||||||
nm_secret_agent::{self, PasswordFlag, SecretSender},
|
|
||||||
};
|
};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use nmrs::{
|
||||||
|
NetworkManager as NmrsManager, SettingsSummary,
|
||||||
|
agent::{SecretAgent, SecretAgentCapabilities, SecretRequest, SecretResponder, SecretSetting},
|
||||||
|
};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use secure_string::SecureString;
|
use secure_string::SecureString;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{BTreeMap, BTreeSet},
|
collections::{BTreeMap, HashMap},
|
||||||
sync::{Arc, LazyLock},
|
sync::{Arc, LazyLock},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
|
|
@ -33,18 +33,17 @@ use cosmic::{
|
||||||
},
|
},
|
||||||
surface, theme,
|
surface, theme,
|
||||||
widget::{
|
widget::{
|
||||||
Column, Id, Row, button, column, container, divider,
|
Id, button, column, container, divider,
|
||||||
icon::{self, from_name},
|
icon::{self, from_name},
|
||||||
row, scrollable, secure_input, text, text_input, toggler,
|
row, scrollable, secure_input, text, text_input, toggler,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use cosmic_dbus_networkmanager::interface::{
|
use cosmic_dbus_networkmanager::interface::enums::{
|
||||||
access_point,
|
ActiveConnectionState, DeviceState, NmConnectivityState,
|
||||||
enums::{ActiveConnectionState, DeviceState, NmConnectivityState, NmState},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use futures::{StreamExt, channel::mpsc::TrySendError};
|
use futures::{StreamExt, lock::Mutex as AsyncMutex};
|
||||||
use zbus::{Connection, zvariant::ObjectPath};
|
use zbus::Connection;
|
||||||
|
|
||||||
use crate::{config, fl};
|
use crate::{config, fl};
|
||||||
|
|
||||||
|
|
@ -74,14 +73,6 @@ impl NewConnectionState {
|
||||||
}
|
}
|
||||||
.ssid
|
.ssid
|
||||||
}
|
}
|
||||||
pub fn hw_address(&self) -> HwAddress {
|
|
||||||
match self {
|
|
||||||
Self::EnterPassword { access_point, .. } => access_point,
|
|
||||||
Self::Waiting(ap) => ap,
|
|
||||||
Self::Failure(ap) => ap,
|
|
||||||
}
|
|
||||||
.hw_address
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<NewConnectionState> for AccessPoint {
|
impl From<NewConnectionState> for AccessPoint {
|
||||||
|
|
@ -101,51 +92,53 @@ pub struct MyNetworkState {
|
||||||
pub known_vpns: IndexMap<UUID, ConnectionSettings>,
|
pub known_vpns: IndexMap<UUID, ConnectionSettings>,
|
||||||
pub ssid_to_uuid: BTreeMap<Box<str>, Box<str>>,
|
pub ssid_to_uuid: BTreeMap<Box<str>, Box<str>>,
|
||||||
pub devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
|
pub devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
|
||||||
pub password: Option<Password>,
|
|
||||||
pub connecting: BTreeSet<network_manager::SSID>,
|
|
||||||
pub nm_state: NetworkManagerState,
|
pub nm_state: NetworkManagerState,
|
||||||
pub requested_vpn: Option<RequestedVpn>,
|
pub requested_vpn: Option<RequestedVpn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared, take-once handle to an `nmrs` [`SecretResponder`]. Cloned freely
|
||||||
|
/// across `Message` boundaries; the first consumer to `lock().take()` it owns
|
||||||
|
/// the reply to NetworkManager.
|
||||||
|
pub type SecretResponderHandle = Arc<AsyncMutex<Option<SecretResponder>>>;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RequestedVpn {
|
pub struct RequestedVpn {
|
||||||
name: String,
|
|
||||||
uuid: Arc<str>,
|
uuid: Arc<str>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
password: SecureString,
|
password: SecureString,
|
||||||
password_hidden: bool,
|
password_hidden: bool,
|
||||||
tx: SecretSender,
|
responder: SecretResponderHandle,
|
||||||
|
/// VPN secret keys NM hinted as needed (e.g. `["password"]`). When empty,
|
||||||
|
/// `"password"` is used as a fallback.
|
||||||
|
secret_keys: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum ConnectionSettings {
|
pub enum ConnectionSettings {
|
||||||
Vpn(VpnConnectionSettings),
|
Vpn { id: String },
|
||||||
Wireguard { id: String },
|
Wireguard { id: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
/// Local mirror of the secret-agent events the applet cares about. Sourced
|
||||||
pub struct VpnConnectionSettings {
|
/// from `nmrs::agent` instead of the previous `nm_secret_agent` subscription.
|
||||||
id: String,
|
#[derive(Debug, Clone)]
|
||||||
username: Option<String>,
|
pub enum NmAgentEvent {
|
||||||
connection_type: Option<ConnectionType>,
|
RequestSecret {
|
||||||
password_flag: Option<PasswordFlag>,
|
connection_uuid: String,
|
||||||
|
connection_id: String,
|
||||||
|
setting: AgentSetting,
|
||||||
|
responder: SecretResponderHandle,
|
||||||
|
},
|
||||||
|
CancelGetSecrets,
|
||||||
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Debug, Clone)]
|
||||||
enum ConnectionType {
|
pub enum AgentSetting {
|
||||||
Password,
|
WifiPsk { ssid: String },
|
||||||
}
|
WifiEap,
|
||||||
|
Vpn { secret_keys: Vec<String> },
|
||||||
impl VpnConnectionSettings {
|
Other,
|
||||||
fn password_flag(&self) -> Option<PasswordFlag> {
|
|
||||||
self.connection_type
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|ct| match ct {
|
|
||||||
ConnectionType::Password => true,
|
|
||||||
})
|
|
||||||
.then_some(self.password_flag)
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -157,7 +150,6 @@ struct CosmicNetworkApplet {
|
||||||
// NM state
|
// NM state
|
||||||
nm_sender: Option<futures::channel::mpsc::UnboundedSender<network_manager::Request>>,
|
nm_sender: Option<futures::channel::mpsc::UnboundedSender<network_manager::Request>>,
|
||||||
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
nm_task: Option<tokio::sync::oneshot::Sender<()>>,
|
||||||
secret_tx: Option<tokio::sync::mpsc::Sender<nm_secret_agent::Request>>,
|
|
||||||
nm_state: MyNetworkState,
|
nm_state: MyNetworkState,
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
|
|
@ -264,8 +256,9 @@ fn vpn_section<'a>(
|
||||||
if show_available_vpns {
|
if show_available_vpns {
|
||||||
for (uuid, connection) in &nm_state.known_vpns {
|
for (uuid, connection) in &nm_state.known_vpns {
|
||||||
let id = match connection {
|
let id = match connection {
|
||||||
ConnectionSettings::Vpn(connection) => connection.id.as_str(),
|
ConnectionSettings::Vpn { id } | ConnectionSettings::Wireguard { id } => {
|
||||||
ConnectionSettings::Wireguard { id } => id.as_str(),
|
id.as_str()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Check if this VPN is currently active
|
// Check if this VPN is currently active
|
||||||
let is_active = nm_state.nm_state.active_conns.iter().any(
|
let is_active = nm_state.nm_state.active_conns.iter().any(
|
||||||
|
|
@ -366,15 +359,12 @@ impl CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_togglers(&mut self, state: &NetworkManagerState) {
|
fn update_togglers(&mut self, state: &NetworkManagerState) {
|
||||||
let mut changed = false;
|
|
||||||
if self.nm_state.nm_state.wifi_enabled != state.wifi_enabled {
|
if self.nm_state.nm_state.wifi_enabled != state.wifi_enabled {
|
||||||
self.nm_state.nm_state.wifi_enabled = state.wifi_enabled;
|
self.nm_state.nm_state.wifi_enabled = state.wifi_enabled;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.nm_state.nm_state.airplane_mode != state.airplane_mode {
|
if self.nm_state.nm_state.airplane_mode != state.airplane_mode {
|
||||||
self.nm_state.nm_state.airplane_mode = state.airplane_mode;
|
self.nm_state.nm_state.airplane_mode = state.airplane_mode;
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -397,53 +387,88 @@ impl CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect_vpn(&mut self, uuid: Arc<str>) -> Task<cosmic::Action<Message>> {
|
fn connect_vpn(&mut self, uuid: Arc<str>) -> Task<cosmic::Action<Message>> {
|
||||||
if let Some((tx, conn)) = self.nm_sender.clone().zip(self.conn.clone()) {
|
|
||||||
cosmic::task::future(async move {
|
cosmic::task::future(async move {
|
||||||
// Find the connection by UUID
|
match NmrsManager::new().await {
|
||||||
if let Ok(nm_settings) = NetworkManagerSettings::new(&conn).await {
|
Ok(nm) => match nm.connect_vpn_by_uuid(&uuid).await {
|
||||||
if let Ok(connections) = nm_settings.list_connections().await {
|
Ok(()) => Message::Refresh,
|
||||||
for connection in connections {
|
Err(e) => Message::Error(format!("activate VPN {uuid}: {e}")),
|
||||||
if let Ok(settings) = connection.get_settings().await {
|
},
|
||||||
let settings = Settings::new(settings);
|
Err(e) => Message::Error(format!("nmrs init: {e}")),
|
||||||
if let Some(conn_settings) = &settings.connection {
|
}
|
||||||
if conn_settings.uuid.as_ref().is_some_and(|conn_uuid| {
|
})
|
||||||
conn_uuid.as_str() == uuid.as_ref()
|
}
|
||||||
}) {
|
|
||||||
let path = connection.inner().path().clone().to_owned();
|
|
||||||
if let Err(err) =
|
|
||||||
tx.unbounded_send(network_manager::Request::Activate(
|
|
||||||
ObjectPath::try_from("/").unwrap(),
|
|
||||||
path,
|
|
||||||
))
|
|
||||||
{
|
|
||||||
if err.is_disconnected() {
|
|
||||||
return zbus::Connection::system()
|
|
||||||
.await
|
|
||||||
.context(
|
|
||||||
"failed to create system dbus connection",
|
|
||||||
)
|
|
||||||
.map_or_else(
|
|
||||||
|why| Message::Error(why.to_string()),
|
|
||||||
Message::NetworkManagerConnect,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::error!("{err:?}");
|
/// Registers an `nmrs` secret agent on the system bus and yields its
|
||||||
|
/// requests + cancellations as [`NmAgentEvent`] for the applet to handle.
|
||||||
|
fn secret_agent_task(identifier: String) -> Task<NmAgentEvent> {
|
||||||
|
cosmic::Task::stream(async_fn_stream::fn_stream(move |emitter| async move {
|
||||||
|
let registration = SecretAgent::builder()
|
||||||
|
.with_identifier(identifier)
|
||||||
|
.with_capabilities(SecretAgentCapabilities::VPN_HINTS)
|
||||||
|
.register()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (mut handle, mut requests) = match registration {
|
||||||
|
Ok(pair) => pair,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = emitter.emit(NmAgentEvent::Failed(e.to_string())).await;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
break;
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
req = requests.next() => match req {
|
||||||
|
Some(req) => {
|
||||||
|
let event = secret_request_to_event(req);
|
||||||
|
let _ = emitter.emit(event).await;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
|
cancel = handle.cancellations().next() => match cancel {
|
||||||
|
Some(_reason) => {
|
||||||
|
let _ = emitter.emit(NmAgentEvent::CancelGetSecrets).await;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(e) = handle.unregister().await {
|
||||||
|
tracing::warn!("failed to unregister secret agent: {e}");
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn secret_request_to_event(req: SecretRequest) -> NmAgentEvent {
|
||||||
|
let setting = match req.setting {
|
||||||
|
SecretSetting::WifiPsk { ssid } => AgentSetting::WifiPsk { ssid },
|
||||||
|
SecretSetting::WifiEap { .. } => AgentSetting::WifiEap,
|
||||||
|
SecretSetting::Vpn { .. } => AgentSetting::Vpn {
|
||||||
|
secret_keys: req.hints.clone(),
|
||||||
|
},
|
||||||
|
_ => AgentSetting::Other,
|
||||||
|
};
|
||||||
|
|
||||||
|
NmAgentEvent::RequestSecret {
|
||||||
|
connection_uuid: req.connection_uuid,
|
||||||
|
connection_id: req.connection_id,
|
||||||
|
setting,
|
||||||
|
responder: Arc::new(AsyncMutex::new(Some(req.responder))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reply with [`NoSecrets`](nmrs::agent::SecretResponder::no_secrets) to free
|
||||||
|
/// NetworkManager when the applet decides not to use the responder. Dropping
|
||||||
|
/// it would also auto-reply, but doing it explicitly keeps the log clean.
|
||||||
|
fn release_responder(responder: SecretResponderHandle) -> Task<cosmic::Action<Message>> {
|
||||||
|
cosmic::task::future(async move {
|
||||||
|
if let Some(r) = responder.lock().await.take() {
|
||||||
|
let _ = r.no_secrets().await;
|
||||||
}
|
}
|
||||||
}
|
Message::NoOp
|
||||||
Message::Refresh
|
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
tracing::warn!("No sender available to activate VPN.");
|
|
||||||
Task::none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -460,12 +485,13 @@ pub(crate) enum Message {
|
||||||
TogglePasswordVisibility,
|
TogglePasswordVisibility,
|
||||||
FocusSecureInput,
|
FocusSecureInput,
|
||||||
NoOp,
|
NoOp,
|
||||||
|
#[allow(dead_code)] // required by `cosmic::applet` surface path; not always emitted
|
||||||
Surface(surface::Action),
|
Surface(surface::Action),
|
||||||
ActivateVpn(Arc<str>), // UUID of VPN to activate
|
ActivateVpn(Arc<str>), // UUID of VPN to activate
|
||||||
DeactivateVpn(Arc<str>), // UUID of VPN to deactivate
|
DeactivateVpn(Arc<str>), // UUID of VPN to deactivate
|
||||||
ToggleVpnList, // Show/hide available VPNs
|
ToggleVpnList, // Show/hide available VPNs
|
||||||
/// An update from the secret agent
|
/// An update from the secret agent
|
||||||
SecretAgent(network_manager::nm_secret_agent::Event),
|
SecretAgent(NmAgentEvent),
|
||||||
/// Connect to a WiFi network access point.
|
/// Connect to a WiFi network access point.
|
||||||
Connect(network_manager::SSID, HwAddress),
|
Connect(network_manager::SSID, HwAddress),
|
||||||
/// Connect with a password
|
/// Connect with a password
|
||||||
|
|
@ -501,16 +527,6 @@ pub(crate) enum Message {
|
||||||
SelectDevice(Option<Arc<network_manager::devices::DeviceInfo>>),
|
SelectDevice(Option<Arc<network_manager::devices::DeviceInfo>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct Password {
|
|
||||||
ssid: network_manager::SSID,
|
|
||||||
hw_address: HwAddress,
|
|
||||||
identity: Option<String>,
|
|
||||||
password: SecureString,
|
|
||||||
password_hidden: bool,
|
|
||||||
tx: SecretSender,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connection_settings(conn: zbus::Connection) -> Task<Message> {
|
fn connection_settings(conn: zbus::Connection) -> Task<Message> {
|
||||||
let settings = async move {
|
let settings = async move {
|
||||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
||||||
|
|
@ -634,109 +650,30 @@ impl CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_vpns(conn: zbus::Connection) -> Task<crate::app::Message> {
|
fn load_vpns(_conn: zbus::Connection) -> Task<crate::app::Message> {
|
||||||
let settings = async move {
|
cosmic::task::future(async move {
|
||||||
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
|
let nm = match NmrsManager::new().await {
|
||||||
|
Ok(nm) => nm,
|
||||||
_ = settings.load_connections(&[]).await;
|
Err(e) => return Message::Error(format!("nmrs init: {e}")),
|
||||||
|
|
||||||
let settings = settings
|
|
||||||
// Get a list of known connections.
|
|
||||||
.list_connections()
|
|
||||||
.await?
|
|
||||||
// Prepare for wrapping in a concurrent stream.
|
|
||||||
.into_iter()
|
|
||||||
.map(|conn| async move { conn })
|
|
||||||
// Create a concurrent stream for each connection.
|
|
||||||
.apply(futures::stream::FuturesOrdered::from_iter)
|
|
||||||
// Concurrently fetch settings for each connection, and filter for VPN.
|
|
||||||
.filter_map(|conn| async move {
|
|
||||||
let settings = conn.get_settings().await.ok()?;
|
|
||||||
|
|
||||||
let connection = settings.get("connection")?;
|
|
||||||
|
|
||||||
match connection
|
|
||||||
.get("type")?
|
|
||||||
.downcast_ref::<String>()
|
|
||||||
.ok()?
|
|
||||||
.as_str()
|
|
||||||
{
|
|
||||||
"vpn" => (),
|
|
||||||
|
|
||||||
"wireguard" => {
|
|
||||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
|
||||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
|
||||||
return Some((Arc::from(uuid), ConnectionSettings::Wireguard { id }));
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
|
|
||||||
let vpn = settings.get("vpn")?;
|
|
||||||
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
|
|
||||||
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
|
|
||||||
|
|
||||||
let (connection_type, username, password_flag) = vpn
|
|
||||||
.get("data")
|
|
||||||
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
|
|
||||||
.map(|dict| {
|
|
||||||
let (mut connection_type, mut password_flag) = (None, None);
|
|
||||||
let mut username = vpn
|
|
||||||
.get("user-name")
|
|
||||||
.and_then(|u| u.downcast_ref::<String>().ok());
|
|
||||||
if dict
|
|
||||||
.get::<String, String>(&String::from("connection-type"))
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.as_deref()
|
|
||||||
// may be "password" or "password-tls"
|
|
||||||
.is_some_and(|p| p.starts_with("password"))
|
|
||||||
{
|
|
||||||
connection_type = Some(ConnectionType::Password);
|
|
||||||
username = Some(username.unwrap_or_default());
|
|
||||||
|
|
||||||
password_flag = dict
|
|
||||||
.get::<String, String>(&String::from("password-flags"))
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.and_then(|value| match value.as_str() {
|
|
||||||
"0" => Some(PasswordFlag::None),
|
|
||||||
"1" => Some(PasswordFlag::AgentOwned),
|
|
||||||
"2" => Some(PasswordFlag::NotSaved),
|
|
||||||
"4" => Some(PasswordFlag::NotRequired),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(connection_type, username, password_flag)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Some((
|
|
||||||
Arc::from(uuid),
|
|
||||||
ConnectionSettings::Vpn(VpnConnectionSettings {
|
|
||||||
id,
|
|
||||||
connection_type,
|
|
||||||
password_flag,
|
|
||||||
username,
|
|
||||||
}),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
// Reduce the settings list into
|
|
||||||
.fold(IndexMap::new(), |mut set, (uuid, data)| async move {
|
|
||||||
set.insert(uuid, data);
|
|
||||||
set
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok::<_, zbus::Error>(settings)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cosmic::task::future(async move {
|
let saved = match nm.list_saved_connections().await {
|
||||||
settings.await.map_or_else(
|
Ok(saved) => saved,
|
||||||
|why| Message::Error(why.to_string()),
|
Err(e) => return Message::Error(format!("list saved connections: {e}")),
|
||||||
Message::KnownConnections,
|
};
|
||||||
)
|
|
||||||
|
let mut map: IndexMap<UUID, ConnectionSettings> = IndexMap::new();
|
||||||
|
for c in saved {
|
||||||
|
let uuid: UUID = Arc::from(c.uuid.as_str());
|
||||||
|
let entry = match c.summary {
|
||||||
|
SettingsSummary::WireGuard { .. } => ConnectionSettings::Wireguard { id: c.id },
|
||||||
|
SettingsSummary::Vpn { .. } => ConnectionSettings::Vpn { id: c.id },
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
map.insert(uuid, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::KnownConnections(map)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -759,7 +696,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
const APP_ID: &'static str = config::APP_ID;
|
const APP_ID: &'static str = config::APP_ID;
|
||||||
|
|
||||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
|
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
|
||||||
let mut applet = Self {
|
let applet = Self {
|
||||||
core,
|
core,
|
||||||
icon_name: "network-wired-disconnected-symbolic".to_string(),
|
icon_name: "network-wired-disconnected-symbolic".to_string(),
|
||||||
token_tx: None,
|
token_tx: None,
|
||||||
|
|
@ -789,19 +726,11 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
tasks.push(update_state(conn.clone()));
|
tasks.push(update_state(conn.clone()));
|
||||||
tasks.push(update_devices(conn.clone()));
|
tasks.push(update_devices(conn.clone()));
|
||||||
tasks.push(load_vpns(conn));
|
tasks.push(load_vpns(conn));
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
|
||||||
self.secret_tx = Some(tx);
|
|
||||||
let my_id = format!(
|
let my_id = format!(
|
||||||
"com.system76.CosmicSettings.Applet.{}.NetworkManager.SecretAgent",
|
"com.system76.CosmicSettings.Applet.{}.NetworkManager.SecretAgent",
|
||||||
uuid::Uuid::new_v4()
|
uuid::Uuid::new_v4()
|
||||||
);
|
);
|
||||||
tasks.push(
|
tasks.push(secret_agent_task(my_id).map(Message::SecretAgent));
|
||||||
cosmic::Task::stream(nm_secret_agent::secret_agent_stream(
|
|
||||||
my_id.clone(),
|
|
||||||
rx,
|
|
||||||
))
|
|
||||||
.map(Message::SecretAgent),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// TODO request update of state maybe
|
// TODO request update of state maybe
|
||||||
let new_id = window::Id::unique();
|
let new_id = window::Id::unique();
|
||||||
|
|
@ -823,17 +752,20 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
Message::ToggleAirplaneMode(enabled) => {
|
Message::ToggleAirplaneMode(enabled) => {
|
||||||
self.toggle_wifi_ctr += 1;
|
self.toggle_wifi_ctr += 1;
|
||||||
if let Some(tx) = self.nm_sender.as_mut() {
|
self.nm_state.nm_state.airplane_mode = enabled;
|
||||||
if let Err(err) =
|
return cosmic::task::future(async move {
|
||||||
tx.unbounded_send(network_manager::Request::SetAirplaneMode(enabled))
|
match NmrsManager::new().await {
|
||||||
{
|
Ok(nm) => match nm.set_airplane_mode(enabled).await {
|
||||||
if err.is_disconnected() {
|
Ok(()) => Message::Refresh,
|
||||||
return system_conn().map(cosmic::Action::App);
|
Err(e) => {
|
||||||
}
|
tracing::warn!("set_airplane_mode partial failure: {e}");
|
||||||
|
Message::Refresh
|
||||||
tracing::error!("{err:?}");
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(e) => Message::Error(format!("nmrs init: {e}")),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.map(cosmic::Action::App);
|
||||||
}
|
}
|
||||||
Message::SelectWirelessAccessPoint(access_point) => {
|
Message::SelectWirelessAccessPoint(access_point) => {
|
||||||
let Some(tx) = self.nm_sender.as_ref() else {
|
let Some(tx) = self.nm_sender.as_ref() else {
|
||||||
|
|
@ -845,7 +777,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
tx.unbounded_send(network_manager::Request::SelectAccessPoint(
|
tx.unbounded_send(network_manager::Request::SelectAccessPoint(
|
||||||
access_point.ssid.clone(),
|
access_point.ssid.clone(),
|
||||||
access_point.network_type,
|
access_point.network_type,
|
||||||
self.secret_tx.clone(),
|
None,
|
||||||
self.active_device.as_ref().map(|d| d.interface.clone()),
|
self.active_device.as_ref().map(|d| d.interface.clone()),
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
|
|
@ -862,12 +794,11 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
.nm_state
|
.nm_state
|
||||||
.known_access_points
|
.known_access_points
|
||||||
.contains(&access_point)
|
.contains(&access_point)
|
||||||
{
|
&& let Err(err) =
|
||||||
if let Err(err) =
|
|
||||||
tx.unbounded_send(network_manager::Request::SelectAccessPoint(
|
tx.unbounded_send(network_manager::Request::SelectAccessPoint(
|
||||||
access_point.ssid.clone(),
|
access_point.ssid.clone(),
|
||||||
access_point.network_type,
|
access_point.network_type,
|
||||||
self.secret_tx.clone(),
|
None,
|
||||||
self.active_device.as_ref().map(|d| d.interface.clone()),
|
self.active_device.as_ref().map(|d| d.interface.clone()),
|
||||||
))
|
))
|
||||||
{
|
{
|
||||||
|
|
@ -877,7 +808,6 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
|
|
||||||
tracing::error!("{err:?}");
|
tracing::error!("{err:?}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.new_connection = Some(NewConnectionState::EnterPassword {
|
self.new_connection = Some(NewConnectionState::EnterPassword {
|
||||||
access_point,
|
access_point,
|
||||||
description: None,
|
description: None,
|
||||||
|
|
@ -915,7 +845,6 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
_ = cancel.send(());
|
_ = cancel.send(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.secret_tx = None;
|
|
||||||
return system_conn().map(cosmic::Action::App);
|
return system_conn().map(cosmic::Action::App);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -981,19 +910,23 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
tracing::warn!("Failed to find known access point with ssid: {}", ssid);
|
tracing::warn!("Failed to find known access point with ssid: {}", ssid);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
if let Some(tx) = self.nm_sender.as_ref() {
|
|
||||||
if let Err(err) =
|
|
||||||
tx.unbounded_send(network_manager::Request::Forget(ssid.into()))
|
|
||||||
{
|
|
||||||
if err.is_disconnected() {
|
|
||||||
return system_conn().map(cosmic::Action::App);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::error!("{err:?}");
|
|
||||||
}
|
|
||||||
self.show_visible_networks = true;
|
self.show_visible_networks = true;
|
||||||
return self.update(Message::SelectWirelessAccessPoint(ap));
|
let ssid_for_task = ssid.clone();
|
||||||
|
let forget_task = cosmic::task::future(async move {
|
||||||
|
match NmrsManager::new().await {
|
||||||
|
Ok(nm) => match nm.forget(&ssid_for_task).await {
|
||||||
|
Ok(()) => Message::Refresh,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("forget {ssid_for_task} failed: {e}");
|
||||||
|
Message::Refresh
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Err(e) => Message::Error(format!("nmrs init: {e}")),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(cosmic::Action::App);
|
||||||
|
let reconnect_task = self.update(Message::SelectWirelessAccessPoint(ap));
|
||||||
|
return Task::batch(vec![forget_task, reconnect_task]);
|
||||||
}
|
}
|
||||||
Message::Surface(a) => {
|
Message::Surface(a) => {
|
||||||
return cosmic::task::message(cosmic::Action::Cosmic(
|
return cosmic::task::message(cosmic::Action::Cosmic(
|
||||||
|
|
@ -1003,17 +936,17 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
Message::ActivateVpn(uuid) => {
|
Message::ActivateVpn(uuid) => {
|
||||||
return self.connect_vpn(uuid.clone());
|
return self.connect_vpn(uuid.clone());
|
||||||
}
|
}
|
||||||
Message::DeactivateVpn(name) => {
|
Message::DeactivateVpn(uuid) => {
|
||||||
if let Some(tx) = self.nm_sender.as_ref() {
|
return cosmic::task::future(async move {
|
||||||
if let Err(err) = tx.unbounded_send(network_manager::Request::Deactivate(name))
|
match NmrsManager::new().await {
|
||||||
{
|
Ok(nm) => match nm.disconnect_vpn_by_uuid(&uuid).await {
|
||||||
if err.is_disconnected() {
|
Ok(()) => Message::Refresh,
|
||||||
return system_conn().map(cosmic::Action::App);
|
Err(e) => Message::Error(format!("disconnect VPN {uuid}: {e}")),
|
||||||
}
|
},
|
||||||
|
Err(e) => Message::Error(format!("nmrs init: {e}")),
|
||||||
tracing::error!("{err:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.map(cosmic::Action::App);
|
||||||
}
|
}
|
||||||
Message::ToggleVpnList => {
|
Message::ToggleVpnList => {
|
||||||
self.show_available_vpns = !self.show_available_vpns;
|
self.show_available_vpns = !self.show_available_vpns;
|
||||||
|
|
@ -1038,7 +971,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
if let Err(err) = tx.unbounded_send(network_manager::Request::SelectAccessPoint(
|
if let Err(err) = tx.unbounded_send(network_manager::Request::SelectAccessPoint(
|
||||||
ssid,
|
ssid,
|
||||||
network_type,
|
network_type,
|
||||||
self.secret_tx.clone(),
|
None,
|
||||||
self.active_device.as_ref().map(|d| d.interface.clone()),
|
self.active_device.as_ref().map(|d| d.interface.clone()),
|
||||||
)) {
|
)) {
|
||||||
if err.is_disconnected() {
|
if err.is_disconnected() {
|
||||||
|
|
@ -1067,7 +1000,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
ssid: access_point.ssid.to_string(),
|
ssid: access_point.ssid.to_string(),
|
||||||
identity: is_enterprise.then(|| identity.clone()),
|
identity: is_enterprise.then(|| identity.clone()),
|
||||||
password,
|
password,
|
||||||
secret_tx: self.secret_tx.clone(),
|
secret_tx: None,
|
||||||
interface: self.active_device.as_ref().map(|d| d.interface.clone()),
|
interface: self.active_device.as_ref().map(|d| d.interface.clone()),
|
||||||
}) {
|
}) {
|
||||||
if err.is_disconnected() {
|
if err.is_disconnected() {
|
||||||
|
|
@ -1153,9 +1086,9 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
} => {
|
} => {
|
||||||
if let network_manager::Request::SelectAccessPoint(
|
if let network_manager::Request::SelectAccessPoint(
|
||||||
ssid,
|
ssid,
|
||||||
hw_address,
|
_hw_address,
|
||||||
_network_type,
|
_network_type,
|
||||||
secret_tx,
|
_secret_tx,
|
||||||
) = &req
|
) = &req
|
||||||
{
|
{
|
||||||
let conn_match = self
|
let conn_match = self
|
||||||
|
|
@ -1167,15 +1100,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
if let Some(ActiveConnectionInfo::WiFi { state, .. }) = state
|
if let Some(ActiveConnectionInfo::WiFi { state, .. }) = state
|
||||||
.active_conns
|
.active_conns
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|ap| {
|
.find(|ap| ap.name().as_str() == ssid.as_ref())
|
||||||
let ap_hw_address = match ap {
|
|
||||||
ActiveConnectionInfo::Wired { hw_address, .. }
|
|
||||||
| ActiveConnectionInfo::WiFi { hw_address, .. } => {
|
|
||||||
HwAddress::from_str(&hw_address).unwrap()
|
|
||||||
}
|
|
||||||
ActiveConnectionInfo::Vpn { .. } => HwAddress::default(),
|
|
||||||
};
|
|
||||||
ap.name().as_str() == ssid.as_ref()})
|
|
||||||
{
|
{
|
||||||
*state = ActiveConnectionState::Activated;
|
*state = ActiveConnectionState::Activated;
|
||||||
}
|
}
|
||||||
|
|
@ -1192,8 +1117,8 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
ssid,
|
ssid,
|
||||||
identity: _,
|
identity: _,
|
||||||
password: _,
|
password: _,
|
||||||
secret_tx,
|
secret_tx: _,
|
||||||
interface
|
interface: _,
|
||||||
} = &req
|
} = &req
|
||||||
{
|
{
|
||||||
if let Some(NewConnectionState::Waiting(access_point)) =
|
if let Some(NewConnectionState::Waiting(access_point)) =
|
||||||
|
|
@ -1207,15 +1132,14 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
} else {
|
} else {
|
||||||
self.show_visible_networks = false;
|
self.show_visible_networks = false;
|
||||||
}
|
}
|
||||||
} else if let Some(NewConnectionState::EnterPassword {
|
} else if let Some(NewConnectionState::EnterPassword { access_point, .. }) =
|
||||||
access_point, ..
|
self.new_connection.as_ref()
|
||||||
}) = self.new_connection.as_ref()
|
&& success
|
||||||
|
&& ssid.as_str() == access_point.ssid.as_ref()
|
||||||
{
|
{
|
||||||
if success && ssid.as_str() == access_point.ssid.as_ref() {
|
|
||||||
self.new_connection = None;
|
self.new_connection = None;
|
||||||
self.show_visible_networks = false;
|
self.show_visible_networks = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if self
|
} else if self
|
||||||
.new_connection
|
.new_connection
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -1246,9 +1170,9 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cosmic_settings_network_manager_subscription::Event::WiFiCredentials {
|
cosmic_settings_network_manager_subscription::Event::WiFiCredentials {
|
||||||
ssid,
|
ssid: _,
|
||||||
password,
|
password: _,
|
||||||
security_type,
|
security_type: _,
|
||||||
} => {}
|
} => {}
|
||||||
},
|
},
|
||||||
Message::NetworkManagerConnect(connection) => {
|
Message::NetworkManagerConnect(connection) => {
|
||||||
|
|
@ -1271,72 +1195,91 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
self.nm_state.devices = device_infos.into_iter().map(Arc::new).collect();
|
self.nm_state.devices = device_infos.into_iter().map(Arc::new).collect();
|
||||||
}
|
}
|
||||||
Message::WiFiEnable(enable) => {
|
Message::WiFiEnable(enable) => {
|
||||||
if let Some(sender) = self.nm_sender.as_mut() {
|
self.nm_state.nm_state.wifi_enabled = enable;
|
||||||
if let Err(err) =
|
return cosmic::task::future(async move {
|
||||||
sender.unbounded_send(network_manager::Request::SetWiFi(enable))
|
match NmrsManager::new().await {
|
||||||
{
|
Ok(nm) => match nm.set_wireless_enabled(enable).await {
|
||||||
if err.is_disconnected() {
|
Ok(()) => Message::Refresh,
|
||||||
return system_conn().map(cosmic::Action::App);
|
Err(e) => Message::Error(format!("set_wireless_enabled: {e}")),
|
||||||
}
|
},
|
||||||
|
Err(e) => Message::Error(format!("nmrs init: {e}")),
|
||||||
tracing::error!("{err:?}");
|
|
||||||
}
|
|
||||||
if let Err(err) = sender.unbounded_send(network_manager::Request::Reload) {
|
|
||||||
if err.is_disconnected() {
|
|
||||||
return system_conn().map(cosmic::Action::App);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::error!("{err:?}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.map(cosmic::Action::App);
|
||||||
}
|
}
|
||||||
Message::SecretAgent(agent_event) => match agent_event {
|
Message::SecretAgent(agent_event) => match agent_event {
|
||||||
nm_secret_agent::Event::RequestSecret {
|
NmAgentEvent::RequestSecret {
|
||||||
uuid,
|
connection_uuid,
|
||||||
name,
|
connection_id,
|
||||||
description,
|
setting,
|
||||||
previous,
|
responder,
|
||||||
tx,
|
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
|
let description = (!connection_id.is_empty()).then_some(connection_id);
|
||||||
|
let known_vpn = self
|
||||||
|
.nm_state
|
||||||
|
.known_vpns
|
||||||
|
.contains_key(connection_uuid.as_str());
|
||||||
|
|
||||||
|
let mut consumed = false;
|
||||||
|
|
||||||
if let Some(state) = self.new_connection.as_mut() {
|
if let Some(state) = self.new_connection.as_mut() {
|
||||||
match state {
|
match state {
|
||||||
NewConnectionState::EnterPassword { access_point, .. }
|
NewConnectionState::EnterPassword { access_point, .. }
|
||||||
| NewConnectionState::Waiting(access_point)
|
| NewConnectionState::Waiting(access_point)
|
||||||
| NewConnectionState::Failure(access_point) => {
|
| NewConnectionState::Failure(access_point) => {
|
||||||
if self
|
let matches_ssid = matches!(
|
||||||
|
&setting,
|
||||||
|
AgentSetting::WifiPsk { ssid }
|
||||||
|
if ssid == access_point.ssid.as_ref()
|
||||||
|
);
|
||||||
|
let matches_uuid = self
|
||||||
.nm_state
|
.nm_state
|
||||||
.ssid_to_uuid
|
.ssid_to_uuid
|
||||||
.get(access_point.ssid.as_ref())
|
.get(access_point.ssid.as_ref())
|
||||||
.is_some_and(|ap_uuid| ap_uuid.as_ref() == uuid.as_str())
|
.is_some_and(|ap_uuid| {
|
||||||
{
|
ap_uuid.as_ref() == connection_uuid.as_str()
|
||||||
|
});
|
||||||
|
|
||||||
|
if matches_ssid || matches_uuid {
|
||||||
*state = NewConnectionState::EnterPassword {
|
*state = NewConnectionState::EnterPassword {
|
||||||
access_point: access_point.clone(),
|
access_point: access_point.clone(),
|
||||||
description,
|
description: description.clone(),
|
||||||
identity: String::new(),
|
identity: String::new(),
|
||||||
password: String::new().into(),
|
password: String::new().into(),
|
||||||
password_hidden: true,
|
password_hidden: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if known_vpn {
|
||||||
} else if self.nm_state.known_vpns.contains_key(uuid.as_str()) {
|
let secret_keys = match &setting {
|
||||||
|
AgentSetting::Vpn { secret_keys } => secret_keys.clone(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
};
|
||||||
self.nm_state.requested_vpn = Some(RequestedVpn {
|
self.nm_state.requested_vpn = Some(RequestedVpn {
|
||||||
name,
|
uuid: connection_uuid.into(),
|
||||||
uuid: uuid.into(),
|
|
||||||
description,
|
description,
|
||||||
password: previous,
|
password: SecureString::from(String::new()),
|
||||||
password_hidden: true,
|
password_hidden: true,
|
||||||
tx,
|
responder: responder.clone(),
|
||||||
|
secret_keys,
|
||||||
});
|
});
|
||||||
|
consumed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The applet's Wi-Fi flow re-issues the password through
|
||||||
|
// `Authenticate` rather than the agent. Free NM with
|
||||||
|
// `NoSecrets` so it doesn't sit on a stalled GetSecrets call.
|
||||||
|
if !consumed {
|
||||||
|
return release_responder(responder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nm_secret_agent::Event::CancelGetSecrets { .. } => {
|
NmAgentEvent::CancelGetSecrets => {
|
||||||
self.new_connection = None;
|
self.new_connection = None;
|
||||||
self.nm_state.requested_vpn = None;
|
self.nm_state.requested_vpn = None;
|
||||||
}
|
}
|
||||||
nm_secret_agent::Event::Failed(error) => {
|
NmAgentEvent::Failed(error) => {
|
||||||
tracing::error!("Error from secret agent: {error:?}");
|
tracing::error!("Error from secret agent: {error}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Message::KnownConnections(index_map) => {
|
Message::KnownConnections(index_map) => {
|
||||||
|
|
@ -1358,12 +1301,30 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::ConnectVPNWithPassword => {
|
Message::ConnectVPNWithPassword => {
|
||||||
if let Some(RequestedVpn { password, tx, .. }) = self.nm_state.requested_vpn.take()
|
if let Some(RequestedVpn {
|
||||||
|
password,
|
||||||
|
responder,
|
||||||
|
secret_keys,
|
||||||
|
..
|
||||||
|
}) = self.nm_state.requested_vpn.take()
|
||||||
{
|
{
|
||||||
return Task::future(async move {
|
return Task::future(async move {
|
||||||
let mut guard = tx.lock().await;
|
let Some(responder) = responder.lock().await.take() else {
|
||||||
if let Some(tx) = guard.take() {
|
return Message::Refresh;
|
||||||
let _ = tx.send(password);
|
};
|
||||||
|
|
||||||
|
let mut secrets: HashMap<String, String> = HashMap::new();
|
||||||
|
let value = password.unsecure().to_owned();
|
||||||
|
if secret_keys.is_empty() {
|
||||||
|
secrets.insert("password".to_owned(), value);
|
||||||
|
} else {
|
||||||
|
for key in secret_keys {
|
||||||
|
secrets.insert(key, value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = responder.vpn_secrets(secrets).await {
|
||||||
|
tracing::error!("vpn secret reply failed: {e}");
|
||||||
}
|
}
|
||||||
Message::Refresh
|
Message::Refresh
|
||||||
})
|
})
|
||||||
|
|
@ -1376,7 +1337,15 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::CancelVPNConnection => {
|
Message::CancelVPNConnection => {
|
||||||
self.nm_state.requested_vpn = None;
|
if let Some(req) = self.nm_state.requested_vpn.take() {
|
||||||
|
return Task::future(async move {
|
||||||
|
if let Some(responder) = req.responder.lock().await.take() {
|
||||||
|
let _ = responder.cancel().await;
|
||||||
|
}
|
||||||
|
Message::NoOp
|
||||||
|
})
|
||||||
|
.map(cosmic::Action::App);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -1441,7 +1410,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
ActiveConnectionInfo::Wired {
|
ActiveConnectionInfo::Wired {
|
||||||
name,
|
name,
|
||||||
hw_address,
|
hw_address: _,
|
||||||
speed,
|
speed,
|
||||||
ip_addresses,
|
ip_addresses,
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -1561,7 +1530,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
.icon_size(16)
|
.icon_size(16)
|
||||||
.on_press(Message::ResetFailedKnownSsid(
|
.on_press(Message::ResetFailedKnownSsid(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
HwAddress::from_str(&hw_address).unwrap(),
|
HwAddress::from_str(hw_address).unwrap(),
|
||||||
))
|
))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
@ -1576,7 +1545,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
)
|
)
|
||||||
.on_press(Message::Disconnect(
|
.on_press(Message::Disconnect(
|
||||||
Arc::from(name.as_str()),
|
Arc::from(name.as_str()),
|
||||||
HwAddress::from_str(&hw_address).unwrap(),
|
HwAddress::from_str(hw_address).unwrap(),
|
||||||
)),
|
)),
|
||||||
)])
|
)])
|
||||||
.align_x(Alignment::Center),
|
.align_x(Alignment::Center),
|
||||||
|
|
@ -1733,15 +1702,14 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
for known in &self.nm_state.nm_state.known_access_points {
|
for known in &self.nm_state.nm_state.known_access_points {
|
||||||
if let Some(active_device) = self.active_device.as_ref() {
|
if let Some(active_device) = self.active_device.as_ref()
|
||||||
if active_device
|
&& active_device
|
||||||
.known_connections
|
.known_connections
|
||||||
.iter()
|
.iter()
|
||||||
.all(|c| &c.id != known.ssid.as_ref())
|
.all(|c| c.id != *known.ssid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let mut btn_content = Vec::with_capacity(2);
|
let mut btn_content = Vec::with_capacity(2);
|
||||||
let ssid = text::body(known.ssid.as_ref()).width(Length::Fill);
|
let ssid = text::body(known.ssid.as_ref()).width(Length::Fill);
|
||||||
if known.working {
|
if known.working {
|
||||||
|
|
@ -1872,12 +1840,10 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
content = content.push(id);
|
content = content.push(id);
|
||||||
|
|
||||||
let is_enterprise = matches!(access_point.network_type, NetworkType::EAP);
|
let is_enterprise = matches!(access_point.network_type, NetworkType::EAP);
|
||||||
let enter_password_col =
|
let enter_password_col = cosmic::widget::column::with_capacity(4)
|
||||||
cosmic::widget::column::with_capacity(4)
|
|
||||||
.push_maybe(is_enterprise.then(|| text::body(fl!("identity"))))
|
.push_maybe(is_enterprise.then(|| text::body(fl!("identity"))))
|
||||||
.push_maybe(is_enterprise.then(|| {
|
.push_maybe(is_enterprise.then(|| {
|
||||||
text_input::text_input("", identity)
|
text_input::text_input("", identity).on_input(Message::IdentityUpdate)
|
||||||
.on_input(|i| Message::IdentityUpdate(i))
|
|
||||||
}))
|
}))
|
||||||
.push(text::body(fl!("enter-password")))
|
.push(text::body(fl!("enter-password")))
|
||||||
.push_maybe(description.as_ref().map(|d| text::body(d.clone())))
|
.push_maybe(description.as_ref().map(|d| text::body(d.clone())))
|
||||||
|
|
@ -1893,9 +1859,11 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
.on_paste(|s| Message::PasswordUpdate(SecureString::from(s)))
|
.on_paste(|s| Message::PasswordUpdate(SecureString::from(s)))
|
||||||
.on_submit(|_| Message::ConnectWithPassword),
|
.on_submit(|_| Message::ConnectWithPassword),
|
||||||
)
|
)
|
||||||
.push_maybe(access_point.wps_push.then(|| {
|
.push_maybe(
|
||||||
|
access_point.wps_push.then(|| {
|
||||||
container(text::body(fl!("router-wps-button"))).padding(8)
|
container(text::body(fl!("router-wps-button"))).padding(8)
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.push(
|
.push(
|
||||||
row::with_children([
|
row::with_children([
|
||||||
Element::from(
|
Element::from(
|
||||||
|
|
@ -1987,7 +1955,7 @@ impl cosmic::Application for CosmicNetworkApplet {
|
||||||
.filter(|ap| {
|
.filter(|ap| {
|
||||||
let among_active = self.nm_state.nm_state.active_conns.iter().any(|a| {
|
let among_active = self.nm_state.nm_state.active_conns.iter().any(|a| {
|
||||||
let hw_address = active_conn_hw_address(a);
|
let hw_address = active_conn_hw_address(a);
|
||||||
ap.ssid.as_ref() == &a.name() && ap.hw_address == hw_address
|
ap.ssid.as_ref() == a.name() && ap.hw_address == hw_address
|
||||||
});
|
});
|
||||||
let among_known =
|
let among_known =
|
||||||
self.nm_state
|
self.nm_state
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ license = "GPL-3.0-only"
|
||||||
cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" }
|
cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" }
|
||||||
cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" }
|
cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
# cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" }
|
# cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" }
|
||||||
# cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" }
|
# cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
hours-ago =
|
||||||
|
{ $duration ->
|
||||||
|
[0] Μόλις τώρα
|
||||||
|
[one] Πριν από 1 ώρα
|
||||||
|
*[other] Πριν από { $duration } ώρες
|
||||||
|
}
|
||||||
|
show-more = Εμφάνιση { $more } ακόμη
|
||||||
|
clear-all = Απαλοιφή όλων των ειδοποιήσεων
|
||||||
|
notification-settings = Ρυθμίσεις ειδοποιήσεων...
|
||||||
|
no-notifications = Καμία ειδοποίηση
|
||||||
|
do-not-disturb = Μην ενοχλείτε
|
||||||
|
show-less = Εμφάνιση λιγότερων
|
||||||
|
clear-group = Απαλοιφή ομάδας
|
||||||
|
minutes-ago =
|
||||||
|
{ $duration ->
|
||||||
|
[0] Μόλις τώρα
|
||||||
|
[one] Πριν από 1 λεπτό
|
||||||
|
*[other] Πριν από { $duration } λεπτά
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
hours-ago =
|
hours-ago =
|
||||||
{ $duration ->
|
{ $duration ->
|
||||||
[0] Most
|
[0] Épp most
|
||||||
[one] 1 órával ezelőtt
|
[one] 1 órája
|
||||||
*[other] { $duration } órával ezelőtt
|
*[other] { $duration } órája
|
||||||
}
|
}
|
||||||
minutes-ago =
|
minutes-ago =
|
||||||
{ $duration ->
|
{ $duration ->
|
||||||
[0] Most
|
[0] Épp most
|
||||||
[one] 1 perccel ezelőtt
|
[one] 1 perce
|
||||||
*[other] { $duration } perccel ezelőtt
|
*[other] { $duration } perce
|
||||||
}
|
}
|
||||||
show-less = Kevesebb megjelenítése
|
show-less = Kevesebb megjelenítése
|
||||||
show-more = { $more } további megjelenítése
|
show-more = { $more } további megjelenítése
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
show-more = Прикажи још { $more }
|
||||||
|
clear-all = Очисти сва обавештења
|
||||||
|
notification-settings = Подешавања обавештења...
|
||||||
|
no-notifications = Без обавештења
|
||||||
|
do-not-disturb = Не узнемиравај
|
||||||
|
show-less = Прикажи мање
|
||||||
|
clear-group = Очисти групу
|
||||||
|
hours-ago =
|
||||||
|
{ $duration ->
|
||||||
|
[0] Управо сада
|
||||||
|
[one] Пре 1 сата
|
||||||
|
*[other] Пре { $duration } сати
|
||||||
|
}
|
||||||
|
minutes-ago =
|
||||||
|
{ $duration ->
|
||||||
|
[0] Управо сада
|
||||||
|
[one] Пре 1 минута
|
||||||
|
*[other] Пре { $duration } минута
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ license = "GPL-3.0-only"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
logind-zbus = "5.3.2"
|
logind-zbus = "5.3.2"
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
rustix.workspace = true
|
rustix.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
cancel = Cancel·lar
|
||||||
|
confirm = Confirmar
|
||||||
|
restart = Reinicia
|
||||||
|
suspend = Suspèn
|
||||||
|
|
@ -4,3 +4,33 @@ log-out = Αποσύνδεση
|
||||||
restart = Επανεκκίνηση
|
restart = Επανεκκίνηση
|
||||||
suspend = Αναστολή
|
suspend = Αναστολή
|
||||||
confirm = Επιβεβαίωση
|
confirm = Επιβεβαίωση
|
||||||
|
confirm-button =
|
||||||
|
{ $action ->
|
||||||
|
[restart] { restart }
|
||||||
|
[suspend] { suspend }
|
||||||
|
[shutdown] Τερματισμός
|
||||||
|
[log-out] { log-out }
|
||||||
|
*[other] { confirm }
|
||||||
|
}
|
||||||
|
power = Ενέργεια
|
||||||
|
confirm-body =
|
||||||
|
Θα εκτελεστεί αυτόματα { $action ->
|
||||||
|
[restart] επανεκκίνηση
|
||||||
|
[suspend] αναστολή
|
||||||
|
[shutdown] τερματισμός
|
||||||
|
[lock-screen] κλείδωμα της οθόνης
|
||||||
|
[log-out] αποσύνδεση
|
||||||
|
*[other] η επιλεγμένη ενέργεια
|
||||||
|
} του συστήματος σε { $countdown } δευτερόλεπτα.
|
||||||
|
lock-screen = Κλείδωμα οθόνης
|
||||||
|
log-out-shortcut = Super + Shift + Escape
|
||||||
|
settings = Ρυθμίσεις...
|
||||||
|
lock-screen-shortcut = Super + Escape
|
||||||
|
confirm-title =
|
||||||
|
{ $action ->
|
||||||
|
[restart] { restart }
|
||||||
|
[suspend] { suspend }
|
||||||
|
[shutdown] { shutdown }
|
||||||
|
[log-out] Έξοδος από όλες τις εφαρμογές και αποσύνδεση
|
||||||
|
*[other] Εφαρμογή της επιλεγμένης ενέργειας
|
||||||
|
} τώρα;
|
||||||
|
|
|
||||||
|
|
@ -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/lo/cosmic_applet_power.ftl
Normal file
0
cosmic-applet-power/i18n/lo/cosmic_applet_power.ftl
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
power = Напајање
|
||||||
|
lock-screen = Закључај екран
|
||||||
|
shutdown = Угаси
|
||||||
|
log-out = Одјава
|
||||||
|
restart = Поново покрени
|
||||||
|
log-out-shortcut = Супер + Shift + Esc
|
||||||
|
cancel = Откажи
|
||||||
|
suspend = Обустави
|
||||||
|
confirm = Потврди
|
||||||
|
settings = Подешавања...
|
||||||
|
lock-screen-shortcut = Супер + Esc
|
||||||
|
confirm-button =
|
||||||
|
{ $action ->
|
||||||
|
[restart] { restart }
|
||||||
|
[suspend] { suspend }
|
||||||
|
[shutdown] Искључи
|
||||||
|
[log-out] { log-out }
|
||||||
|
*[other] { confirm }
|
||||||
|
}
|
||||||
|
confirm-body =
|
||||||
|
Систем ће { $action ->
|
||||||
|
[restart] поново покренути
|
||||||
|
[suspend] обуставити
|
||||||
|
[shutdown] искључити
|
||||||
|
[lock-screen] закључати екран
|
||||||
|
[log-out] одјавити се
|
||||||
|
*[other] применити изабрану радњу
|
||||||
|
} самостално за { $countdown } секунде.
|
||||||
|
confirm-title =
|
||||||
|
{ $action ->
|
||||||
|
[restart] { restart }
|
||||||
|
[suspend] { suspend }
|
||||||
|
[shutdown] { shutdown }
|
||||||
|
[log-out] Затвори све програме и одјави се
|
||||||
|
*[other] Примени изабрану радњу
|
||||||
|
} сада?
|
||||||
|
|
@ -16,9 +16,8 @@ use cosmic::{
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
surface, theme,
|
surface, theme,
|
||||||
widget::{Space, button, divider, icon, space, text},
|
widget::{button, divider, icon, space, text},
|
||||||
};
|
};
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use logind_zbus::{
|
use logind_zbus::{
|
||||||
manager::ManagerProxy,
|
manager::ManagerProxy,
|
||||||
|
|
@ -35,9 +34,6 @@ pub mod session_manager;
|
||||||
|
|
||||||
use crate::{cosmic_session::CosmicSessionProxy, session_manager::SessionManagerProxy};
|
use crate::{cosmic_session::CosmicSessionProxy, session_manager::SessionManagerProxy};
|
||||||
|
|
||||||
static SUBSURFACE_ID: LazyLock<cosmic::widget::Id> =
|
|
||||||
LazyLock::new(|| cosmic::widget::Id::new("subsurface"));
|
|
||||||
|
|
||||||
pub fn run() -> cosmic::iced::Result {
|
pub fn run() -> cosmic::iced::Result {
|
||||||
localize::localize();
|
localize::localize();
|
||||||
|
|
||||||
|
|
@ -49,7 +45,6 @@ struct Power {
|
||||||
icon_name: String,
|
icon_name: String,
|
||||||
popup: Option<window::Id>,
|
popup: Option<window::Id>,
|
||||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||||
subsurface_id: window::Id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
@ -75,6 +70,7 @@ impl PowerAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Message {
|
enum Message {
|
||||||
Action(PowerAction),
|
Action(PowerAction),
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
|
|
@ -104,7 +100,6 @@ impl cosmic::Application for Power {
|
||||||
Self {
|
Self {
|
||||||
core,
|
core,
|
||||||
icon_name: "system-shutdown-symbolic".to_string(),
|
icon_name: "system-shutdown-symbolic".to_string(),
|
||||||
subsurface_id: window::Id::unique(),
|
|
||||||
token_tx: None,
|
token_tx: None,
|
||||||
popup: Option::default(),
|
popup: Option::default(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ use cosmic::{
|
||||||
iced::{
|
iced::{
|
||||||
self, Length, Subscription,
|
self, Length, Subscription,
|
||||||
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
||||||
theme::Style,
|
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
surface,
|
surface,
|
||||||
|
|
@ -25,6 +24,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
None,
|
None,
|
||||||
Activate(usize),
|
Activate(usize),
|
||||||
|
|
@ -577,7 +577,7 @@ fn menu_icon_button<'a>(
|
||||||
let icon = menu.icon_handle().clone();
|
let icon = menu.icon_handle().clone();
|
||||||
|
|
||||||
let theme = cosmic::theme::active();
|
let theme = cosmic::theme::active();
|
||||||
let theme = theme.cosmic();
|
let _theme = theme.cosmic();
|
||||||
|
|
||||||
let suggested = applet.suggested_size(true);
|
let suggested = applet.suggested_size(true);
|
||||||
let padding = applet.suggested_padding(true).1;
|
let padding = applet.suggested_padding(true).1;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub struct State {
|
||||||
expanded: Option<i32>,
|
expanded: Option<i32>,
|
||||||
// TODO handle icon with multiple sizes?
|
// TODO handle icon with multiple sizes?
|
||||||
icon_handle: icon::Handle,
|
icon_handle: icon::Handle,
|
||||||
|
icon_theme_path: Option<PathBuf>,
|
||||||
click_event: Option<(i32, bool)>,
|
click_event: Option<(i32, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ impl State {
|
||||||
icon_handle: icon::from_name("application-default")
|
icon_handle: icon::from_name("application-default")
|
||||||
.prefer_svg(true)
|
.prefer_svg(true)
|
||||||
.handle(),
|
.handle(),
|
||||||
|
icon_theme_path: None,
|
||||||
click_event: None,
|
click_event: None,
|
||||||
},
|
},
|
||||||
iced::Task::none(),
|
iced::Task::none(),
|
||||||
|
|
@ -63,6 +65,7 @@ impl State {
|
||||||
}
|
}
|
||||||
Msg::Icon(update) => {
|
Msg::Icon(update) => {
|
||||||
let icon_name = update.name.unwrap_or_default();
|
let icon_name = update.name.unwrap_or_default();
|
||||||
|
self.icon_theme_path = update.theme_path;
|
||||||
|
|
||||||
// Use the icon pixmap if an icon was not defined by name.
|
// Use the icon pixmap if an icon was not defined by name.
|
||||||
if icon_name.is_empty() {
|
if icon_name.is_empty() {
|
||||||
|
|
@ -93,13 +96,18 @@ impl State {
|
||||||
self.icon_handle = if Path::new(&icon_name).exists() {
|
self.icon_handle = if Path::new(&icon_name).exists() {
|
||||||
icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true)
|
icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true)
|
||||||
} else {
|
} else {
|
||||||
icon::from_name(icon_name)
|
let mut builder = icon::from_name(icon_name).prefer_svg(true).fallback(Some(
|
||||||
.prefer_svg(true)
|
IconFallback::Names(vec![
|
||||||
.fallback(Some(IconFallback::Names(vec![
|
|
||||||
"application-default".into(),
|
"application-default".into(),
|
||||||
"application-x-executable".into(),
|
"application-x-executable".into(),
|
||||||
])))
|
]),
|
||||||
.handle()
|
));
|
||||||
|
|
||||||
|
if let Some(ref theme_path) = self.icon_theme_path {
|
||||||
|
builder = builder.with_extra_paths(vec![theme_path.clone()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.handle()
|
||||||
};
|
};
|
||||||
|
|
||||||
iced::Task::none()
|
iced::Task::none()
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,18 @@
|
||||||
//! can be socket-activated and not conflict with anything else running as a status notifier
|
//! can be socket-activated and not conflict with anything else running as a status notifier
|
||||||
//! watcher.
|
//! watcher.
|
||||||
//!
|
//!
|
||||||
//! The daemon runs as long as as there is at least one client still connected. Which it checks
|
//! The daemon runs as long as there is at least one client still connected.
|
||||||
//! for every `REFRESH_INTERVAL`.
|
|
||||||
|
|
||||||
use crate::subscriptions::status_notifier_watcher::server::create_service;
|
use crate::subscriptions::status_notifier_watcher::server::create_service;
|
||||||
use crate::unique_names::UniqueNames;
|
use crate::unique_names::UniqueNames;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::{collections::HashSet, time::Duration};
|
use std::collections::HashSet;
|
||||||
use zbus::fdo;
|
use zbus::fdo;
|
||||||
use zbus::message::Header;
|
use zbus::message::Header;
|
||||||
|
|
||||||
const DBUS_NAME: &str = "com.system76.CosmicStatusNotifierWatcher";
|
const DBUS_NAME: &str = "com.system76.CosmicStatusNotifierWatcher";
|
||||||
const OBJECT_PATH: &str = "/CosmicStatusNotifierWatcher";
|
const OBJECT_PATH: &str = "/CosmicStatusNotifierWatcher";
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
|
||||||
|
|
||||||
/// Run daemon
|
/// Run daemon
|
||||||
pub fn run() -> cosmic::iced::Result {
|
pub fn run() -> cosmic::iced::Result {
|
||||||
if let Err(err) = run_inner() {
|
if let Err(err) = run_inner() {
|
||||||
|
|
@ -42,7 +39,7 @@ pub async fn cosmic_register(conn: &zbus::Connection) -> zbus::Result<()> {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(value) = stream.next().await {
|
while let Some(value) = stream.next().await {
|
||||||
if let Some(_unique_name) = value {
|
if let Some(_unique_name) = value {
|
||||||
/// Register with new owner
|
// Register with new owner.
|
||||||
let _ = cosmic_watcher.register_applet().await;
|
let _ = cosmic_watcher.register_applet().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cosmic::iced::{self, Subscription};
|
use cosmic::iced::{self, Subscription};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
|
|
@ -27,7 +28,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 {
|
||||||
|
|
@ -118,11 +119,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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
cctk.workspace = true
|
cctk.workspace = true
|
||||||
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
|
cosmic-comp-config.workspace = true
|
||||||
cosmic-protocols.workspace = true
|
cosmic-protocols.workspace = true
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
arrow-keys = βέλη
|
||||||
|
move-window = Μετακίνηση παραθύρου
|
||||||
|
shift = Shift
|
||||||
|
super = Super
|
||||||
|
new-workspace = Συμπεριφορά νέων χώρων εργασίας
|
||||||
|
tiled = Παράθεση
|
||||||
|
active-hint = Ένδειξη ενεργού παραθύρου
|
||||||
|
navigate-windows = Πλοήγηση στα παράθυρα
|
||||||
|
toggle-floating-window = Εναλλαγή αιώρησης παραθύρων
|
||||||
|
all-workspaces = Όλοι οι χώροι εργασίας
|
||||||
|
per-workspace = Ανά χώρο εργασίας
|
||||||
|
autotile-behavior = Παράθεση παραθύρων στους χώρους εργασίας
|
||||||
|
tile-current = Παράθεση στον τρέχοντα χώρο εργασίας
|
||||||
|
floating = Αιώρηση
|
||||||
|
gaps = Κενά
|
||||||
|
tile-windows = Αυτόματη παράθεση παραθύρων
|
||||||
|
view-all-shortcuts = Προβολή όλων των συντομεύσεων...
|
||||||
|
shortcuts = Συντομεύσεις
|
||||||
|
floating-window-exceptions = Εξαιρέσεις αιωρούμενων παραθύρων...
|
||||||
|
window-management-settings = Ρυθμίσεις διαχείρισης παραθύρων...
|
||||||
|
|
@ -3,11 +3,11 @@ tile-current = Jelenlegi munkaterület csempézése
|
||||||
shortcuts = Gyorsbillentyűk
|
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 = Lebegő ablak be/ki
|
||||||
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
|
||||||
|
|
|
||||||
0
cosmic-applet-tiling/i18n/lo/cosmic_applet_tiling.ftl
Normal file
0
cosmic-applet-tiling/i18n/lo/cosmic_applet_tiling.ftl
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
arrow-keys = стрелице
|
||||||
|
tiled = Поплочано
|
||||||
|
active-hint = Активна помоћ
|
||||||
|
navigate-windows = Крећите се прозорима
|
||||||
|
toggle-floating-window = Окини пловни прозор
|
||||||
|
all-workspaces = Сви радни простори
|
||||||
|
per-workspace = По радном простору
|
||||||
|
autotile-behavior = Поплочај прозоре на радним просторима
|
||||||
|
tile-current = Поплочај тренутни радни простор
|
||||||
|
floating = Плутајуће
|
||||||
|
gaps = Размаци
|
||||||
|
move-window = Премести прозор
|
||||||
|
tile-windows = Самостално поплочај прозоре
|
||||||
|
view-all-shortcuts = Прикажи све пречице...
|
||||||
|
shortcuts = Пречице
|
||||||
|
shift = Shift
|
||||||
|
floating-window-exceptions = Изузеци за пловне прозоре...
|
||||||
|
super = Супер
|
||||||
|
window-management-settings = Подешавања за управљање прозорима...
|
||||||
|
new-workspace = Понашање новог радног простора
|
||||||
|
|
@ -26,7 +26,7 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
use cosmic_comp_config::{CosmicCompConfig, TileBehavior};
|
use cosmic_comp_config::{CosmicCompConfig, TileBehavior};
|
||||||
use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState;
|
use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState;
|
||||||
use std::{thread, time::Instant};
|
use std::thread;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
const ID: &str = "com.system76.CosmicAppletTiling";
|
const ID: &str = "com.system76.CosmicAppletTiling";
|
||||||
|
|
@ -46,6 +46,7 @@ pub struct Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
PopupClosed(Id),
|
PopupClosed(Id),
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ cosmic-applets-config.workspace = true
|
||||||
jiff = "0.2"
|
jiff = "0.2"
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
|
|
@ -17,5 +17,5 @@ tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
icu = { version = "2.1.1", features = ["compiled_data"] }
|
icu = { version = "2.1.1", features = ["compiled_data"] }
|
||||||
zbus.workspace = true
|
zbus.workspace = true
|
||||||
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
timedate-zbus = { path = "../../dbus-settings-bindings/timedate" }
|
||||||
logind-zbus = "5.3.2"
|
logind-zbus = "5.3.2"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
datetime-settings = Ρυθμίσεις ημερομηνίας, ώρας και ημερολογίου...
|
||||||
0
cosmic-applet-time/i18n/lo/cosmic_applet_time.ftl
Normal file
0
cosmic-applet-time/i18n/lo/cosmic_applet_time.ftl
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
datetime-settings = Датум, време и подешавања календара...
|
||||||
|
|
@ -94,6 +94,7 @@ pub struct Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
@ -361,7 +362,7 @@ impl cosmic::Application for Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
fn time_subscription(mut show_seconds: watch::Receiver<bool>) -> Subscription<Message> {
|
fn time_subscription(show_seconds: watch::Receiver<bool>) -> Subscription<Message> {
|
||||||
struct Wrapper {
|
struct Wrapper {
|
||||||
inner: watch::Receiver<bool>,
|
inner: watch::Receiver<bool>,
|
||||||
id: &'static str,
|
id: &'static str,
|
||||||
|
|
@ -376,7 +377,7 @@ impl cosmic::Application for Window {
|
||||||
inner: show_seconds,
|
inner: show_seconds,
|
||||||
id: "time-sub",
|
id: "time-sub",
|
||||||
},
|
},
|
||||||
|Wrapper { inner, id }| {
|
|Wrapper { inner, id: _ }| {
|
||||||
let mut show_seconds = inner.clone();
|
let mut show_seconds = inner.clone();
|
||||||
stream::channel(1, move |mut output: mpsc::Sender<Message>| async move {
|
stream::channel(1, move |mut output: mpsc::Sender<Message>| async move {
|
||||||
// Mark this receiver's state as changed so that it always receives an initial
|
// Mark this receiver's state as changed so that it always receives an initial
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
cctk.workspace = true
|
cctk.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
cosmic-applet-workspaces = Χώροι εργασίας COSMIC
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
cosmic-applet-workspaces = КОСМИК радни простори
|
||||||
|
|
@ -84,6 +84,7 @@ impl IcedWorkspacesApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
enum Message {
|
enum Message {
|
||||||
WorkspaceUpdate(WorkspacesUpdate),
|
WorkspaceUpdate(WorkspacesUpdate),
|
||||||
WorkspacePressed(ExtWorkspaceHandleV1),
|
WorkspacePressed(ExtWorkspaceHandleV1),
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ cosmic-applet-time = { path = "../cosmic-applet-time" }
|
||||||
cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" }
|
cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" }
|
||||||
cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" }
|
cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" }
|
||||||
cosmic-panel-button = { path = "../cosmic-panel-button" }
|
cosmic-panel-button = { path = "../cosmic-panel-button" }
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@ fn main() -> cosmic::iced::Result {
|
||||||
};
|
};
|
||||||
|
|
||||||
let start = applet.rfind('/').map_or(0, |v| v + 1);
|
let start = applet.rfind('/').map_or(0, |v| v + 1);
|
||||||
let cmd = &applet.as_str()[start..];
|
let cmd = applet.as_str()[start..].to_string();
|
||||||
|
|
||||||
tracing::info!("Starting `{cmd}` with version {VERSION}");
|
tracing::info!("Starting `{cmd}` with version {VERSION}");
|
||||||
|
|
||||||
match cmd {
|
let cmd_for_run = cmd.clone();
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
|
||||||
|
match cmd_for_run.as_str() {
|
||||||
"cosmic-app-list" => cosmic_app_list::run(),
|
"cosmic-app-list" => cosmic_app_list::run(),
|
||||||
"cosmic-applet-a11y" => cosmic_applet_a11y::run(),
|
"cosmic-applet-a11y" => cosmic_applet_a11y::run(),
|
||||||
"cosmic-applet-audio" => cosmic_applet_audio::run(),
|
"cosmic-applet-audio" => cosmic_applet_audio::run(),
|
||||||
|
|
@ -34,4 +36,20 @@ fn main() -> cosmic::iced::Result {
|
||||||
"cosmic-panel-button" => cosmic_panel_button::run(),
|
"cosmic-panel-button" => cosmic_panel_button::run(),
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(payload) => {
|
||||||
|
let msg = payload
|
||||||
|
.downcast_ref::<&str>()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.or_else(|| payload.downcast_ref::<String>().cloned())
|
||||||
|
.unwrap_or_else(|| "<non-string panic>".to_string());
|
||||||
|
tracing::error!(
|
||||||
|
"`{cmd}` panicked (likely compositor disconnect), exiting cleanly: {msg}"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
libcosmic.workspace = true
|
cosmic.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ impl Button {
|
||||||
icon: cosmic::widget::icon::Handle,
|
icon: cosmic::widget::icon::Handle,
|
||||||
) -> cosmic::widget::Button<'a, Message> {
|
) -> cosmic::widget::Button<'a, Message> {
|
||||||
let theme = cosmic::theme::active();
|
let theme = cosmic::theme::active();
|
||||||
let theme = theme.cosmic();
|
let _theme = theme.cosmic();
|
||||||
|
|
||||||
let suggested = self.core.applet.suggested_size(icon.symbolic);
|
let suggested = self.core.applet.suggested_size(icon.symbolic);
|
||||||
let (major_padding, applet_padding_minor_axis) =
|
let (major_padding, applet_padding_minor_axis) =
|
||||||
|
|
@ -123,11 +123,15 @@ impl cosmic::Application for Button {
|
||||||
fn update(&mut self, message: Msg) -> app::Task<Msg> {
|
fn update(&mut self, message: Msg) -> app::Task<Msg> {
|
||||||
match message {
|
match message {
|
||||||
Msg::Press => {
|
Msg::Press => {
|
||||||
let _ = Command::new("sh")
|
if let Ok(mut child) = Command::new("sh")
|
||||||
.arg("-c")
|
.arg("-c")
|
||||||
.arg(&self.desktop.exec)
|
.arg(format!("exec {}", self.desktop.exec))
|
||||||
.spawn()
|
.spawn()
|
||||||
.unwrap();
|
{
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = child.wait();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Msg::ConfigUpdated(conf) => {
|
Msg::ConfigUpdated(conf) => {
|
||||||
self.config = conf
|
self.config = conf
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue