Compare commits
No commits in common. "pre-1.6-merge-20260819" and "master" have entirely different histories.
pre-1.6-me
...
master
133 changed files with 2447 additions and 5055 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.93.1
|
toolchain: 1.90.0
|
||||||
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.93.1
|
toolchain: 1.90.0
|
||||||
args: --all --all-targets --all-features
|
args: --all --all-targets --all-features
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"format_on_save": "on",
|
|
||||||
"lsp": {
|
|
||||||
"rust-analyzer": {
|
|
||||||
"initialization_options": {
|
|
||||||
"check": {
|
|
||||||
"command": "clippy",
|
|
||||||
},
|
|
||||||
"rustfmt": {
|
|
||||||
"extraArgs": ["+nightly"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
1622
Cargo.lock
generated
1622
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
59
Cargo.toml
59
Cargo.toml
|
|
@ -25,11 +25,11 @@ resolver = "3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
cctk = { package = "cosmic-client-toolkit", path = "../cosmic-protocols/client-toolkit" }
|
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be" }
|
||||||
cosmic-applets-config = { path = "cosmic-applets-config" }
|
cosmic-applets-config = { path = "cosmic-applets-config" }
|
||||||
cosmic-protocols = { path = "../cosmic-protocols", default-features = false, features = [
|
cosmic-protocols = { git = "https://github.com/pop-os/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"
|
||||||
cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features = false, features = [
|
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = [
|
||||||
"applet",
|
"applet",
|
||||||
"applet-token",
|
"applet-token",
|
||||||
"dbus-config",
|
"dbus-config",
|
||||||
|
|
@ -48,7 +48,6 @@ cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features =
|
||||||
"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"
|
||||||
|
|
@ -59,14 +58,14 @@ 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 = { path = "../libcosmic/cosmic-config" }
|
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
zlink = "0.5.0"
|
|
||||||
|
|
||||||
[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"]
|
||||||
|
|
@ -83,48 +82,12 @@ 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 = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols" }
|
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
||||||
cosmic-client-toolkit = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols/client-toolkit" }
|
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
|
||||||
|
|
||||||
[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-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" }
|
|
||||||
|
|
||||||
[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" }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-app-list"
|
name = "cosmic-app-list"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -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 }
|
||||||
cosmic.workspace = true
|
libcosmic.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 = { path = "../../dbus-settings-bindings/switcheroo-control" }
|
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-app-list-config"
|
name = "cosmic-app-list-config"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
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,8 +1 @@
|
||||||
new-window = Νέο παράθυρο
|
new-window = Νέο παράθυρο
|
||||||
quit = Έξοδος
|
|
||||||
cosmic-app-list = Περιοχή εφαρμογών
|
|
||||||
run = Εκτέλεση
|
|
||||||
run-on = Εκτέλεση με { $gpu }
|
|
||||||
quit-all = Έξοδος από όλα
|
|
||||||
run-on-default = (Προεπιλογή)
|
|
||||||
pin = Καρφίτσωμα στην περιοχή εφαρμογών
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,4 @@ quit-all = Quit All
|
||||||
new-window = New Window
|
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,17 +3,6 @@ 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 = Exécuter
|
run = Lancer
|
||||||
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 = Қолданбалар сөресі
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
run = Bixebitîne
|
|
||||||
run-on = Li ser { $gpu } bixebitîne
|
|
||||||
run-on-default = (Berdest)
|
|
||||||
pin = Bo destgeha sepanan bi dar bixe
|
|
||||||
quit = Biqedîne
|
|
||||||
quit-all = Hemûyan biqedîne
|
|
||||||
new-window = Çarçoveya nû
|
|
||||||
cosmic-app-list = Destgeha sepanan
|
|
||||||
|
|
@ -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 = (По умолчанию)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
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 = (Основна)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,440 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,378 +0,0 @@
|
||||||
// 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,8 +2,6 @@
|
||||||
// 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,10 +388,7 @@ impl CaptureData {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if let Err(err) = self.conn.flush() {
|
self.conn.flush().unwrap();
|
||||||
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())
|
||||||
|
|
@ -440,10 +437,7 @@ impl CaptureData {
|
||||||
session: capture_session.clone(),
|
session: capture_session.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if let Err(err) = self.conn.flush() {
|
self.conn.flush().unwrap();
|
||||||
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
|
||||||
|
|
@ -715,10 +709,7 @@ pub(crate) fn wayland_handler(
|
||||||
if app_data.exit {
|
if app_data.exit {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Err(err) = event_loop.dispatch(None, &mut app_data) {
|
event_loop.dispatch(None, &mut app_data).unwrap();
|
||||||
tracing::error!("Wayland event loop terminated: {err}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-a11y"
|
name = "cosmic-applet-a11y"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
@ -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
|
||||||
cosmic.workspace = true
|
libcosmic.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,7 +17,9 @@ tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
||||||
[dependencies.cosmic-settings-a11y-manager-subscription]
|
[dependencies.cosmic-settings-a11y-manager-subscription]
|
||||||
path = "../../cosmic-settings/subscriptions/a11y-manager"
|
git = "https://github.com/pop-os/cosmic-settings"
|
||||||
|
# path = "../../cosmic-settings/subscriptions/a11y-manager"
|
||||||
|
|
||||||
[dependencies.cosmic-settings-accessibility-subscription]
|
[dependencies.cosmic-settings-accessibility-subscription]
|
||||||
path = "../../cosmic-settings/subscriptions/accessibility"
|
git = "https://github.com/pop-os/cosmic-settings"
|
||||||
|
# path = "../../cosmic-settings/subscriptions/accessibility"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
screen-reader = Ανάγνωση οθόνης
|
|
||||||
invert-colors = Αναστροφή χρωμάτων
|
|
||||||
high-contrast = Υψηλή αντίθεση
|
|
||||||
settings = Ρυθμίσεις προσβασιμότητας...
|
|
||||||
magnifier = Μεγεθυντικός φακός
|
|
||||||
filter-colors = Φίλτρο χρωμάτων
|
|
||||||
|
|
@ -2,5 +2,5 @@ screen-reader = スクリーンリーダー
|
||||||
magnifier = 拡大鏡
|
magnifier = 拡大鏡
|
||||||
settings = アクセシビリティ設定…
|
settings = アクセシビリティ設定…
|
||||||
invert-colors = 色を反転
|
invert-colors = 色を反転
|
||||||
filter-colors = カラーフィルター
|
filter-colors = 色のフィルター
|
||||||
high-contrast = ハイコントラスト
|
high-contrast = ハイコントラスト
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
screen-reader = Xwînerê dîmenderê
|
|
||||||
magnifier = Mezinker
|
|
||||||
invert-colors = Rengan berevajî bike
|
|
||||||
settings = Sazkariyên gihîştinê...
|
|
||||||
filter-colors = Rengan parzûn bike
|
|
||||||
high-contrast = Dijbiriya bilind
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
screen-reader = Читач екрана
|
|
||||||
invert-colors = Обрни боје
|
|
||||||
high-contrast = Високи контраст
|
|
||||||
filter-colors = Филтрирај боје
|
|
||||||
settings = Подешавања приступачности...
|
|
||||||
magnifier = Увеличавач
|
|
||||||
|
|
@ -26,9 +26,10 @@ use cosmic::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use cosmic_settings_a11y_manager_subscription::{
|
use cosmic_settings_a11y_manager_subscription::{
|
||||||
AccessibilityEvent, AccessibilityRequest, ColorFilter,
|
self as cosmic_a11y_manager, 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 {
|
||||||
|
|
@ -51,7 +52,6 @@ struct CosmicA11yApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use anyhow;
|
use anyhow;
|
||||||
use cctk::sctk::reexports::calloop::{self};
|
use cctk::sctk::reexports::calloop::{self, channel::SyncSender};
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
self, Subscription,
|
self, Subscription,
|
||||||
futures::{self, SinkExt},
|
futures::{self, SinkExt, StreamExt, channel::mpsc},
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-audio"
|
name = "cosmic-applet-audio"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures.workspace = true
|
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
cosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
|
mpris2-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||||
|
# 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
|
||||||
|
|
@ -19,9 +19,7 @@ tracing.workspace = true
|
||||||
url = "2"
|
url = "2"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
zbus.workspace = true
|
zbus.workspace = true
|
||||||
zlink.workspace = true
|
|
||||||
intmap = "3.1.3"
|
|
||||||
|
|
||||||
[dependencies.cosmic-settings-audio-client]
|
[dependencies.cosmic-settings-sound-subscription]
|
||||||
path = "../../cosmic-settings-daemon/audio-client"
|
git = "https://github.com/pop-os/cosmic-settings"
|
||||||
features = ["codec"]
|
# path = "../../cosmic-settings/subscriptions/sound"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
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 = A PulseAudio-kapcsolat megszakadt
|
disconnected = PulseAudio nincs csatlakozva
|
||||||
no-device = Nincs eszköz kiválasztva
|
no-device = Nincs kiválasztott eszköz
|
||||||
unknown-artist = Ismeretlen
|
unknown-artist = Ismeretlen
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
output = Deran
|
|
||||||
input = Ketan
|
|
||||||
unknown-artist = Nenas
|
|
||||||
sound-settings = Sazkariyên dengê...
|
|
||||||
disconnected = Girêdana PulseAudio qut bû
|
|
||||||
no-device = Tu amûr nehatine hilbijartin
|
|
||||||
show-media-controls = Kontrolên mediyayê li ser destgehê nîşan bide
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
show-media-controls = Прикажи управљања медијима на траци
|
|
||||||
disconnected = Пулс-аудио откачен
|
|
||||||
no-device = Ниједан уређај није изабран
|
|
||||||
input = Улаз
|
|
||||||
output = Излаз
|
|
||||||
unknown-artist = Непознато
|
|
||||||
sound-settings = Подешавања звука...
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
mod localize;
|
mod localize;
|
||||||
mod model;
|
|
||||||
mod mouse_area;
|
mod mouse_area;
|
||||||
|
|
||||||
use crate::localize::localize;
|
use crate::localize::localize;
|
||||||
use config::{AudioAppletConfig, amplification_sink, amplification_source};
|
use config::{AudioAppletConfig, amplification_sink, amplification_source};
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Apply, Element, Renderer, Task, Theme, app,
|
Element, Renderer, Task, Theme, app,
|
||||||
applet::{
|
applet::{
|
||||||
column as applet_column,
|
column as applet_column,
|
||||||
cosmic_panel_config::PanelAnchor,
|
cosmic_panel_config::PanelAnchor,
|
||||||
|
|
@ -19,24 +18,18 @@ use cosmic::{
|
||||||
cosmic_config::CosmicConfigEntry,
|
cosmic_config::CosmicConfigEntry,
|
||||||
cosmic_theme::Spacing,
|
cosmic_theme::Spacing,
|
||||||
iced::{
|
iced::{
|
||||||
self, Alignment, Length, Rectangle, Subscription,
|
self, Alignment, Length, Subscription,
|
||||||
futures::StreamExt,
|
futures::StreamExt,
|
||||||
widget::{self, column, row, slider},
|
widget::{self, column, row, slider},
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
surface, theme,
|
surface, theme,
|
||||||
widget::{
|
widget::{Row, button, container, divider, icon, space, text, toggler},
|
||||||
Row, button, container, divider, icon,
|
|
||||||
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
|
|
||||||
space, text, toggler,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
|
use cosmic_settings_sound_subscription as css;
|
||||||
use futures::SinkExt;
|
|
||||||
use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup};
|
use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup};
|
||||||
use mpris_subscription::{MprisRequest, MprisUpdate};
|
use mpris_subscription::{MprisRequest, MprisUpdate};
|
||||||
use mpris2_zbus::player::PlaybackStatus;
|
use mpris2_zbus::player::PlaybackStatus;
|
||||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod mpris_subscription;
|
mod mpris_subscription;
|
||||||
|
|
@ -57,10 +50,8 @@ pub struct Audio {
|
||||||
core: cosmic::app::Core,
|
core: cosmic::app::Core,
|
||||||
/// Track the applet's popup window.
|
/// Track the applet's popup window.
|
||||||
popup: Option<window::Id>,
|
popup: Option<window::Id>,
|
||||||
/// Varlink connection to `com.system76.CosmicSettings.Audio`.
|
/// The model from cosmic-settings for managing pipewire devices.
|
||||||
audio_client: Option<Rc<RefCell<audio_client::Client>>>,
|
model: css::Model,
|
||||||
/// Known audio device state
|
|
||||||
model: model::Model,
|
|
||||||
/// Whether to expand the revealer of a source or sink device.
|
/// Whether to expand the revealer of a source or sink device.
|
||||||
is_open: IsOpen,
|
is_open: IsOpen,
|
||||||
/// Max slider volume for the sink device, as determined by the amplification property.
|
/// Max slider volume for the sink device, as determined by the amplification property.
|
||||||
|
|
@ -77,14 +68,12 @@ pub struct Audio {
|
||||||
player_status: Option<mpris_subscription::PlayerStatus>,
|
player_status: Option<mpris_subscription::PlayerStatus>,
|
||||||
/// Used to request an activation token for opening cosmic-settings.
|
/// Used to request an activation token for opening cosmic-settings.
|
||||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||||
rectangle_tracker: Option<RectangleTracker<u32>>,
|
|
||||||
rectangle: Option<iced::Rectangle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Audio {
|
impl Audio {
|
||||||
fn output_icon_name(&self) -> &'static str {
|
fn output_icon_name(&self) -> &'static str {
|
||||||
let volume = self.model.active_sink.volume;
|
let volume = self.model.sink_volume;
|
||||||
let mute = self.model.active_sink.mute;
|
let mute = self.model.sink_mute;
|
||||||
if mute || volume == 0 {
|
if mute || volume == 0 {
|
||||||
"audio-volume-muted-symbolic"
|
"audio-volume-muted-symbolic"
|
||||||
} else if volume < 33 {
|
} else if volume < 33 {
|
||||||
|
|
@ -99,8 +88,8 @@ impl Audio {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_icon_name(&self) -> &'static str {
|
fn input_icon_name(&self) -> &'static str {
|
||||||
let volume = self.model.active_source.volume;
|
let volume = self.model.source_volume;
|
||||||
let mute = self.model.active_source.mute;
|
let mute = self.model.source_mute;
|
||||||
if mute || volume == 0 {
|
if mute || volume == 0 {
|
||||||
"microphone-sensitivity-muted-symbolic"
|
"microphone-sensitivity-muted-symbolic"
|
||||||
} else if volume < 33 {
|
} else if volume < 33 {
|
||||||
|
|
@ -111,21 +100,6 @@ impl Audio {
|
||||||
"microphone-sensitivity-high-symbolic"
|
"microphone-sensitivity-high-symbolic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh the volume ceilings and slider breakpoints from the
|
|
||||||
/// amplification config.
|
|
||||||
///
|
|
||||||
/// Must run at startup, not only when the popup opens: the
|
|
||||||
/// scroll-to-adjust handler clamps against `max_sink_volume`, which
|
|
||||||
/// `Default` leaves at 0. Until the popup had been opened once, every
|
|
||||||
/// wheel event therefore resolved to `SetSinkVolume(0)`.
|
|
||||||
fn refresh_volume_limits(&mut self) {
|
|
||||||
(self.max_sink_volume, self.sink_breakpoints) =
|
|
||||||
if amplification_sink() { (150, &[100][..]) } else { (100, &[][..]) };
|
|
||||||
|
|
||||||
(self.max_source_volume, self.source_breakpoints) =
|
|
||||||
if amplification_source() { (150, &[100][..]) } else { (100, &[][..]) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Default)]
|
#[derive(Debug, PartialEq, Eq, Default)]
|
||||||
|
|
@ -136,10 +110,8 @@ enum IsOpen {
|
||||||
Input,
|
Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
/// Connection to `com.system76.CosmicSettings`.
|
|
||||||
Client(Arc<audio_client::Client>),
|
|
||||||
Ignore,
|
Ignore,
|
||||||
SetSinkVolume(u32),
|
SetSinkVolume(u32),
|
||||||
SetSourceVolume(u32),
|
SetSourceVolume(u32),
|
||||||
|
|
@ -157,9 +129,8 @@ pub enum Message {
|
||||||
MprisRequest(MprisRequest),
|
MprisRequest(MprisRequest),
|
||||||
Token(TokenUpdate),
|
Token(TokenUpdate),
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
Subscription(audio_client::Event),
|
Subscription(css::Message),
|
||||||
Surface(surface::Action),
|
Surface(surface::Action),
|
||||||
Rectangle(RectangleUpdate<u32>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|
@ -271,13 +242,19 @@ impl cosmic::Application for Audio {
|
||||||
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
|
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
|
||||||
|
|
||||||
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
|
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
|
||||||
let mut app = Self {
|
let mut model = css::Model::default();
|
||||||
core,
|
model.unplugged_text = "Unplugged".into();
|
||||||
..Default::default()
|
model.hd_audio_text = "HD Audio".into();
|
||||||
};
|
model.usb_audio_text = "USB Audio".into();
|
||||||
app.refresh_volume_limits();
|
|
||||||
|
|
||||||
(app, Task::none())
|
(
|
||||||
|
Self {
|
||||||
|
core,
|
||||||
|
model,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Task::none(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn core(&self) -> &cosmic::app::Core {
|
fn core(&self) -> &cosmic::app::Core {
|
||||||
|
|
@ -294,14 +271,6 @@ impl cosmic::Application for Audio {
|
||||||
|
|
||||||
fn update(&mut self, message: Message) -> app::Task<Message> {
|
fn update(&mut self, message: Message) -> app::Task<Message> {
|
||||||
match message {
|
match message {
|
||||||
Message::Rectangle(u) => match u {
|
|
||||||
RectangleUpdate::Rectangle(r) => {
|
|
||||||
self.rectangle = Some(r.1);
|
|
||||||
}
|
|
||||||
RectangleUpdate::Init(tracker) => {
|
|
||||||
self.rectangle_tracker.replace(tracker);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Message::Ignore => {}
|
Message::Ignore => {}
|
||||||
Message::TogglePopup => {
|
Message::TogglePopup => {
|
||||||
if let Some(p) = self.popup.take() {
|
if let Some(p) = self.popup.take() {
|
||||||
|
|
@ -310,23 +279,26 @@ impl cosmic::Application for Audio {
|
||||||
let new_id = window::Id::unique();
|
let new_id = window::Id::unique();
|
||||||
self.popup.replace(new_id);
|
self.popup.replace(new_id);
|
||||||
|
|
||||||
self.refresh_volume_limits();
|
(self.max_sink_volume, self.sink_breakpoints) = if amplification_sink() {
|
||||||
|
(150, &[100][..])
|
||||||
|
} else {
|
||||||
|
(100, &[][..])
|
||||||
|
};
|
||||||
|
|
||||||
let mut popup_settings = self.core.applet.get_popup_settings(
|
(self.max_source_volume, self.source_breakpoints) = if amplification_source() {
|
||||||
|
(150, &[100][..])
|
||||||
|
} else {
|
||||||
|
(100, &[][..])
|
||||||
|
};
|
||||||
|
|
||||||
|
let popup_settings = self.core.applet.get_popup_settings(
|
||||||
self.core.main_window_id().unwrap(),
|
self.core.main_window_id().unwrap(),
|
||||||
new_id,
|
new_id,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
if let Some(r) = self.rectangle {
|
|
||||||
popup_settings.positioner.anchor_rect = Rectangle {
|
|
||||||
x: r.x as i32,
|
|
||||||
y: r.y as i32,
|
|
||||||
width: r.width as i32,
|
|
||||||
height: r.height as i32,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return get_popup(popup_settings);
|
return get_popup(popup_settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,74 +318,52 @@ impl cosmic::Application for Audio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Subscription(message) => {
|
Message::Subscription(message) => {
|
||||||
self.model.update(message);
|
return self
|
||||||
|
.model
|
||||||
|
.update(message)
|
||||||
|
.map(|message| Message::Subscription(message).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SetDefaultSink(pos) => {
|
Message::SetDefaultSink(pos) => {
|
||||||
if let Some(&pos) = self.model.sinks.sorted_index.get(pos)
|
return self
|
||||||
&& let Some(&node_id) = self.model.sinks.id.get(pos as usize)
|
.model
|
||||||
&& let Some(client) = self.audio_client.as_mut()
|
.set_default_sink(pos)
|
||||||
{
|
.map(|message| Message::Subscription(message).into());
|
||||||
futures::executor::block_on(async {
|
|
||||||
_ = client.borrow_mut().conn.set_default(node_id, true).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SetDefaultSource(pos) => {
|
Message::SetDefaultSource(pos) => {
|
||||||
if let Some(&pos) = self.model.sources.sorted_index.get(pos)
|
return self
|
||||||
&& let Some(&node_id) = self.model.sources.id.get(pos as usize)
|
.model
|
||||||
&& let Some(client) = self.audio_client.as_mut()
|
.set_default_source(pos)
|
||||||
{
|
.map(|message| Message::Subscription(message).into());
|
||||||
futures::executor::block_on(async {
|
|
||||||
_ = client.borrow_mut().conn.set_default(node_id, true).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ToggleSinkMute => {
|
Message::ToggleSinkMute => self.model.toggle_sink_mute(),
|
||||||
if let Some(ref mut client) = self.audio_client {
|
|
||||||
futures::executor::block_on(async {
|
|
||||||
_ = client.borrow_mut().conn.sink_mute_toggle().await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::ToggleSourceMute => {
|
Message::ToggleSourceMute => self.model.toggle_source_mute(),
|
||||||
if let Some(ref mut client) = self.audio_client {
|
|
||||||
futures::executor::block_on(async {
|
|
||||||
_ = client.borrow_mut().conn.source_mute_toggle().await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message::SetSinkVolume(volume) => {
|
Message::SetSinkVolume(volume) => {
|
||||||
if let Some(ref mut client) = self.audio_client {
|
return self
|
||||||
self.model.active_sink.volume = volume;
|
.model
|
||||||
self.model.active_sink.volume_text = volume.to_string();
|
.set_sink_volume(volume)
|
||||||
futures::executor::block_on(async {
|
.map(|message| Message::Subscription(message).into());
|
||||||
_ = client.borrow_mut().conn.set_sink_volume(volume).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::SetSourceVolume(volume) => {
|
Message::SetSourceVolume(volume) => {
|
||||||
if let Some(ref mut client) = self.audio_client {
|
return self
|
||||||
self.model.active_source.volume = volume;
|
.model
|
||||||
self.model.active_source.volume_text = volume.to_string();
|
.set_source_volume(volume)
|
||||||
futures::executor::block_on(async {
|
.map(|message| Message::Subscription(message).into());
|
||||||
_ = client.borrow_mut().conn.set_source_volume(volume).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::ToggleMediaControlsInTopPanel(enabled) => {
|
Message::ToggleMediaControlsInTopPanel(enabled) => {
|
||||||
self.config.show_media_controls_in_top_panel = enabled;
|
self.config.show_media_controls_in_top_panel = enabled;
|
||||||
if let Ok(helper) =
|
if let Ok(helper) =
|
||||||
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
|
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
|
||||||
&& let Err(err) = self.config.write_entry(&helper)
|
|
||||||
{
|
{
|
||||||
tracing::error!(?err, "Error writing config");
|
if let Err(err) = self.config.write_entry(&helper) {
|
||||||
|
tracing::error!(?err, "Error writing config");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::CloseRequested(id) => {
|
Message::CloseRequested(id) => {
|
||||||
|
|
@ -511,12 +461,6 @@ impl cosmic::Application for Audio {
|
||||||
cosmic::app::Action::Surface(a),
|
cosmic::app::Action::Surface(a),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Message::Client(client) => {
|
|
||||||
if let Some(client) = Arc::into_inner(client) {
|
|
||||||
self.audio_client = Some(Rc::new(RefCell::new(client)));
|
|
||||||
self.model = model::Model::default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -532,50 +476,7 @@ impl cosmic::Application for Audio {
|
||||||
}),
|
}),
|
||||||
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
|
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
|
||||||
activation_token_subscription(0).map(Message::Token),
|
activation_token_subscription(0).map(Message::Token),
|
||||||
Subscription::run(|| {
|
Subscription::run(|| css::watch().map(Message::Subscription)),
|
||||||
iced::stream::channel(
|
|
||||||
1,
|
|
||||||
move |mut emitter: futures::channel::mpsc::Sender<_>| async move {
|
|
||||||
loop {
|
|
||||||
let mut client = match audio_client::connect().await {
|
|
||||||
Ok(client) => client,
|
|
||||||
Err(why) => {
|
|
||||||
if let zlink::Error::Io(ref why) = why
|
|
||||||
&& why.kind() == std::io::ErrorKind::NotFound
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"cosmic-settings-daemon varlink service not found."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
tracing::error!(
|
|
||||||
?why,
|
|
||||||
"failed to connect to cosmic-settings's varlink service"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(Ok(mut stream)) = client.recv_events().await {
|
|
||||||
_ = emitter.send(Message::Client(Arc::new(client))).await;
|
|
||||||
while let Some(message) = stream.next().await {
|
|
||||||
match message {
|
|
||||||
Ok(event) => {
|
|
||||||
_ = emitter.send(Message::Subscription(event)).await;
|
|
||||||
}
|
|
||||||
Err(why) => {
|
|
||||||
tracing::error!(?why, "event error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -586,94 +487,63 @@ 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% par cran logique
|
const WHEEL_STEP: f32 = 5.0; // 5% per wheel event
|
||||||
// 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, .. } => {
|
iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1
|
||||||
PIXEL_ACC.with(|a| a.set(0.0));
|
iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_volume = (self.model.active_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.round() as u32)
|
Message::SetSinkVolume(new_volume as u32)
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut has_playback_buttons = false;
|
|
||||||
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| {
|
||||||
// if we have a configure for width and height, we're in a overflow popup
|
// if we have a configure for width and height, we're in a overflow popup
|
||||||
c.width > 0. && c.height > 0.
|
c.width > 0. && c.height > 0.
|
||||||
}))
|
}))
|
||||||
.then(|| self.playback_buttons());
|
.then(|| self.playback_buttons());
|
||||||
|
|
||||||
let mut ret = if let Some(playback_buttons) = playback_buttons
|
self.core
|
||||||
&& !playback_buttons.is_empty()
|
.applet
|
||||||
{
|
.autosize_window(
|
||||||
has_playback_buttons = true;
|
if let Some(playback_buttons) = playback_buttons
|
||||||
Element::from(match self.core.applet.anchor {
|
&& !playback_buttons.is_empty()
|
||||||
PanelAnchor::Left | PanelAnchor::Right => Element::from(
|
{
|
||||||
applet_column::Column::with_children(playback_buttons)
|
match self.core.applet.anchor {
|
||||||
.push(btn)
|
PanelAnchor::Left | PanelAnchor::Right => Element::from(
|
||||||
.align_x(Alignment::Center)
|
applet_column::Column::with_children(playback_buttons)
|
||||||
.height(Length::Shrink)
|
.push(btn)
|
||||||
// TODO configurable variable from the panel?
|
.align_x(Alignment::Center)
|
||||||
.spacing(
|
.height(Length::Shrink)
|
||||||
-(self.core.applet.suggested_padding(true).0 as f32)
|
// TODO configurable variable from the panel?
|
||||||
* self.core.applet.padding_overlap,
|
.spacing(
|
||||||
|
-(self.core.applet.suggested_padding(true).0 as f32)
|
||||||
|
* self.core.applet.padding_overlap,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
PanelAnchor::Top | PanelAnchor::Bottom => {
|
||||||
PanelAnchor::Top | PanelAnchor::Bottom => {
|
applet_row::Row::with_children(playback_buttons)
|
||||||
applet_row::Row::with_children(playback_buttons)
|
.push(btn)
|
||||||
.push(btn)
|
.align_y(Alignment::Center)
|
||||||
.align_y(Alignment::Center)
|
.width(Length::Shrink)
|
||||||
.width(Length::Shrink)
|
// TODO configurable variable from the panel?
|
||||||
// TODO configurable variable from the panel?
|
.spacing(
|
||||||
.spacing(
|
-(self.core.applet.suggested_padding(true).0 as f32)
|
||||||
-(self.core.applet.suggested_padding(true).0 as f32)
|
* self.core.applet.padding_overlap,
|
||||||
* self.core.applet.padding_overlap,
|
)
|
||||||
)
|
.into()
|
||||||
.into()
|
}
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
} else {
|
btn.into()
|
||||||
btn.into()
|
},
|
||||||
};
|
)
|
||||||
|
.into()
|
||||||
if let Some(tracker) = self.rectangle_tracker.as_ref()
|
|
||||||
&& has_playback_buttons
|
|
||||||
{
|
|
||||||
ret = tracker.container(0, ret).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
|
|
||||||
// if we have a configure for width and height, we're in a overflow popup
|
|
||||||
c.width > 0. && c.height > 0.
|
|
||||||
}) {
|
|
||||||
ret = self.core.applet.autosize_window(ret).into();
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_window(&self, _id: window::Id) -> Element<'_, Message> {
|
fn view_window(&self, _id: window::Id) -> Element<'_, Message> {
|
||||||
|
|
@ -681,21 +551,19 @@ impl cosmic::Application for Audio {
|
||||||
space_xxs, space_s, ..
|
space_xxs, space_s, ..
|
||||||
} = theme::active().cosmic().spacing;
|
} = theme::active().cosmic().spacing;
|
||||||
|
|
||||||
let sink: Option<&str> = self
|
let sink = self
|
||||||
.model
|
.model
|
||||||
.sinks
|
.active_sink()
|
||||||
.active()
|
.and_then(|pos| self.model.sinks().get(pos));
|
||||||
.map(|pos| self.model.sinks.sorted_display[pos].as_ref());
|
let source = self
|
||||||
let source: Option<&str> = self
|
|
||||||
.model
|
.model
|
||||||
.sources
|
.active_source()
|
||||||
.active()
|
.and_then(|pos| self.model.sources().get(pos));
|
||||||
.map(|pos| self.model.sources.sorted_display[pos].as_ref());
|
|
||||||
|
|
||||||
let mut audio_content = {
|
let mut audio_content = {
|
||||||
let output_slider = slider(
|
let output_slider = slider(
|
||||||
0..=self.max_sink_volume,
|
0..=self.max_sink_volume,
|
||||||
self.model.active_sink.volume,
|
self.model.sink_volume,
|
||||||
Message::SetSinkVolume,
|
Message::SetSinkVolume,
|
||||||
)
|
)
|
||||||
.width(Length::FillPortion(5))
|
.width(Length::FillPortion(5))
|
||||||
|
|
@ -703,7 +571,7 @@ impl cosmic::Application for Audio {
|
||||||
|
|
||||||
let input_slider = slider(
|
let input_slider = slider(
|
||||||
0..=self.max_source_volume,
|
0..=self.max_source_volume,
|
||||||
self.model.active_source.volume,
|
self.model.source_volume,
|
||||||
Message::SetSourceVolume,
|
Message::SetSourceVolume,
|
||||||
)
|
)
|
||||||
.width(Length::FillPortion(5))
|
.width(Length::FillPortion(5))
|
||||||
|
|
@ -722,7 +590,7 @@ impl cosmic::Application for Audio {
|
||||||
.line_height(24)
|
.line_height(24)
|
||||||
.on_press(Message::ToggleSinkMute),
|
.on_press(Message::ToggleSinkMute),
|
||||||
output_slider,
|
output_slider,
|
||||||
container(text(&self.model.active_sink.volume_text).size(16))
|
container(text(&self.model.sink_volume_text).size(16))
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.align_x(Alignment::End)
|
.align_x(Alignment::End)
|
||||||
]
|
]
|
||||||
|
|
@ -741,7 +609,7 @@ impl cosmic::Application for Audio {
|
||||||
.line_height(24)
|
.line_height(24)
|
||||||
.on_press(Message::ToggleSourceMute),
|
.on_press(Message::ToggleSourceMute),
|
||||||
input_slider,
|
input_slider,
|
||||||
container(text(&self.model.active_source.volume_text).size(16))
|
container(text(&self.model.source_volume_text).size(16))
|
||||||
.width(Length::FillPortion(1))
|
.width(Length::FillPortion(1))
|
||||||
.align_x(Alignment::End)
|
.align_x(Alignment::End)
|
||||||
]
|
]
|
||||||
|
|
@ -756,7 +624,7 @@ impl cosmic::Application for Audio {
|
||||||
Some(sink) => sink.to_owned(),
|
Some(sink) => sink.to_owned(),
|
||||||
None => fl!("no-device"),
|
None => fl!("no-device"),
|
||||||
},
|
},
|
||||||
&self.model.sinks.sorted_display,
|
self.model.sinks(),
|
||||||
Message::OutputToggle,
|
Message::OutputToggle,
|
||||||
Message::SetDefaultSink,
|
Message::SetDefaultSink,
|
||||||
),
|
),
|
||||||
|
|
@ -767,7 +635,7 @@ impl cosmic::Application for Audio {
|
||||||
Some(source) => source.to_owned(),
|
Some(source) => source.to_owned(),
|
||||||
None => fl!("no-device"),
|
None => fl!("no-device"),
|
||||||
},
|
},
|
||||||
&self.model.sources.sorted_display,
|
self.model.sources(),
|
||||||
Message::InputToggle,
|
Message::InputToggle,
|
||||||
Message::SetDefaultSource,
|
Message::SetDefaultSource,
|
||||||
)
|
)
|
||||||
|
|
@ -823,15 +691,19 @@ impl cosmic::Application for Audio {
|
||||||
}
|
}
|
||||||
if let Some(play) = self.is_play() {
|
if let Some(play) = self.is_play() {
|
||||||
control_elements.push(
|
control_elements.push(
|
||||||
button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true))
|
button::icon(
|
||||||
.extra_small()
|
icon::from_name(if play { PLAY } else { PAUSE })
|
||||||
.class(cosmic::theme::Button::AppletIcon)
|
.size(32)
|
||||||
.on_press(if play {
|
.symbolic(true),
|
||||||
Message::MprisRequest(MprisRequest::Play)
|
)
|
||||||
} else {
|
.extra_small()
|
||||||
Message::MprisRequest(MprisRequest::Pause)
|
.class(cosmic::theme::Button::AppletIcon)
|
||||||
})
|
.on_press(if play {
|
||||||
.into(),
|
Message::MprisRequest(MprisRequest::Play)
|
||||||
|
} else {
|
||||||
|
Message::MprisRequest(MprisRequest::Pause)
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(go_next) = self.go_next(32) {
|
if let Some(go_next) = self.go_next(32) {
|
||||||
|
|
@ -882,21 +754,20 @@ impl cosmic::Application for Audio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn revealer<'a>(
|
fn revealer(
|
||||||
open: bool,
|
open: bool,
|
||||||
title: String,
|
title: String,
|
||||||
selected: String,
|
selected: String,
|
||||||
devices: &'a [Arc<str>],
|
devices: &[String],
|
||||||
toggle: Message,
|
toggle: Message,
|
||||||
mut change: impl FnMut(usize) -> Message + 'static,
|
mut change: impl FnMut(usize) -> Message + 'static,
|
||||||
) -> widget::Column<'a, Message, crate::Theme, Renderer> {
|
) -> widget::Column<'static, Message, crate::Theme, Renderer> {
|
||||||
if open {
|
if open {
|
||||||
devices.iter().enumerate().fold(
|
devices.iter().cloned().enumerate().fold(
|
||||||
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|
||||||
move |col, (id, name)| {
|
|col, (id, name)| {
|
||||||
col.push(
|
col.push(
|
||||||
text::body(name.as_ref())
|
menu_button(text::body(name))
|
||||||
.apply(menu_button)
|
|
||||||
.on_press(change(id))
|
.on_press(change(id))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.padding([8, 48]),
|
.padding([8, 48]),
|
||||||
|
|
@ -914,9 +785,9 @@ fn revealer_head(
|
||||||
selected: String,
|
selected: String,
|
||||||
toggle: Message,
|
toggle: Message,
|
||||||
) -> cosmic::widget::Button<'static, Message> {
|
) -> cosmic::widget::Button<'static, Message> {
|
||||||
cosmic::widget::column::with_capacity(2)
|
menu_button(column![
|
||||||
.push(text::body(title).width(Length::Fill))
|
text::body(title).width(Length::Fill),
|
||||||
.push(text::caption(selected))
|
text::caption(selected),
|
||||||
.apply(menu_button)
|
])
|
||||||
.on_press(toggle)
|
.on_press(toggle)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,393 +0,0 @@
|
||||||
// Copyright 2026 System76 <info@system76.com>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use cosmic_settings_audio_client::{self as audio_client, Availability, RouteInfo};
|
|
||||||
use intmap::IntMap;
|
|
||||||
|
|
||||||
pub type DeviceId = u32;
|
|
||||||
pub type NodeId = u32;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Model {
|
|
||||||
pub device_routes: IntMap<DeviceId, Vec<RouteInfo>>,
|
|
||||||
pub node_devices: IntMap<NodeId, Option<u32>>,
|
|
||||||
pub sinks: Nodes,
|
|
||||||
pub sources: Nodes,
|
|
||||||
pub active_sink: ActiveNode,
|
|
||||||
pub active_source: ActiveNode,
|
|
||||||
pub default_sink: Option<NodeId>,
|
|
||||||
pub default_source: Option<NodeId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Nodes {
|
|
||||||
active: Option<usize>,
|
|
||||||
pub sorted_display: Box<[Arc<str>]>,
|
|
||||||
pub sorted_index: Box<[u16]>,
|
|
||||||
pub balance: Vec<Option<f32>>,
|
|
||||||
pub card_profile_device: Vec<Option<u32>>,
|
|
||||||
pub description: Vec<String>,
|
|
||||||
pub devices: Vec<Option<NodeId>>,
|
|
||||||
pub display: Vec<Arc<str>>,
|
|
||||||
pub mute: Vec<bool>,
|
|
||||||
pub name: Vec<String>,
|
|
||||||
pub id: Vec<NodeId>,
|
|
||||||
pub volume: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Nodes {
|
|
||||||
pub fn active(&self) -> Option<usize> {
|
|
||||||
self.active.and_then(|active| {
|
|
||||||
self.sorted_index
|
|
||||||
.iter()
|
|
||||||
.position(|idx| *idx as usize == active)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dropdown_sort(&mut self) {
|
|
||||||
let mut enumerated_displays = self
|
|
||||||
.display
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, display)| (index as u16, display))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
enumerated_displays.sort_by_key(|v| v.1.clone());
|
|
||||||
let (indexes, displays): (Vec<_>, Vec<_>) = enumerated_displays.into_iter().unzip();
|
|
||||||
self.sorted_display = displays.into_boxed_slice();
|
|
||||||
self.sorted_index = indexes.into_boxed_slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, node_id: u32) -> bool {
|
|
||||||
let Some(pos) = self.id.iter().position(|id| node_id == *id) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
self.balance.remove(pos);
|
|
||||||
self.card_profile_device.remove(pos);
|
|
||||||
self.description.remove(pos);
|
|
||||||
self.devices.remove(pos);
|
|
||||||
self.display.remove(pos);
|
|
||||||
self.mute.remove(pos);
|
|
||||||
self.name.remove(pos);
|
|
||||||
self.id.remove(pos);
|
|
||||||
self.volume.remove(pos);
|
|
||||||
self.dropdown_sort();
|
|
||||||
if self.active == Some(pos) {
|
|
||||||
self.active = None;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct ActiveNode {
|
|
||||||
pub volume_text: String,
|
|
||||||
pub volume: u32,
|
|
||||||
pub mute: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model {
|
|
||||||
pub fn update(&mut self, event: audio_client::Event) {
|
|
||||||
tracing::debug!(?event, "update");
|
|
||||||
match event {
|
|
||||||
audio_client::Event::NodeMute(node_id, mute) => {
|
|
||||||
if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
|
|
||||||
self.sinks.mute[pos] = mute;
|
|
||||||
if self.sinks.active == Some(pos) {
|
|
||||||
self.active_sink.mute = mute;
|
|
||||||
}
|
|
||||||
} else if let Some(pos) = self.sources.id.iter().position(|id| node_id == *id) {
|
|
||||||
self.sources.mute[pos] = mute;
|
|
||||||
if self.sources.active == Some(pos) {
|
|
||||||
self.active_source.mute = mute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::NodeVolume(node_id, volume, balance) => {
|
|
||||||
if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
|
|
||||||
self.sinks.volume[pos] = volume;
|
|
||||||
self.sinks.balance[pos] = balance;
|
|
||||||
if self.default_sink.as_ref().is_some_and(|&id| id == node_id)
|
|
||||||
&& let Some(pos) = self.sinks.active
|
|
||||||
{
|
|
||||||
self.active_sink.mute = self.sinks.mute[pos];
|
|
||||||
self.active_sink.volume = self.sinks.volume[pos];
|
|
||||||
self.active_sink.volume_text = self.active_sink.volume.to_string();
|
|
||||||
}
|
|
||||||
} else if let Some(pos) = self.sources.id.iter().position(|id| node_id == *id) {
|
|
||||||
self.sources.volume[pos] = volume;
|
|
||||||
self.sources.balance[pos] = balance;
|
|
||||||
if self
|
|
||||||
.default_source
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|&id| id == node_id)
|
|
||||||
&& let Some(pos) = self.sources.active
|
|
||||||
{
|
|
||||||
self.active_source.mute = self.sources.mute[pos];
|
|
||||||
self.active_source.volume = self.sources.volume[pos];
|
|
||||||
self.active_source.volume_text = self.active_source.volume.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::DefaultSink(node_id) => {
|
|
||||||
self.default_sink = Some(node_id);
|
|
||||||
if let Some(pos) = self.sinks.id.iter().position(|&id| id == node_id) {
|
|
||||||
self.sinks.active = Some(pos);
|
|
||||||
self.active_sink.mute = self.sinks.mute[pos];
|
|
||||||
self.active_sink.volume = self.sinks.volume[pos];
|
|
||||||
self.active_sink.volume_text = self.active_sink.volume.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::DefaultSource(node_id) => {
|
|
||||||
self.default_source = Some(node_id);
|
|
||||||
if let Some(pos) = self.sources.id.iter().position(|&id| id == node_id) {
|
|
||||||
self.sources.active = Some(pos);
|
|
||||||
self.active_source.mute = self.sources.mute[pos];
|
|
||||||
self.active_source.volume = self.sources.volume[pos];
|
|
||||||
self.active_source.volume_text = self.active_source.volume.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::Node(node_id, node) => {
|
|
||||||
self.node_devices.insert(node_id, node.device_id);
|
|
||||||
if node.is_sink {
|
|
||||||
let pos = if let Some(pos) = self.sinks.id.iter().position(|&id| id == node_id)
|
|
||||||
{
|
|
||||||
self.sinks.description[pos] = self.translate(&node.description);
|
|
||||||
self.sinks.name[pos] = node.name;
|
|
||||||
self.sinks.card_profile_device[pos] = node.card_profile_device;
|
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
self.sinks.display.push(Arc::default());
|
|
||||||
self.sinks
|
|
||||||
.description
|
|
||||||
.push(self.translate(&node.description));
|
|
||||||
self.sinks.id.push(node_id);
|
|
||||||
self.sinks.volume.push(0);
|
|
||||||
self.sinks.balance.push(None);
|
|
||||||
self.sinks.mute.push(false);
|
|
||||||
self.sinks.name.push(node.name);
|
|
||||||
self.sinks.devices.push(node.device_id);
|
|
||||||
self.sinks
|
|
||||||
.card_profile_device
|
|
||||||
.push(node.card_profile_device);
|
|
||||||
self.sinks.id.len() - 1
|
|
||||||
};
|
|
||||||
|
|
||||||
self.sinks.display[pos] = node
|
|
||||||
.device_id
|
|
||||||
.zip(node.card_profile_device)
|
|
||||||
.and_then(|(device_id, node_card_profile_device)| {
|
|
||||||
let routes = self.device_routes.get(device_id)?;
|
|
||||||
for route in routes {
|
|
||||||
if matches!(route.availability, Availability::No) || !route.is_sink
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.devices.contains(&node_card_profile_device) {
|
|
||||||
return Some(node_name(
|
|
||||||
&self.translate(&route.description),
|
|
||||||
&self.sinks.description[pos],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
node_name(
|
|
||||||
&node.device_profile_description,
|
|
||||||
&self.sinks.description[pos],
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.sinks.dropdown_sort();
|
|
||||||
|
|
||||||
if let Some(default_node_id) = self.default_sink
|
|
||||||
&& default_node_id == node_id
|
|
||||||
{
|
|
||||||
self.sinks.active = Some(pos);
|
|
||||||
self.active_sink.mute = self.sinks.mute[pos];
|
|
||||||
self.active_sink.volume = self.sinks.volume[pos];
|
|
||||||
self.active_sink.volume_text = self.active_sink.volume.to_string();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let pos =
|
|
||||||
if let Some(pos) = self.sources.id.iter().position(|&id| id == node_id) {
|
|
||||||
self.sources.description[pos] = self.translate(&node.description);
|
|
||||||
self.sources.name[pos] = node.name;
|
|
||||||
self.sources.card_profile_device[pos] = node.card_profile_device;
|
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
self.sources
|
|
||||||
.description
|
|
||||||
.push(self.translate(&node.description));
|
|
||||||
self.sources.display.push(Arc::default());
|
|
||||||
self.sources.id.push(node_id);
|
|
||||||
self.sources.volume.push(0);
|
|
||||||
self.sources.balance.push(None);
|
|
||||||
self.sources.mute.push(false);
|
|
||||||
self.sources.name.push(node.name);
|
|
||||||
self.sources.devices.push(node.device_id);
|
|
||||||
self.sources
|
|
||||||
.card_profile_device
|
|
||||||
.push(node.card_profile_device);
|
|
||||||
self.sources.id.len() - 1
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(name) = node
|
|
||||||
.device_id
|
|
||||||
.zip(node.card_profile_device)
|
|
||||||
.map(|(device_id, node_card_profile_device)| {
|
|
||||||
let routes = self.device_routes.get(device_id)?;
|
|
||||||
for route in routes {
|
|
||||||
if route.is_sink || matches!(route.availability, Availability::No) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.devices.contains(&node_card_profile_device) {
|
|
||||||
return Some(node_name(
|
|
||||||
&self.translate(&route.description),
|
|
||||||
&self.sources.description[pos],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
Some(node_name(
|
|
||||||
&node.device_profile_description,
|
|
||||||
&self.sources.description[pos],
|
|
||||||
))
|
|
||||||
})
|
|
||||||
{
|
|
||||||
self.sources.display[pos] = name;
|
|
||||||
self.sources.dropdown_sort();
|
|
||||||
} else {
|
|
||||||
// Remove sources that are unplugged.
|
|
||||||
self.sources.remove(node_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(default_node_id) = self.default_source
|
|
||||||
&& default_node_id == node_id
|
|
||||||
{
|
|
||||||
self.sources.active = Some(pos);
|
|
||||||
self.active_source.mute = self.sources.mute[pos];
|
|
||||||
self.active_source.volume = self.sources.volume[pos];
|
|
||||||
self.active_source.volume_text = self.active_source.volume.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::ActiveRoute(device_id, _index, route) => {
|
|
||||||
self.update_device_names(device_id, &route);
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::Route(device_id, index, route) => {
|
|
||||||
let routes = self.device_routes.entry(device_id).or_default();
|
|
||||||
if index == 0 {
|
|
||||||
*routes = vec![route];
|
|
||||||
} else {
|
|
||||||
if routes.len() < index as usize + 1 {
|
|
||||||
let additional = (index as usize + 1) - routes.capacity();
|
|
||||||
routes.reserve_exact(additional);
|
|
||||||
routes.extend(std::iter::repeat_n(RouteInfo::default(), additional));
|
|
||||||
}
|
|
||||||
routes[index as usize] = route;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::RemoveNode(node_id) => {
|
|
||||||
self.node_devices.remove(node_id);
|
|
||||||
|
|
||||||
if !self.sinks.remove(node_id) {
|
|
||||||
self.sources.remove(node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_client::Event::RemoveDevice(device_id) => {
|
|
||||||
self.device_routes.remove(device_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_device_names(&mut self, device_id: DeviceId, route: &RouteInfo) {
|
|
||||||
if matches!(route.availability, Availability::No) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let compatible_nodes = self.node_devices.iter().filter_map(|(node, &dev_id)| {
|
|
||||||
if dev_id? == device_id {
|
|
||||||
Some(node)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if route.is_sink {
|
|
||||||
for n_id in compatible_nodes {
|
|
||||||
let Some(pos) = self.sinks.id.iter().position(|&node| node == n_id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(card_profile_device) = self.sinks.card_profile_device[pos] else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if route.devices.contains(&card_profile_device) {
|
|
||||||
self.sinks.display[pos] = node_name(
|
|
||||||
&self.translate(&route.description),
|
|
||||||
&self.sinks.description[pos],
|
|
||||||
);
|
|
||||||
self.sinks.dropdown_sort();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for n_id in compatible_nodes {
|
|
||||||
let Some(pos) = self.sources.id.iter().position(|&node| node == n_id) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(card_profile_device) = self.sources.card_profile_device[pos] else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if route.devices.contains(&card_profile_device) {
|
|
||||||
self.sources.display[pos] = node_name(
|
|
||||||
&self.translate(&route.description),
|
|
||||||
&self.sources.description[pos],
|
|
||||||
);
|
|
||||||
self.sources.dropdown_sort();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn translate(&self, description: &str) -> String {
|
|
||||||
description
|
|
||||||
.replace("High Definition", "HD")
|
|
||||||
.replace("DisplayPort", "DP")
|
|
||||||
.replace("Controller", "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn node_name(route: &str, node: &str) -> Arc<str> {
|
|
||||||
if route.is_empty() {
|
|
||||||
node.to_owned()
|
|
||||||
} else {
|
|
||||||
[route, " - ", node].concat()
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
@ -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::Event,
|
event::{self, Event},
|
||||||
layout, mouse, overlay, renderer, touch,
|
layout, mouse, overlay, renderer, touch,
|
||||||
widget::{Operation, Tree, tree},
|
widget::{Operation, Tree, tree},
|
||||||
};
|
};
|
||||||
|
|
@ -307,19 +307,6 @@ fn update<Message: Clone, Theme, Renderer>(
|
||||||
shell: &mut Shell<'_, Message>,
|
shell: &mut Shell<'_, Message>,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
) {
|
) {
|
||||||
// Handle wheel events before the bounds check. The panel's nested
|
|
||||||
// compositor routes axis events to this surface only when the pointer
|
|
||||||
// is over the applet, but the cursor coordinates it sends may be in
|
|
||||||
// panel-local space rather than surface-local space. This makes
|
|
||||||
// cursor.is_over() unreliable for scroll events.
|
|
||||||
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
|
|
||||||
if let Some(message) = widget.on_mouse_wheel.as_ref() {
|
|
||||||
shell.publish((message)(*delta));
|
|
||||||
shell.capture_event();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cursor.is_over(layout.bounds()) {
|
if !cursor.is_over(layout.bounds()) {
|
||||||
if !state.is_out_of_bounds {
|
if !state.is_out_of_bounds {
|
||||||
if widget
|
if widget
|
||||||
|
|
@ -429,4 +416,12 @@ fn update<Message: Clone, Theme, Renderer>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(message) = widget.on_mouse_wheel.as_ref() {
|
||||||
|
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
|
||||||
|
shell.publish((message)(*delta));
|
||||||
|
shell.capture_event();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-battery"
|
name = "cosmic-applet-battery"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
cosmic.workspace = true
|
libcosmic.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,7 +24,9 @@ zbus.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
[dependencies.cosmic-settings-upower-subscription]
|
[dependencies.cosmic-settings-upower-subscription]
|
||||||
path = "../../cosmic-settings/subscriptions/upower"
|
git = "https://github.com/pop-os/cosmic-settings"
|
||||||
|
# path = "../../cosmic-settings/subscriptions/upower"
|
||||||
|
|
||||||
[dependencies.cosmic-settings-daemon-subscription]
|
[dependencies.cosmic-settings-daemon-subscription]
|
||||||
path = "../../cosmic-settings/subscriptions/settings-daemon"
|
git = "https://github.com/pop-os/cosmic-settings"
|
||||||
|
# path = "../../cosmic-settings/subscriptions/settings-daemon"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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ítsd a maximális töltési szintet 80%-ra
|
max-charge = Az akkumulátor élettartamának növelése érdekében állítsa a maximális töltési szintet 80%-ra
|
||||||
seconds = mp
|
seconds = másodperc
|
||||||
minutes = p
|
minutes = perc
|
||||||
hours = ó
|
hours = óra
|
||||||
until-empty = a lemerülésig
|
until-empty = a lemerülésig
|
||||||
power-settings = Energia- és akkumulátorbeállítások…
|
power-settings = Energia- és akkumulátorbeállítások…
|
||||||
dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát
|
dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
battery = Betarî
|
|
||||||
seconds = ç
|
|
||||||
minutes = x
|
|
||||||
hours = d
|
|
||||||
until-empty = Heya vala dibe
|
|
||||||
power-settings = Sazkariyên hêz û betariyê...
|
|
||||||
battery-desc = Bikaranîna hêzê kêmkirî û performans.
|
|
||||||
balanced = Hevseng
|
|
||||||
balanced-desc = Performansa standard û bikaranîna betariyê.
|
|
||||||
performance = Performansa bilind
|
|
||||||
performance-desc = Performansa bilind û bikaranîna hêzê.
|
|
||||||
max-charge = Jiyana betariyê xwe dirêj bike bi riya danîna sînorê herî mezin yê tijîkrinê %80
|
|
||||||
dgpu-running = YKG a veqetandî çalak e û dikare jiyana betariyê kêm bike
|
|
||||||
dgpu-applications = Sepanên ku YKG a veqetandî ya { $gpu_name } bi kar tînin
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
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, time::Duration};
|
use std::{path::PathBuf, sync::LazyLock, time::Duration};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
// XXX improve
|
// XXX improve
|
||||||
|
|
@ -169,24 +169,9 @@ impl CosmicBatteryApplet {
|
||||||
self.charging_limit = Some(limit);
|
self.charging_limit = Some(limit);
|
||||||
self.update_battery(self.battery_percent, self.on_battery);
|
self.update_battery(self.battery_percent, self.on_battery);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn panel_icon_name(&self) -> &str {
|
|
||||||
if self.no_battery {
|
|
||||||
if self.screen_brightness.is_some() {
|
|
||||||
self.display_icon_name.as_str()
|
|
||||||
} else if self.kbd_brightness.is_some() {
|
|
||||||
"keyboard-brightness-symbolic"
|
|
||||||
} else {
|
|
||||||
"plugged-into-power-symbolic"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.icon_name.as_str()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
@ -383,10 +368,6 @@ 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| {
|
||||||
|
|
@ -526,7 +507,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,
|
||||||
|
|
@ -536,9 +517,19 @@ impl cosmic::Application for CosmicBatteryApplet {
|
||||||
let applet_padding = self.core.applet.suggested_padding(true);
|
let applet_padding = self.core.applet.suggested_padding(true);
|
||||||
|
|
||||||
let mut children = vec![
|
let mut children = vec![
|
||||||
icon::from_name(self.panel_icon_name())
|
icon::from_name(if self.no_battery {
|
||||||
.size(suggested_size.0)
|
if self.screen_brightness.is_some() {
|
||||||
.into(),
|
self.display_icon_name.as_str()
|
||||||
|
} else if self.kbd_brightness.is_some() {
|
||||||
|
"keyboard-brightness-symbolic"
|
||||||
|
} else {
|
||||||
|
self.icon_name.as_str()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.icon_name.as_str()
|
||||||
|
})
|
||||||
|
.size(suggested_size.0)
|
||||||
|
.into(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if self.config.show_percentage && !self.no_battery {
|
if self.config.show_percentage && !self.no_battery {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-bluetooth"
|
name = "cosmic-applet-bluetooth"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
cosmic.workspace = true
|
libcosmic.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
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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,12 +1,2 @@
|
||||||
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 = Kapcsolódva
|
connected = Csatlakoztatva
|
||||||
confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal
|
confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal
|
||||||
confirm = Megerősítés
|
confirm = Megerősítés
|
||||||
cancel = Mégse
|
cancel = Mégse
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
cancel = Têk bibe
|
|
||||||
bluetooth = Bluetooth
|
|
||||||
connected = Hat girêdan
|
|
||||||
other-devices = Amûrên Bluetooth ên din
|
|
||||||
settings = Sazkariyên Bluetooth...
|
|
||||||
confirm-pin = Tika ye piştrast bike ku PIN a jêrîn bi ya ku li ser { $deviceName } tê xuyakirin re li hev tê
|
|
||||||
confirm = Bipejirîne
|
|
||||||
check-device = Piştrast bike ku { $deviceName } çalak e, di hundir rêjeyê de ye, û amade ye ji bo cotkirinê.
|
|
||||||
try-again = Dîsa hewl bide
|
|
||||||
discoverable = Dîtbar
|
|
||||||
pairable = Cotbar
|
|
||||||
unsuccessful = Cotkirin têkçûyî ye
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
confirm-pin = Потврдите да се овај ПИН подудара са оним приказаним на { $deviceName }
|
|
||||||
bluetooth = Блутут
|
|
||||||
unsuccessful = Неуспешно упаривање
|
|
||||||
pairable = Упарив
|
|
||||||
try-again = Покушај поново
|
|
||||||
check-device = Постарајте се да је { $deviceName } укључен, у домету и спреман за упаривање.
|
|
||||||
other-devices = Други блутут уређаји
|
|
||||||
cancel = Откажи
|
|
||||||
connected = Повезано
|
|
||||||
confirm = Потврди
|
|
||||||
settings = Подешавања блутута...
|
|
||||||
discoverable = Откривљив
|
|
||||||
|
|
@ -21,10 +21,10 @@ use cosmic::{
|
||||||
widget::{Column, column, container, row},
|
widget::{Column, column, container, row},
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
widget::{button, divider, icon, indeterminate_circular, scrollable, text},
|
widget::{button, divider, icon, scrollable, text},
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::HashMap, sync::LazyLock, time::Duration};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -63,7 +63,6 @@ impl CosmicBluetoothApplet {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
enum Message {
|
enum Message {
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
CloseRequested(window::Id),
|
CloseRequested(window::Id),
|
||||||
|
|
@ -146,7 +145,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,
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -368,12 +367,14 @@ impl cosmic::Application for CosmicBluetoothApplet {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
BluerDeviceStatus::Paired => {}
|
BluerDeviceStatus::Paired => {}
|
||||||
BluerDeviceStatus::Connecting
|
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => {
|
||||||
| BluerDeviceStatus::Disconnecting
|
row = row.push(
|
||||||
| BluerDeviceStatus::Pairing => {
|
icon::from_name("process-working-symbolic")
|
||||||
row = row.push(indeterminate_circular().size(24.0));
|
.size(24)
|
||||||
|
.symbolic(true),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
BluerDeviceStatus::Disconnected => continue,
|
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
known_bluetooth.push(
|
known_bluetooth.push(
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,8 @@ 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;
|
||||||
|
|
@ -197,7 +199,6 @@ pub enum BluerRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum BluerEvent {
|
pub enum BluerEvent {
|
||||||
RequestResponse {
|
RequestResponse {
|
||||||
req: BluerRequest,
|
req: BluerRequest,
|
||||||
|
|
@ -567,21 +568,22 @@ impl BluerSessionState {
|
||||||
tick(&mut interval).await;
|
tick(&mut interval).await;
|
||||||
let new_status = adapter_clone.is_powered().await.unwrap_or_default();
|
let new_status = adapter_clone.is_powered().await.unwrap_or_default();
|
||||||
devices = build_device_list(devices, &adapter_clone).await;
|
devices = build_device_list(devices, &adapter_clone).await;
|
||||||
let power_changed = new_status != status;
|
if new_status != status {
|
||||||
status = new_status;
|
status = new_status;
|
||||||
let state = BluerState {
|
let state = BluerState {
|
||||||
devices: devices.clone(),
|
devices: devices.clone(),
|
||||||
bluetooth_enabled: status,
|
bluetooth_enabled: status,
|
||||||
};
|
};
|
||||||
if power_changed && state.bluetooth_enabled {
|
if state.bluetooth_enabled {
|
||||||
for d in &state.devices {
|
for d in &state.devices {
|
||||||
if d.paired_and_trusted() {
|
if d.paired_and_trusted() {
|
||||||
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
|
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = wake_up_discover_tx.send(()).await;
|
_ = wake_up_discover_tx.send(()).await;
|
||||||
|
let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await;
|
||||||
}
|
}
|
||||||
let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -672,9 +674,6 @@ impl BluerSessionState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always refresh power state from the adapter
|
|
||||||
// to avoid stale cached is_powered values
|
|
||||||
is_powered = adapter_clone.is_powered().await.unwrap_or_default();
|
|
||||||
let _ = tx
|
let _ = tx
|
||||||
.send(BluerSessionEvent::ChangesProcessed(BluerState {
|
.send(BluerSessionEvent::ChangesProcessed(BluerState {
|
||||||
devices: devices.clone(),
|
devices: devices.clone(),
|
||||||
|
|
@ -854,7 +853,13 @@ async fn bluer_state(adapter: &Adapter) -> BluerState {
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> {
|
async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> {
|
||||||
let addrs = adapter.device_addresses().await.unwrap_or_default();
|
let addrs: Vec<Address> = adapter
|
||||||
|
.device_addresses()
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|addr| !devices.iter().any(|d| d.address == *addr))
|
||||||
|
.collect();
|
||||||
|
|
||||||
devices.clear();
|
devices.clear();
|
||||||
if addrs.len() > devices.capacity() {
|
if addrs.len() > devices.capacity() {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-input-sources"
|
name = "cosmic-applet-input-sources"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmic-comp-config.workspace = true
|
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
|
||||||
i18n-embed-fl.workspace = true
|
i18n-embed-fl.workspace = true
|
||||||
i18n-embed.workspace = true
|
i18n-embed.workspace = true
|
||||||
cosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
show-keyboard-layout = Εμφάνιση διάταξης πληκτρολογίου...
|
|
||||||
keyboard-settings = Ρυθμίσεις πληκτρολογίου...
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
show-keyboard-layout = Awaya kilîtdankê nîşan bide...
|
|
||||||
keyboard-settings = Sazkariyên kilîtdankê...
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
show-keyboard-layout = Прикажи распоред тастатуре...
|
|
||||||
keyboard-settings = Подешавања тастатуре...
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
mod localize;
|
mod localize;
|
||||||
|
|
||||||
|
use cosmic::iced::{Alignment, Length};
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
app,
|
app,
|
||||||
app::Core,
|
app::Core,
|
||||||
|
|
@ -13,14 +14,16 @@ 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,
|
iced::{core::window, runtime::Appearance},
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-minimize"
|
name = "cosmic-applet-minimize"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -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 }
|
||||||
cosmic.workspace = true
|
libcosmic.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(
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,6 @@ struct AppData {
|
||||||
toplevel_info_state: ToplevelInfoState,
|
toplevel_info_state: ToplevelInfoState,
|
||||||
toplevel_manager_state: ToplevelManagerState,
|
toplevel_manager_state: ToplevelManagerState,
|
||||||
seat_state: SeatState,
|
seat_state: SeatState,
|
||||||
captured_toplevels: std::collections::HashSet<ExtForeignToplevelHandleV1>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CaptureData {
|
struct CaptureData {
|
||||||
|
|
@ -352,36 +351,6 @@ impl AppData {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_toplevel(&mut self, toplevel: &ExtForeignToplevelHandleV1, is_new: bool) {
|
|
||||||
let Some(info) = self.toplevel_info_state.info(toplevel) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if info
|
|
||||||
.state
|
|
||||||
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
|
|
||||||
{
|
|
||||||
// Capture a thumbnail once, on the transition into minimized.
|
|
||||||
if self.captured_toplevels.insert(toplevel.clone()) {
|
|
||||||
self.send_image(toplevel.clone());
|
|
||||||
}
|
|
||||||
let update = if is_new {
|
|
||||||
ToplevelUpdate::Add(info.clone())
|
|
||||||
} else {
|
|
||||||
ToplevelUpdate::Update(info.clone())
|
|
||||||
};
|
|
||||||
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(update)));
|
|
||||||
} else {
|
|
||||||
self.remove_toplevel(toplevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_toplevel(&mut self, toplevel: &ExtForeignToplevelHandleV1) {
|
|
||||||
self.captured_toplevels.remove(toplevel);
|
|
||||||
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
|
|
||||||
ToplevelUpdate::Remove(toplevel.clone()),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToplevelInfoHandler for AppData {
|
impl ToplevelInfoHandler for AppData {
|
||||||
|
|
@ -395,7 +364,23 @@ impl ToplevelInfoHandler for AppData {
|
||||||
_qh: &QueueHandle<Self>,
|
_qh: &QueueHandle<Self>,
|
||||||
toplevel: &ExtForeignToplevelHandleV1,
|
toplevel: &ExtForeignToplevelHandleV1,
|
||||||
) {
|
) {
|
||||||
self.handle_toplevel(toplevel, true);
|
if let Some(info) = self.toplevel_info_state.info(toplevel) {
|
||||||
|
if info
|
||||||
|
.state
|
||||||
|
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
|
||||||
|
{
|
||||||
|
// spawn thread for sending the image
|
||||||
|
self.send_image(toplevel.clone());
|
||||||
|
let _ = futures::executor::block_on(
|
||||||
|
self.tx
|
||||||
|
.send(WaylandUpdate::Toplevel(ToplevelUpdate::Add(info.clone()))),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
|
||||||
|
ToplevelUpdate::Remove(toplevel.clone()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_toplevel(
|
fn update_toplevel(
|
||||||
|
|
@ -404,7 +389,21 @@ impl ToplevelInfoHandler for AppData {
|
||||||
_qh: &QueueHandle<Self>,
|
_qh: &QueueHandle<Self>,
|
||||||
toplevel: &ExtForeignToplevelHandleV1,
|
toplevel: &ExtForeignToplevelHandleV1,
|
||||||
) {
|
) {
|
||||||
self.handle_toplevel(toplevel, false);
|
if let Some(info) = self.toplevel_info_state.info(toplevel) {
|
||||||
|
if info
|
||||||
|
.state
|
||||||
|
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
|
||||||
|
{
|
||||||
|
self.send_image(toplevel.clone());
|
||||||
|
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
|
||||||
|
ToplevelUpdate::Update(info.clone()),
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
|
||||||
|
ToplevelUpdate::Remove(toplevel.clone()),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toplevel_closed(
|
fn toplevel_closed(
|
||||||
|
|
@ -413,7 +412,9 @@ impl ToplevelInfoHandler for AppData {
|
||||||
_qh: &QueueHandle<Self>,
|
_qh: &QueueHandle<Self>,
|
||||||
toplevel: &ExtForeignToplevelHandleV1,
|
toplevel: &ExtForeignToplevelHandleV1,
|
||||||
) {
|
) {
|
||||||
self.remove_toplevel(toplevel);
|
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
|
||||||
|
ToplevelUpdate::Remove(toplevel.clone()),
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -481,7 +482,6 @@ pub(crate) fn wayland_handler(
|
||||||
toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh),
|
toplevel_manager_state: ToplevelManagerState::new(®istry_state, &qh),
|
||||||
queue_handle: qh,
|
queue_handle: qh,
|
||||||
registry_state,
|
registry_state,
|
||||||
captured_toplevels: std::collections::HashSet::new(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-network"
|
name = "cosmic-applet-network"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
async-fn-stream = "0.3"
|
async-fn-stream = "0.3"
|
||||||
|
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
|
||||||
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
|
||||||
cosmic = { workspace = true, features = [
|
libcosmic = { workspace = true, features = [
|
||||||
"applet",
|
"applet",
|
||||||
"applet-token",
|
"applet-token",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -23,7 +25,17 @@ tokio.workspace = true
|
||||||
tracing-log.workspace = true
|
tracing-log.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
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.4.0"
|
|
||||||
|
|
||||||
|
[dependencies.cosmic-settings-network-manager-subscription]
|
||||||
|
git = "https://github.com/pop-os/cosmic-settings/"
|
||||||
|
# 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"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<!DOCTYPE busconfig PUBLIC
|
|
||||||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
|
||||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
|
||||||
<busconfig>
|
|
||||||
<policy context="default">
|
|
||||||
<allow own_prefix="com.system76.CosmicSettings.Applet"/>
|
|
||||||
</policy>
|
|
||||||
</busconfig>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
cancel = Cancel·lar
|
|
||||||
connected = Connectat
|
|
||||||
wifi = Wi-Fi
|
|
||||||
identity = Identitat
|
|
||||||
|
|
@ -1,24 +1 @@
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
network = Hálózat
|
network = Hálózat
|
||||||
airplane-mode = Repülőgép-üzemmód
|
airplane-mode = Repülőgép-üzemmód
|
||||||
airplane-mode-on = A repülőgép-üzemmód be van kapcsolva
|
airplane-mode-on = A repülőgép-üzemmód be van kapcsolva
|
||||||
turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil széles sáv engedélyezéséhez.
|
turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil szélessáv engedélyezéséhez.
|
||||||
wifi = Wi-Fi
|
wifi = Wi-Fi
|
||||||
identity = Azonosító
|
identity = Azonosító
|
||||||
ipv4 = IPv4-cím
|
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 = Kapcsolódva
|
connected = Csatlakoztatva
|
||||||
connecting = Kapcsolódás
|
connecting = Csatlakozás…
|
||||||
connect = Kapcsolódás
|
connect = Csatlakozás
|
||||||
cancel = Mégse
|
cancel = Mégse
|
||||||
settings = Hálózati beállítások…
|
settings = Hálózati beállítások…
|
||||||
visible-wireless-networks = Látható vezeték nélküli hálózatok
|
visible-wireless-networks = Látható vezeték nélküli hálózatok
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
cancel = Têk bibe
|
|
||||||
connect = Girê bide
|
|
||||||
connected = Hat girêdan
|
|
||||||
network = Tor
|
|
||||||
airplane-mode = Awaya balafirê
|
|
||||||
airplane-mode-on = Awaya balafirê vekirî ye
|
|
||||||
turn-off-airplane-mode = Ji bo çalakirina Wi-Fi, Bluetooth û torên mobayîl, awaya balafirê bigire.
|
|
||||||
wifi = Wi-Fi
|
|
||||||
identity = Nasname
|
|
||||||
ipv4 = IPv4
|
|
||||||
ipv6 = IPv6
|
|
||||||
mac = MAC
|
|
||||||
megabits-per-second = Mbps
|
|
||||||
gigabits-per-second = Gbps
|
|
||||||
terabits-per-second = Tbps
|
|
||||||
connecting = Tê girêdan
|
|
||||||
settings = Sazkariyên torê...
|
|
||||||
visible-wireless-networks = Torên bêtêl ên xuya
|
|
||||||
vpn-connections = Girêdanên VPN
|
|
||||||
enter-password = Borînpeyvê yan jî kilîtê şîfrekirinê têxîne
|
|
||||||
router-wps-button = Tu dikarî bi pêlkirina bişkoka "WPS" ya li ser routerê jî girê bidî
|
|
||||||
unable-to-connect = Nikare bi torê ve were girêdan
|
|
||||||
check-wifi-connection = Piştrast bike ku Wi-Fi bi înternetê ve girêdayî ye û borînpeyv rast e
|
|
||||||
reset = Ji nû ve saz bike
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
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-з'єднання
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,7 @@
|
||||||
mod app;
|
mod app;
|
||||||
mod config;
|
mod config;
|
||||||
mod localize;
|
mod localize;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
use crate::localize::localize;
|
use crate::localize::localize;
|
||||||
|
|
||||||
|
|
|
||||||
18
cosmic-applet-network/src/utils.rs
Normal file
18
cosmic-applet-network/src/utils.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use futures_util::future::select;
|
||||||
|
|
||||||
|
/// Spawn a background tasks and forward its messages
|
||||||
|
pub fn forward_event_loop<M: 'static + Send, T: Future<Output = ()> + Send + 'static>(
|
||||||
|
event_loop: impl FnOnce(async_fn_stream::StreamEmitter<M>) -> T + Send + 'static,
|
||||||
|
) -> (tokio::sync::oneshot::Sender<()>, cosmic::Task<M>) {
|
||||||
|
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel::<()>();
|
||||||
|
|
||||||
|
let task = cosmic::Task::stream(async_fn_stream::fn_stream(|emitter| async move {
|
||||||
|
select(
|
||||||
|
std::pin::pin!(cancel_rx),
|
||||||
|
std::pin::pin!(event_loop(emitter)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}));
|
||||||
|
|
||||||
|
(cancel_tx, task)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-notifications"
|
name = "cosmic-applet-notifications"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
cosmic.workspace = true
|
libcosmic.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" }
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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] Épp most
|
[0] Most
|
||||||
[one] 1 órája
|
[one] 1 órával ezelőtt
|
||||||
*[other] { $duration } órája
|
*[other] { $duration } órával ezelőtt
|
||||||
}
|
}
|
||||||
minutes-ago =
|
minutes-ago =
|
||||||
{ $duration ->
|
{ $duration ->
|
||||||
[0] Épp most
|
[0] Most
|
||||||
[one] 1 perce
|
[one] 1 perccel ezelőtt
|
||||||
*[other] { $duration } perce
|
*[other] { $duration } perccel ezelőtt
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
hours-ago =
|
|
||||||
{ $duration ->
|
|
||||||
[0] Niha
|
|
||||||
[one] 1 demjimêr berê
|
|
||||||
*[other] { $duration } demjimêr berê
|
|
||||||
}
|
|
||||||
minutes-ago =
|
|
||||||
{ $duration ->
|
|
||||||
[0] Niha
|
|
||||||
[one] 1 xulek berê
|
|
||||||
*[other] { $duration } xulek berê
|
|
||||||
}
|
|
||||||
show-less = Kêmtir nîşan bide
|
|
||||||
show-more = Bêtir { $more } nîşan bide
|
|
||||||
clear-group = Komê pak bike
|
|
||||||
clear-all = Hemû agahdariyan pak bike
|
|
||||||
do-not-disturb = Rawestandina balkêşiyê
|
|
||||||
notification-settings = Sazkariyên agahdariyê....
|
|
||||||
no-notifications = Agahdarî tune ne
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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 } минута
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-power"
|
name = "cosmic-applet-power"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
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
|
||||||
cosmic.workspace = true
|
libcosmic.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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
cancel = Cancel·lar
|
|
||||||
confirm = Confirmar
|
|
||||||
restart = Reinicia
|
|
||||||
suspend = Suspèn
|
|
||||||
|
|
@ -4,33 +4,3 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
cancel = Têk bibe
|
|
||||||
confirm = Bipejirîne
|
|
||||||
power = Hêz
|
|
||||||
restart = Ji nû ve bide destpêkirin
|
|
||||||
settings = Sazkarî...
|
|
||||||
shutdown = Vemrîne
|
|
||||||
suspend = Rawestîne
|
|
||||||
log-out = Derkeve
|
|
||||||
lock-screen = Dîmenderê kilît bike
|
|
||||||
lock-screen-shortcut = Super + Escape
|
|
||||||
log-out-shortcut = Super + Shift + Escape
|
|
||||||
confirm-button =
|
|
||||||
{ $action ->
|
|
||||||
[restart] { restart }
|
|
||||||
[suspend] { suspend }
|
|
||||||
[shutdown] Vemrîne
|
|
||||||
[log-out] { log-out }
|
|
||||||
*[other] { confirm }
|
|
||||||
}
|
|
||||||
confirm-title =
|
|
||||||
{ $action ->
|
|
||||||
[restart] { restart }
|
|
||||||
[suspend] { suspend }
|
|
||||||
[shutdown] { shutdown }
|
|
||||||
[log-out] Hemû sepanan bigire û derkeve
|
|
||||||
*[other] Çalakiyê hilbijartî bisepîne
|
|
||||||
} niha?
|
|
||||||
confirm-body =
|
|
||||||
Pergal wê xweber were { $action ->
|
|
||||||
[restart] jinûvedestpêkkirin
|
|
||||||
[suspend] rawestandin
|
|
||||||
[shutdown] vemirandin
|
|
||||||
[lock-screen] kilîtkirin
|
|
||||||
[log-out] derketin
|
|
||||||
*[other] çalakiyê hilbijartî bisepîne
|
|
||||||
}di { $countdown } çirke de.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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,8 +16,9 @@ use cosmic::{
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
surface, theme,
|
surface, theme,
|
||||||
widget::{button, divider, icon, space, text},
|
widget::{Space, button, divider, icon, space, text},
|
||||||
};
|
};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use logind_zbus::{
|
use logind_zbus::{
|
||||||
manager::ManagerProxy,
|
manager::ManagerProxy,
|
||||||
|
|
@ -34,6 +35,9 @@ 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();
|
||||||
|
|
||||||
|
|
@ -45,6 +49,7 @@ 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)]
|
||||||
|
|
@ -70,7 +75,6 @@ impl PowerAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
enum Message {
|
enum Message {
|
||||||
Action(PowerAction),
|
Action(PowerAction),
|
||||||
TogglePopup,
|
TogglePopup,
|
||||||
|
|
@ -100,6 +104,7 @@ 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(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-status-area"
|
name = "cosmic-applet-status-area"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
cosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
rustc-hash.workspace = true
|
rustc-hash.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,11 @@ use cosmic::{
|
||||||
},
|
},
|
||||||
cctk::sctk::reexports::calloop,
|
cctk::sctk::reexports::calloop,
|
||||||
iced::{
|
iced::{
|
||||||
self,
|
self, Length, Subscription,
|
||||||
Event::Mouse,
|
|
||||||
Length, Subscription, event,
|
|
||||||
mouse::{self, ScrollDelta},
|
|
||||||
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
|
||||||
|
theme::Style,
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
scroll::{DiscreteScrollDelta, DiscreteScrollState},
|
|
||||||
surface,
|
surface,
|
||||||
widget::{container, mouse_area},
|
widget::{container, mouse_area},
|
||||||
};
|
};
|
||||||
|
|
@ -28,7 +25,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
None,
|
None,
|
||||||
Activate(usize),
|
Activate(usize),
|
||||||
|
|
@ -37,8 +33,7 @@ pub enum Msg {
|
||||||
StatusMenu((usize, status_menu::Msg)),
|
StatusMenu((usize, status_menu::Msg)),
|
||||||
StatusNotifier(status_notifier_watcher::Event),
|
StatusNotifier(status_notifier_watcher::Event),
|
||||||
TogglePopup(usize),
|
TogglePopup(usize),
|
||||||
Hovered(Option<usize>),
|
Hovered(usize),
|
||||||
WheelScrolled(ScrollDelta),
|
|
||||||
Surface(surface::Action),
|
Surface(surface::Action),
|
||||||
ToggleOverflow,
|
ToggleOverflow,
|
||||||
HoveredOverflow,
|
HoveredOverflow,
|
||||||
|
|
@ -54,8 +49,6 @@ pub(crate) struct App {
|
||||||
max_menu_id: usize,
|
max_menu_id: usize,
|
||||||
popup: Option<window::Id>,
|
popup: Option<window::Id>,
|
||||||
overflow_popup: Option<window::Id>,
|
overflow_popup: Option<window::Id>,
|
||||||
hovered_menu: Option<usize>,
|
|
||||||
scroll_states: BTreeMap<usize, DiscreteScrollState>,
|
|
||||||
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,10 +103,9 @@ impl App {
|
||||||
let overflow_index = self.overflow_index().unwrap_or(0);
|
let overflow_index = self.overflow_index().unwrap_or(0);
|
||||||
let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| {
|
let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| {
|
||||||
mouse_area(
|
mouse_area(
|
||||||
menu_icon_button(&self.core.applet, menu).on_press_down(Msg::TogglePopup(*id)),
|
menu_icon_button(&self.core.applet, &menu).on_press_down(Msg::TogglePopup(*id)),
|
||||||
)
|
)
|
||||||
.on_enter(Msg::Hovered(Some(*id)))
|
.on_enter(Msg::Hovered(*id))
|
||||||
.on_exit(Msg::Hovered(None))
|
|
||||||
.into()
|
.into()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -218,7 +210,6 @@ impl cosmic::Application for App {
|
||||||
{
|
{
|
||||||
let id = *id;
|
let id = *id;
|
||||||
self.menus.remove(&id);
|
self.menus.remove(&id);
|
||||||
self.scroll_states.remove(&id);
|
|
||||||
if self.open_menu == Some(id) {
|
if self.open_menu == Some(id) {
|
||||||
self.open_menu = None;
|
self.open_menu = None;
|
||||||
if let Some(popup_id) = self.popup {
|
if let Some(popup_id) = self.popup {
|
||||||
|
|
@ -321,10 +312,6 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Msg::Hovered(id) => {
|
Msg::Hovered(id) => {
|
||||||
self.hovered_menu = id;
|
|
||||||
let Some(id) = id else {
|
|
||||||
return Task::none();
|
|
||||||
};
|
|
||||||
let mut cmds = Vec::new();
|
let mut cmds = Vec::new();
|
||||||
if let Some(old_id) = self.open_menu.take() {
|
if let Some(old_id) = self.open_menu.take() {
|
||||||
if old_id != id {
|
if old_id != id {
|
||||||
|
|
@ -374,19 +361,6 @@ impl cosmic::Application for App {
|
||||||
cmds.push(get_popup(popup_settings));
|
cmds.push(get_popup(popup_settings));
|
||||||
Task::batch(cmds)
|
Task::batch(cmds)
|
||||||
}
|
}
|
||||||
Msg::WheelScrolled(delta) => {
|
|
||||||
let Some(id) = self.hovered_menu else {
|
|
||||||
return Task::none();
|
|
||||||
};
|
|
||||||
let discrete_delta = self.scroll_states.entry(id).or_default().update(delta);
|
|
||||||
let Some((delta, orientation)) = discrete_scroll_delta(discrete_delta) else {
|
|
||||||
return Task::none();
|
|
||||||
};
|
|
||||||
let Some(menu) = self.menus.get(&id) else {
|
|
||||||
return Task::none();
|
|
||||||
};
|
|
||||||
scroll(id, menu.item.item_proxy().clone(), delta, orientation)
|
|
||||||
}
|
|
||||||
Msg::Surface(a) => {
|
Msg::Surface(a) => {
|
||||||
return cosmic::task::message(cosmic::Action::Cosmic(
|
return cosmic::task::message(cosmic::Action::Cosmic(
|
||||||
cosmic::app::Action::Surface(a),
|
cosmic::app::Action::Surface(a),
|
||||||
|
|
@ -431,7 +405,6 @@ impl cosmic::Application for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Msg::HoveredOverflow => {
|
Msg::HoveredOverflow => {
|
||||||
self.hovered_menu = None;
|
|
||||||
let mut cmds = Vec::new();
|
let mut cmds = Vec::new();
|
||||||
if self.overflow_popup.is_some() {
|
if self.overflow_popup.is_some() {
|
||||||
// If we already have an overflow popup, do nothing
|
// If we already have an overflow popup, do nothing
|
||||||
|
|
@ -487,12 +460,6 @@ impl cosmic::Application for App {
|
||||||
subscriptions.push(menu.subscription(is_open).with(*id).map(Msg::StatusMenu));
|
subscriptions.push(menu.subscription(is_open).with(*id).map(Msg::StatusMenu));
|
||||||
}
|
}
|
||||||
subscriptions.push(activation_token_subscription(0).map(Msg::Token));
|
subscriptions.push(activation_token_subscription(0).map(Msg::Token));
|
||||||
subscriptions.push(event::listen_with(|e, status, _| match (e, status) {
|
|
||||||
(Mouse(mouse::Event::WheelScrolled { delta }), event::Status::Ignored) => {
|
|
||||||
Some(Msg::WheelScrolled(delta))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}));
|
|
||||||
|
|
||||||
iced::Subscription::batch(subscriptions)
|
iced::Subscription::batch(subscriptions)
|
||||||
}
|
}
|
||||||
|
|
@ -505,10 +472,9 @@ impl cosmic::Application for App {
|
||||||
.iter()
|
.iter()
|
||||||
.take(overflow_index.unwrap_or(self.menus.len()))
|
.take(overflow_index.unwrap_or(self.menus.len()))
|
||||||
.map(|(id, menu)| {
|
.map(|(id, menu)| {
|
||||||
mouse_area(menu_icon_button(&self.core.applet, menu).on_press(Msg::Activate(*id)))
|
mouse_area(menu_icon_button(&self.core.applet, &menu).on_press(Msg::Activate(*id)))
|
||||||
.on_right_press(Msg::TogglePopup(*id))
|
.on_right_press(Msg::TogglePopup(*id))
|
||||||
.on_enter(Msg::Hovered(Some(*id)))
|
.on_enter(Msg::Hovered(*id))
|
||||||
.on_exit(Msg::Hovered(None))
|
|
||||||
.into()
|
.into()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -604,33 +570,6 @@ fn activate(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll(
|
|
||||||
id: usize,
|
|
||||||
item_proxy: crate::subscriptions::status_notifier_item::StatusNotifierItemProxy<'static>,
|
|
||||||
delta: i32,
|
|
||||||
orientation: &'static str,
|
|
||||||
) -> Task<cosmic::Action<Msg>> {
|
|
||||||
Task::future(async move {
|
|
||||||
match item_proxy.scroll(delta, orientation).await {
|
|
||||||
Ok(_) => cosmic::action::app(Msg::None),
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Scroll failed for {}: {}", id, err);
|
|
||||||
cosmic::action::app(Msg::None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn discrete_scroll_delta(delta: DiscreteScrollDelta) -> Option<(i32, &'static str)> {
|
|
||||||
if delta.y != 0 {
|
|
||||||
Some((delta.y as i32, "vertical"))
|
|
||||||
} else if delta.x != 0 {
|
|
||||||
Some((delta.x as i32, "horizontal"))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn menu_icon_button<'a>(
|
fn menu_icon_button<'a>(
|
||||||
applet: &'a cosmic::applet::Context,
|
applet: &'a cosmic::applet::Context,
|
||||||
menu: &'a status_menu::State,
|
menu: &'a status_menu::State,
|
||||||
|
|
@ -638,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;
|
||||||
|
|
@ -656,7 +595,7 @@ fn menu_icon_button<'a>(
|
||||||
.class(if symbolic {
|
.class(if symbolic {
|
||||||
cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| {
|
cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| {
|
||||||
cosmic::iced::widget::svg::Style {
|
cosmic::iced::widget::svg::Style {
|
||||||
color: Some(theme.cosmic().background(false).on.into()),
|
color: Some(theme.cosmic().background.on.into()),
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -675,43 +614,3 @@ fn menu_icon_button<'a>(
|
||||||
pub fn main() -> iced::Result {
|
pub fn main() -> iced::Result {
|
||||||
cosmic::applet::run::<App>(())
|
cosmic::applet::run::<App>(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn discrete_scroll_prefers_vertical_delta() {
|
|
||||||
assert_eq!(
|
|
||||||
discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 4, y: -2 }),
|
|
||||||
Some((-2, "vertical"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn discrete_scroll_uses_horizontal_delta_when_vertical_is_zero() {
|
|
||||||
assert_eq!(
|
|
||||||
discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 3, y: 0 }),
|
|
||||||
Some((3, "horizontal"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn discrete_scroll_ignores_zero_delta() {
|
|
||||||
assert_eq!(
|
|
||||||
discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 0, y: 0 }),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pixel_scroll_accumulates_before_emitting_discrete_delta() {
|
|
||||||
let mut state = cosmic::scroll::DiscreteScrollState::default();
|
|
||||||
|
|
||||||
let first = state.update(ScrollDelta::Pixels { x: 0.0, y: 12.0 });
|
|
||||||
assert_eq!(discrete_scroll_delta(first), None);
|
|
||||||
|
|
||||||
let second = state.update(ScrollDelta::Pixels { x: 0.0, y: 12.0 });
|
|
||||||
assert_eq!(discrete_scroll_delta(second), Some((1, "vertical")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ 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)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +39,6 @@ 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(),
|
||||||
|
|
@ -65,7 +63,6 @@ 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() {
|
||||||
|
|
@ -96,18 +93,13 @@ 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 {
|
||||||
let mut builder = icon::from_name(icon_name).prefer_svg(true).fallback(Some(
|
icon::from_name(icon_name)
|
||||||
IconFallback::Names(vec![
|
.prefer_svg(true)
|
||||||
|
.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,18 +10,21 @@
|
||||||
//! 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 there is at least one client still connected.
|
//! The daemon runs as long as as there is at least one client still connected. Which it checks
|
||||||
|
//! 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;
|
use std::{collections::HashSet, time::Duration};
|
||||||
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() {
|
||||||
|
|
@ -39,7 +42,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,7 +2,6 @@
|
||||||
// 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};
|
||||||
|
|
@ -28,7 +27,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 {
|
||||||
|
|
@ -119,11 +118,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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,8 +201,6 @@ pub trait StatusNotifierItem {
|
||||||
fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
|
fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
|
||||||
|
|
||||||
fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
|
fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
|
||||||
|
|
||||||
fn scroll(&self, delta: i32, orientation: &str) -> zbus::Result<()>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-applet-tiling"
|
name = "cosmic-applet-tiling"
|
||||||
version = "1.3.0"
|
version = "1.0.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cosmic.workspace = true
|
libcosmic.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
cctk.workspace = true
|
cctk.workspace = true
|
||||||
cosmic-comp-config.workspace = true
|
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
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 = Lebegő ablak be/ki
|
toggle-floating-window = Az ablakok lebegtetésének be- és kikapcsolása
|
||||||
view-all-shortcuts = Az összes gyorsbillentyű megtekintése…
|
view-all-shortcuts = Az összes gyorsbillentyű megtekintése…
|
||||||
active-hint = Aktív ablak kiemelése
|
active-hint = Aktív ablak kiemelése
|
||||||
gaps = Hézagok
|
gaps = Hézagok
|
||||||
floating-window-exceptions = Lebegőablak-kivételek…
|
floating-window-exceptions = Lebegő ablak kivételek…
|
||||||
window-management-settings = Ablakkezelési beállítások…
|
window-management-settings = Ablakkezelési beállítások…
|
||||||
all-workspaces = Összes munkaterület
|
all-workspaces = Összes munkaterület
|
||||||
per-workspace = Munkaterületenként
|
per-workspace = Munkaterületenként
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
tile-windows = Çarçoveyan xweber berhev bike
|
|
||||||
tile-current = Qada xebatê heyî berhev bike
|
|
||||||
shortcuts = Kurterê
|
|
||||||
navigate-windows = Di nav çarçoveyan de bigere
|
|
||||||
move-window = Çarçoveyê bilivîne
|
|
||||||
toggle-floating-window = Rewşê çarçoveyê herikbar biguhêrîne
|
|
||||||
view-all-shortcuts = Hemû kurteriyan nîşan bide...
|
|
||||||
active-hint = Pêşniyara çalak
|
|
||||||
gaps = Valahî
|
|
||||||
floating-window-exceptions = Awarteyên çarçoveyê herikbar...
|
|
||||||
window-management-settings = Sazkariyên rêveberiya çarçoveyê...
|
|
||||||
all-workspaces = Hemû qadên xebatê
|
|
||||||
per-workspace = Li gorî qada xebatê
|
|
||||||
super = Super
|
|
||||||
shift = Shift
|
|
||||||
arrow-keys = tîr
|
|
||||||
tiled = Berhevkirî
|
|
||||||
floating = Herikbar
|
|
||||||
autotile-behavior = Li ser qadên xebatê çarçoveyan berhev bike
|
|
||||||
new-workspace = Tevgera qada xebatê ya nû
|
|
||||||
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