Compare commits

..

No commits in common. "pre-1.8-merge-20260910" and "master" have entirely different histories.

229 changed files with 3487 additions and 7045 deletions

View file

@ -19,11 +19,11 @@ jobs:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.93.1
toolchain: 1.90.0
components: clippy
- 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
- uses: actions-rs-plus/clippy-check@v2
with:
toolchain: 1.93.1
toolchain: 1.90.0
args: --all --all-targets --all-features

View file

@ -1,15 +0,0 @@
{
"format_on_save": "on",
"lsp": {
"rust-analyzer": {
"initialization_options": {
"check": {
"command": "clippy",
},
"rustfmt": {
"extraArgs": ["+nightly"],
},
},
},
},
}

2370
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,11 +25,11 @@ resolver = "3"
[workspace.dependencies]
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-protocols = { path = "../cosmic-protocols", default-features = false, features = [
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = [
"client",
]}
], rev = "d0e95be" }
futures = "0.3"
futures-util = "0.3"
@ -38,7 +38,7 @@ i18n-embed = { version = "0.16.0", features = [
"desktop-requester",
] }
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-token",
"dbus-config",
@ -48,7 +48,6 @@ cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features =
"desktop-systemd-scope",
"winit",
] }
cosmic-comp-config = { path = "../cosmic-comp/cosmic-comp-config" }
rust-embed = "8.11.0"
rust-embed-utils = "8.11.0"
rustc-hash = "2.1"
@ -59,14 +58,14 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-log = "0.2.0"
tokio = { version = "1.49.0", features = ["full"] }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
serde = { version = "1.0.228", features = ["derive"] }
zlink = "0.5.0"
[profile.release]
opt-level = 3
panic = "abort"
lto = "thin"
# opt-level = 3
# panic = "abort"
# lto = "thin"
opt-level = 1
[workspace.metadata.cargo-machete]
ignored = ["libcosmic"]
@ -75,52 +74,20 @@ ignored = ["libcosmic"]
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# libcosmic = { path = "../libcosmic" }
# iced_futures = { path = "../libcosmic/iced/futures" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" }
# iced_futures = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//" }
# iced_futures = { git = "https://github.com/pop-os/libcosmic//" }
[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/pop-os/winit.git"]
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
# winit = { path = "../winit" }
[patch."https://github.com/smithay/client-toolkit.git"]
sctk = { package = "smithay-client-toolkit", version = "0.20.0" }
[patch."https://github.com/pop-os/cosmic-protocols"]
cosmic-protocols = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols" }
cosmic-client-toolkit = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols/client-toolkit" }
[patch."https://github.com/pop-os/cosmic-panel"]
cosmic-panel-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/cosmic-panel-config" }
xdg-shell-wrapper-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/xdg-shell-wrapper-config" }
[patch."https://github.com/pop-os/cosmic-notifications"]
cosmic-notifications-config = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-config" }
cosmic-notifications-util = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-util" }
[patch."https://github.com/pop-os/cosmic-settings"]
cosmic-settings-a11y-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/a11y-manager" }
cosmic-settings-accessibility-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/accessibility" }
cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" }
cosmic-settings-daemon-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/settings-daemon" }
cosmic-settings-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" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-app-list"
version = "1.3.0"
version = "1.0.2"
edition = "2024"
license = "GPL-3.0-only"
@ -13,13 +13,13 @@ futures.workspace = true
i18n-embed.workspace = true
i18n-embed-fl.workspace = true
image = { version = "0.25.9", default-features = false }
cosmic.workspace = true
libcosmic.workspace = true
memmap2 = "0.9.10"
fastrand = "2.3.0"
rust-embed.workspace = true
rustix.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
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -1,10 +1,10 @@
[package]
name = "cosmic-app-list-config"
version = "1.3.0"
version = "1.0.2"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmic.workspace = true
libcosmic.workspace = true
serde = { workspace = true, features = ["derive"] }

View file

@ -1,6 +1,6 @@
cosmic-app-list = دُرج التطبيقات
pin = ثبِّت في درج التطبيقات
quit = إنهِ
quit = أنهِ
quit-all = أنهِ الكل
new-window = نافذة جديدة
run = شغِّل

View file

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

View file

@ -1,5 +1,5 @@
cosmic-app-list = Oblast aplikací
pin = Připnout do oblasti aplikací
cosmic-app-list = Panel aplikací
pin = Připnout do panelu aplikací
quit = Ukončit
quit-all = Ukončit vše
new-window = Nové okno

View file

@ -1,8 +0,0 @@
run = Kør
run-on = Kør på { $gpu }
run-on-default = (Standard)
cosmic-app-list = Programbakke
new-window = Nyt Vindue
quit = Afslut
quit-all = Forlad Alle
pin = Fastgør til app-skuffe

View file

@ -1,8 +1 @@
new-window = Νέο παράθυρο
quit = Τερματισμός
cosmic-app-list = Περιοχή εφαρμογών
run = Εκτέλεση
run-on = Εκτέλεση με { $gpu }
quit-all = Έξοδος από όλα
run-on-default = (Προεπιλογή)
pin = Καρφίτσωμα στην περιοχή εφαρμογών

View file

@ -1,8 +0,0 @@
cosmic-app-list = App Tray
new-window = New Window
quit = Quit
run = Run
run-on = Run on { $gpu }
quit-all = Quit All
run-on-default = (Default)
pin = Pin to app tray

View file

@ -5,15 +5,4 @@ quit-all = Quit All
new-window = New Window
run = Run
run-on = Run on {$gpu}
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
run-on-default = (Default)

View file

@ -1,8 +1,8 @@
cosmic-app-list = نوار برنامه‌ها
pin = افزودن به نوار برنامه‌ها
quit = خروج
quit-all = خروج از همه
cosmic-app-list = سینی برنامه‌ها
pin = سنجاق کردن به سینی برنامه‌ها
quit = بستن
quit-all = بستن همه
new-window = پنجره جدید
run = اجرا
run-on = اجرا با { $gpu }
run-on-default = (پیش‌فرض)
run-on = اجرا روی {$gpu}
run-on-default = (پیش‌فرض)

View file

@ -3,17 +3,6 @@ pin = Épingler à la barre d'applis
quit = Quitter
quit-all = Tout quitter
new-window = Nouvelle fenêtre
run = Exécuter
run = Lancer
run-on = Lancer avec { $gpu }
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

View file

@ -1,8 +0,0 @@
quit = Pechar
run = Executar
cosmic-app-list = Bandexa de aplicacións
pin = Fixar na bandexa de aplicacións
quit-all = Quitar todo
new-window = Nova Xanela
run-on = Executar en { $gpu }
run-on-default = (Por defecto)

View file

@ -1,8 +1,8 @@
cosmic-app-list = ऐप ट्रे
pin = ऐप ट्रे में पिन करें
quit = बाहर जाएँ
quit = बंद करें
quit-all = सभी बंद करें
new-window = नई विंडो
run = चलाएँ
run-on = { $gpu } पर चलाएँ
run-on = {$gpu} पर चलाएँ
run-on-default = (डिफ़ॉल्ट)

View file

@ -1,8 +1,8 @@
cosmic-app-list = Area applicazioni
cosmic-app-list = Barra applicazioni
pin = Fissa nella barra delle applicazioni
quit = Esci
quit-all = Chiudi tutto
new-window = Nuova finestra
run = Esegui
run-on = Esegui su { $gpu }
run-on-default = (Predefinito)
run-on = Esegui usando la GPU: { $gpu }
run-on-default = (default)

View file

@ -1,5 +1,5 @@
quit = Шығу
run = Орындау
run = Іске қосу
run-on = { $gpu } арқылы іске қосу
run-on-default = (Әдепкі)
cosmic-app-list = Қолданбалар сөресі

View file

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

View file

@ -1,5 +1,5 @@
new-window = Naujas Langas
quit = Išjungti
quit = Išeiti
run = Paleisti
run-on = Paleisti naudojantis { $gpu }
run-on-default = (Numatytas)

View file

@ -1 +0,0 @@
cosmic-app-list = Cinta d’aplicacions

View file

@ -1,4 +1,4 @@
quit = ਸਮਾਪਤ
quit = ਬਾਹਰ
cosmic-app-list = ਐਪ ਟਰੇ
pin = ਐਪ ਟਰੇ ਵਿੱਚ ਟੰਗੋ
quit-all = ਸਭ ਵਿੱਚੋਂ ਬਾਹਰ

View file

@ -1,6 +1,6 @@
cosmic-app-list = Zasobnik aplikacji
pin = Przypnij do zasobnika aplikacji
quit = Zakończ
quit = Zamknij
quit-all = Zamknij wszystkie
new-window = Nowe okno
run = Uruchom

View file

@ -1,6 +1,6 @@
cosmic-app-list = Bandeja de aplicativos
pin = Fixar na bandeja de aplicativos
quit = Encerrar
quit = Sair
quit-all = Sair de Todos
new-window = Nova Janela
run = Executar

View file

@ -1,8 +1,8 @@
cosmic-app-list = Область приложений
pin = Закрепить на панели приложений
quit = Завершить
quit = Выйти
quit-all = Завершить все
new-window = Новое окно
run = Выполнить
run = Запустить
run-on = Запустить на { $gpu }
run-on-default = (По умолчанию)

View file

@ -1,2 +0,0 @@
run-on = { $gpu } ᱨᱮ ᱫᱟᱹᱲ ᱟᱭ
run-on-default = (ᱢᱩᱞ)

View file

@ -1,8 +0,0 @@
new-window = Нови прозор
quit = Изађи
run = Покрени
run-on = Покрени на { $gpu }
quit-all = Изађи из свега
run-on-default = (подразумевано)
cosmic-app-list = Системска касета
pin = Закачи у системску касету

View file

@ -1,8 +1,8 @@
cosmic-app-list = Панель застосунків
pin = Закріпити
quit = Завершити
quit = Вийти
quit-all = Закрити всі
new-window = Нове вікно
run = Виконати
run = Запустити
run-on = Запустити на { $gpu }
run-on-default = (Основна)

View file

@ -1,8 +1,8 @@
cosmic-app-list = 應用程式托盤
pin = 釘選至應用程式托盤
quit = 結束
cosmic-app-list = 應用程式匣盤
pin = 釘選至應用程式匣盤
quit = 退出
quit-all = 全部退出
new-window = 新增視窗
new-window = 新建視窗
run = 執行
run-on = 在 { $gpu } 上執行
run-on-default = (預設)

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -2,8 +2,6 @@
// SPDX-License-Identifier: GPL-3.0-only
mod app;
mod icon_catalog;
mod launcher_edit;
mod localize;
mod wayland_handler;
mod wayland_subscription;

View file

@ -388,10 +388,7 @@ impl CaptureData {
},
)
.unwrap();
if let Err(err) = self.conn.flush() {
tracing::error!("Wayland flush failed during screencopy session create: {err}");
return None;
}
self.conn.flush().unwrap();
let formats = session
.wait_while(|data| data.formats.is_none())
@ -440,10 +437,7 @@ impl CaptureData {
session: capture_session.clone(),
},
);
if let Err(err) = self.conn.flush() {
tracing::error!("Wayland flush failed during screencopy capture: {err}");
return None;
}
self.conn.flush().unwrap();
// TODO: wait for server to release buffer?
let res = session
@ -715,10 +709,7 @@ pub(crate) fn wayland_handler(
if app_data.exit {
break;
}
if let Err(err) = event_loop.dispatch(None, &mut app_data) {
tracing::error!("Wayland event loop terminated: {err}");
break;
}
event_loop.dispatch(None, &mut app_data).unwrap();
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applet-a11y"
version = "1.3.0"
version = "1.0.2"
edition = "2024"
[dependencies]
@ -9,7 +9,7 @@ cctk.workspace = true
cosmic-protocols.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
cosmic.workspace = true
libcosmic.workspace = true
rust-embed.workspace = true
tokio.workspace = true
tracing-log.workspace = true
@ -17,7 +17,9 @@ tracing-subscriber.workspace = true
tracing.workspace = true
[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]
path = "../../cosmic-settings/subscriptions/accessibility"
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/accessibility"

View file

@ -1,6 +0,0 @@
magnifier = Lupa
screen-reader = Narrador
invert-colors = Colors invertits
settings = Configuració d'accessibilitat...
filter-colors = Filtres de colors
high-contrast = Mode d'alt contrast

View file

@ -1,6 +1,6 @@
screen-reader = Čtečka obrazovky
magnifier = Lupa
invert-colors = Inverze barev
invert-colors = Převrátit barvy
settings = Nastavení přístupnosti...
filter-colors = Filtr barev
filter-colors = Filtrovat barvy
high-contrast = Vysoký kontrast

View file

@ -1,6 +0,0 @@
screen-reader = Skærmlæser
invert-colors = Invertér farver
high-contrast = Høj kontrast
filter-colors = Filtrér farver
settings = Tilgængelighedsindstillinger...
magnifier = Forstørrelsesglas

View file

@ -1,6 +1,6 @@
screen-reader = Bildschirmleser
magnifier = Vergrößerungsglas
invert-colors = Farben invertieren
settings = Barrierefreiheitseinstellungen ...
settings = Barrierefreiheitseinstellungen…
filter-colors = Farben filtern
high-contrast = Hoher Kontrast

View file

@ -1,6 +0,0 @@
screen-reader = Ανάγνωση οθόνης
invert-colors = Αναστροφή χρωμάτων
high-contrast = Υψηλή αντίθεση
settings = Ρυθμίσεις προσβασιμότητας...
magnifier = Μεγεθυντικός φακός
filter-colors = Φίλτρο χρωμάτων

View file

@ -1,6 +0,0 @@
screen-reader = Screen reader
invert-colors = Invert colours
high-contrast = High contrast
filter-colors = Filter colours
settings = Accessibility settings...
magnifier = Magnifier

View file

@ -1,6 +1,6 @@
screen-reader = صفحه‌خوان
magnifier = ذره‌بین
invert-colors = وارونه کردن رنگ‌ها
invert-colors = وارونه‌سازی رنگ‌ها
settings = تنظیمات دسترسی‌پذیری...
filter-colors = فیلتر رنگ‌ها
high-contrast = کنتراست بالا

View file

@ -1,6 +0,0 @@
screen-reader = Lector de pantalla
magnifier = Lupa
invert-colors = Invertir cores
settings = Configuración de accesibilidade...
filter-colors = Filtro de cores
high-contrast = Alto contraste

View file

@ -1,6 +1,4 @@
screen-reader = स्क्रीन रीडर
filter-colors = रंग फ़िल्टर करें
settings = एक्सेसिबिलिटी सेटिंग्स..।
magnifier = मैग्नीफायर
high-contrast = उच्च कंट्रास्ट
invert-colors = रंग उल्टे करें
filter-colors = रंग फ़िल्टर
settings = सुलभता सेटिंग..।
magnifier = आवर्धक

View file

@ -2,5 +2,5 @@ screen-reader = Lettore dello schermo
magnifier = Lente di ingrandimento
invert-colors = Inverti colori
settings = Impostazioni accessibilità...
filter-colors = Filtri colore
filter-colors = Filtra colori
high-contrast = Contrasto elevato

View file

@ -2,5 +2,5 @@ screen-reader = スクリーンリーダー
magnifier = 拡大鏡
settings = アクセシビリティ設定…
invert-colors = 色を反転
filter-colors = カラーフィルター
filter-colors = 色のフィルター
high-contrast = ハイコントラスト

View file

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

View file

@ -1,2 +0,0 @@
screen-reader = ᱥᱠᱨᱤᱱ ᱨᱤᱰᱟᱨ
invert-colors = ᱨᱚᱝ ᱩᱞᱴᱟᱹ ᱪᱷᱚ

View file

@ -1,6 +0,0 @@
screen-reader = Читач екрана
invert-colors = Обрни боје
high-contrast = Високи контраст
filter-colors = Филтрирај боје
settings = Подешавања приступачности...
magnifier = Увеличавач

View file

@ -26,9 +26,10 @@ use cosmic::{
};
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 std::sync::LazyLock;
use tokio::sync::mpsc::UnboundedSender;
pub fn run() -> cosmic::iced::Result {
@ -51,7 +52,6 @@ struct CosmicA11yApplet {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
TogglePopup,
CloseRequested(window::Id),
@ -133,27 +133,20 @@ impl cosmic::Application for CosmicA11yApplet {
}
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
return cosmic::surface::surface_task(cosmic::surface::action::destroy_popup(
p,
));
return destroy_popup(p);
} else {
return cosmic::surface::surface_task(cosmic::surface::action::app_popup(
|_| Default::default(),
|app: &mut CosmicA11yApplet| {
let new_id = window::Id::unique();
app.popup.replace(new_id);
let new_id = window::Id::unique();
self.popup.replace(new_id);
let popup_settings = app.core.applet.get_popup_settings(
app.core.main_window_id().unwrap(),
new_id,
Some((1, 1)),
None,
None,
);
popup_settings
},
let popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(),
new_id,
Some((1, 1)),
None,
));
None,
);
return get_popup(popup_settings);
}
}
Message::CloseRequested(id) => {

View file

@ -2,12 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only
use anyhow;
use cctk::sctk::reexports::calloop::{self};
use cctk::sctk::reexports::calloop::{self, channel::SyncSender};
use cosmic::iced::{
self, Subscription,
futures::{self, SinkExt},
futures::{self, SinkExt, StreamExt, channel::mpsc},
stream,
};
use cosmic_protocols::a11y::v1::client::cosmic_a11y_manager_v1::Filter;
use cosmic_settings_a11y_manager_subscription::{
self as thread, AccessibilityEvent, AccessibilityRequest,
};

View file

@ -1,15 +1,15 @@
[package]
name = "cosmic-applet-audio"
version = "1.3.0"
version = "1.0.2"
edition = "2024"
license = "GPL-3.0-only"
[dependencies]
futures.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
cosmic.workspace = true
mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
libcosmic.workspace = true
mpris2-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
# mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
rust-embed.workspace = true
serde.workspace = true
tokio.workspace = true
@ -19,9 +19,7 @@ tracing.workspace = true
url = "2"
urlencoding = "2.1.3"
zbus.workspace = true
zlink.workspace = true
intmap = "3.1.3"
[dependencies.cosmic-settings-audio-client]
path = "../../cosmic-settings-daemon/audio-client"
features = ["codec"]
[dependencies.cosmic-settings-sound-subscription]
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/sound"

View file

@ -1,7 +0,0 @@
output = Sortida
show-media-controls = Mostra els controls multimèdia a la barra
disconnected = PulseAudio desconnectat
no-device = No s'ha seleccionat cap dispositiu
input = Entrada
unknown-artist = Desconegut
sound-settings = Configuració de so...

View file

@ -1,7 +0,0 @@
show-media-controls = Vis mediekontrol på panel
disconnected = PulseAudio Afbrudt
no-device = Ingen enhed valgt
input = Indgang
output = Udgang
unknown-artist = Ukendt
sound-settings = Lydindstillinger...

View file

@ -1,7 +1,7 @@
output = Ausgabe
input = Eingabe
show-media-controls = Mediensteuerung im Panel anzeigen
sound-settings = Klang-Einstellungen ...
sound-settings = Klang-Einstellungen...
disconnected = PulseAudio getrennt
no-device = Kein Gerät ausgewählt
unknown-artist = Unbekannt

View file

@ -1,7 +0,0 @@
output = Έξοδος
show-media-controls = Εμφάνιση στοιχείων ελέγχου πολυμέσων στη γραμμή συστήματος
disconnected = Το PulseAudio αποσυνδέθηκε
no-device = Καμία επιλεγμένη συσκευή
input = Είσοδος
unknown-artist = Άγνωστος
sound-settings = Ρυθμίσεις ήχου...

View file

@ -1,7 +0,0 @@
show-media-controls = Show media controls on panel
disconnected = PulseAudio Disconnected
no-device = No device selected
input = Input
output = Output
unknown-artist = Unknown
sound-settings = Sound Settings...

View file

@ -1,7 +0,0 @@
output = Saída
input = Entrada
show-media-controls = Mostrar os controis multimedia no panel
sound-settings = Axustes do son...
disconnected = PulseAudio Desconectado
no-device = Ningún dipositivo seleccionado
unknown-artist = Descoñecido

View file

@ -2,6 +2,6 @@ output = Kimenet
input = Bemenet
show-media-controls = Médiavezérlők megjelenítése a panelen
sound-settings = Hangbeállítások…
disconnected = A PulseAudio-kapcsolat megszakadt
no-device = Nincs eszköz kiválasztva
disconnected = PulseAudio nincs csatlakozva
no-device = Nincs kiválasztott eszköz
unknown-artist = Ismeretlen

View file

@ -4,4 +4,4 @@ show-media-controls = Mostra controlli multimediali nel pannello in alto
sound-settings = Impostazioni audio...
disconnected = PulseAudio Disconnesso
no-device = Nessun dispositivo selezionato
unknown-artist = Sconosciuto
unknown-artist = Sconosciuto

View file

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

View file

@ -4,4 +4,4 @@ no-device = Nepasirinktas įrenginys
input = Įvestis
output = Išvestis
unknown-artist = Nežinomas atlikėjas
sound-settings = Garso Nuostatos...
sound-settings = Garso Nustatymai...

View file

@ -1 +0,0 @@
output = ᱟᱨᱡᱟᱣ

View file

@ -1,7 +0,0 @@
show-media-controls = Прикажи управљања медијима на траци
disconnected = Пулс-аудио откачен
no-device = Ниједан уређај није изабран
input = Улаз
output = Излаз
unknown-artist = Непознато
sound-settings = Подешавања звука...

View file

@ -2,13 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-only
mod localize;
mod model;
mod mouse_area;
use crate::localize::localize;
use config::{AudioAppletConfig, amplification_sink, amplification_source};
use cosmic::{
Apply, Element, Renderer, Task, Theme, app,
Element, Renderer, Task, Theme, app,
applet::{
column as applet_column,
cosmic_panel_config::PanelAnchor,
@ -19,29 +18,18 @@ use cosmic::{
cosmic_config::CosmicConfigEntry,
cosmic_theme::Spacing,
iced::{
self, Alignment, Length, Rectangle, Subscription,
self, Alignment, Length, Subscription,
futures::StreamExt,
widget::{self, column, row, slider},
window,
},
surface, theme,
widget::{
Row, button, container, divider, icon,
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
space, text, toggler,
},
widget::{Row, button, container, divider, icon, space, text, toggler},
};
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use futures::SinkExt;
use cosmic_settings_sound_subscription as css;
use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup};
use mpris_subscription::{MprisRequest, MprisUpdate};
use mpris2_zbus::player::PlaybackStatus;
use std::{
sync::{
Arc,
atomic::{self, AtomicU32},
},
time::Duration,
};
mod config;
mod mpris_subscription;
@ -62,16 +50,10 @@ pub struct Audio {
core: cosmic::app::Core,
/// Track the applet's popup window.
popup: Option<window::Id>,
/// Varlink connection to `com.system76.CosmicSettings.Audio`.
audio_client: Option<audio_client::Client>,
/// Known audio device state
model: model::Model,
/// The model from cosmic-settings for managing pipewire devices.
model: css::Model,
/// Whether to expand the revealer of a source or sink device.
is_open: IsOpen,
/// Used to debounce sink volume updates.
applied_sink_volume: Arc<AtomicU32>,
/// Used to deboune source volume updates.
applied_source_volume: Arc<AtomicU32>,
/// Max slider volume for the sink device, as determined by the amplification property.
max_sink_volume: u32,
/// Max slider volume for the source device, as determined by the amplification property.
@ -86,14 +68,12 @@ pub struct Audio {
player_status: Option<mpris_subscription::PlayerStatus>,
/// Used to request an activation token for opening cosmic-settings.
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
rectangle_tracker: Option<RectangleTracker<u32>>,
rectangle: Option<iced::Rectangle>,
}
impl Audio {
fn output_icon_name(&self) -> &'static str {
let volume = self.model.active_sink.volume;
let mute = self.model.active_sink.mute;
let volume = self.model.sink_volume;
let mute = self.model.sink_mute;
if mute || volume == 0 {
"audio-volume-muted-symbolic"
} else if volume < 33 {
@ -108,8 +88,8 @@ impl Audio {
}
fn input_icon_name(&self) -> &'static str {
let volume = self.model.active_source.volume;
let mute = self.model.active_source.mute;
let volume = self.model.source_volume;
let mute = self.model.source_mute;
if mute || volume == 0 {
"microphone-sensitivity-muted-symbolic"
} else if volume < 33 {
@ -120,21 +100,6 @@ impl Audio {
"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)]
@ -145,31 +110,27 @@ enum IsOpen {
Input,
}
#[derive(Clone, Debug)]
#[derive(Debug, Clone)]
pub enum Message {
/// Connection to `com.system76.CosmicSettings`.
Client(Arc<audio_client::Client>),
CloseRequested(window::Id),
ConfigChanged(AudioAppletConfig),
Ignore,
InputToggle,
Mpris(mpris_subscription::MprisUpdate),
MprisRequest(MprisRequest),
OpenSettings,
OutputToggle,
ReattachClient(Arc<audio_client::Client>),
Rectangle(RectangleUpdate<u32>),
SetDefaultSink(usize),
SetDefaultSource(usize),
SetSinkVolume(u32),
SetSourceVolume(u32),
Subscription(audio_client::Event),
Surface(surface::Action),
ToggleMediaControlsInTopPanel(bool),
TogglePopup,
ToggleSinkMute,
ToggleSourceMute,
SetDefaultSink(usize),
SetDefaultSource(usize),
OutputToggle,
InputToggle,
TogglePopup,
CloseRequested(window::Id),
ToggleMediaControlsInTopPanel(bool),
ConfigChanged(AudioAppletConfig),
Mpris(mpris_subscription::MprisUpdate),
MprisRequest(MprisRequest),
Token(TokenUpdate),
OpenSettings,
Subscription(css::Message),
Surface(surface::Action),
}
// TODO
@ -281,13 +242,19 @@ impl cosmic::Application for Audio {
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
let mut app = Self {
core,
..Default::default()
};
app.refresh_volume_limits();
let mut model = css::Model::default();
model.unplugged_text = "Unplugged".into();
model.hd_audio_text = "HD Audio".into();
model.usb_audio_text = "USB Audio".into();
(app, Task::none())
(
Self {
core,
model,
..Default::default()
},
Task::none(),
)
}
fn core(&self) -> &cosmic::app::Core {
@ -304,53 +271,35 @@ impl cosmic::Application for Audio {
fn update(&mut self, message: Message) -> app::Task<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::ReattachClient(client) => {
if let Some(client) = Arc::into_inner(client) {
self.audio_client = Some(client);
}
}
Message::Ignore => {}
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
return cosmic::surface::surface_task(cosmic::surface::action::destroy_popup(
p,
));
return destroy_popup(p);
} else {
return cosmic::surface::surface_task(cosmic::surface::action::app_popup(
|_| Default::default(),
|app: &mut Self| {
let new_id = window::Id::unique();
app.popup.replace(new_id);
let new_id = window::Id::unique();
self.popup.replace(new_id);
app.refresh_volume_limits();
(self.max_sink_volume, self.sink_breakpoints) = if amplification_sink() {
(150, &[100][..])
} else {
(100, &[][..])
};
let mut popup_settings = app.core.applet.get_popup_settings(
app.core.main_window_id().unwrap(),
new_id,
None,
None,
None,
);
if let Some(r) = app.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,
};
}
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(),
new_id,
None,
));
None,
None,
);
return get_popup(popup_settings);
}
}
@ -369,90 +318,52 @@ impl cosmic::Application for Audio {
}
}
Message::Subscription(message) => {
self.model.update(message);
return self
.model
.update(message)
.map(|message| Message::Subscription(message).into());
}
Message::SetDefaultSink(pos) => {
if let Some(&pos) = self.model.sinks.sorted_index.get(pos)
&& let Some(&node_id) = self.model.sinks.id.get(pos as usize)
&& let Some(client) = self.audio_client.as_mut()
{
futures::executor::block_on(async {
_ = client.conn.set_default(node_id, true).await;
});
}
return self
.model
.set_default_sink(pos)
.map(|message| Message::Subscription(message).into());
}
Message::SetDefaultSource(pos) => {
if let Some(&pos) = self.model.sources.sorted_index.get(pos)
&& let Some(&node_id) = self.model.sources.id.get(pos as usize)
&& let Some(client) = self.audio_client.as_mut()
{
futures::executor::block_on(async {
_ = client.conn.set_default(node_id, true).await;
});
}
return self
.model
.set_default_source(pos)
.map(|message| Message::Subscription(message).into());
}
Message::ToggleSinkMute => {
if let Some(ref mut client) = self.audio_client {
futures::executor::block_on(async {
_ = client.conn.sink_mute_toggle().await;
});
}
}
Message::ToggleSinkMute => self.model.toggle_sink_mute(),
Message::ToggleSourceMute => {
if let Some(ref mut client) = self.audio_client {
futures::executor::block_on(async {
_ = client.conn.source_mute_toggle().await;
});
}
}
Message::ToggleSourceMute => self.model.toggle_source_mute(),
Message::SetSinkVolume(volume) => {
self.applied_sink_volume
.store(volume, atomic::Ordering::Relaxed);
self.model.active_sink.volume = volume;
self.model.active_sink.volume_text = volume.to_string();
if let Some(mut client) = self.audio_client.take() {
let volume = Arc::clone(&self.applied_sink_volume);
return cosmic::Task::future(async move {
tokio::time::sleep(Duration::from_millis(128)).await;
_ = client
.conn
.set_sink_volume(volume.load(atomic::Ordering::Relaxed))
.await;
Message::ReattachClient(Arc::new(client)).into()
});
}
return self
.model
.set_sink_volume(volume)
.map(|message| Message::Subscription(message).into());
}
Message::SetSourceVolume(volume) => {
self.applied_source_volume
.store(volume, atomic::Ordering::Relaxed);
self.model.active_source.volume = volume;
self.model.active_source.volume_text = volume.to_string();
if let Some(mut client) = self.audio_client.take() {
let volume = Arc::clone(&self.applied_source_volume);
return cosmic::Task::future(async move {
tokio::time::sleep(Duration::from_millis(128)).await;
_ = client
.conn
.set_sink_volume(volume.load(atomic::Ordering::Relaxed))
.await;
Message::ReattachClient(Arc::new(client)).into()
});
}
return self
.model
.set_source_volume(volume)
.map(|message| Message::Subscription(message).into());
}
Message::ToggleMediaControlsInTopPanel(enabled) => {
self.config.show_media_controls_in_top_panel = enabled;
if let Ok(helper) =
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) => {
@ -550,12 +461,6 @@ impl cosmic::Application for Audio {
cosmic::app::Action::Surface(a),
));
}
Message::Client(client) => {
if let Some(client) = Arc::into_inner(client) {
self.audio_client = Some(client);
self.model = model::Model::default();
}
}
}
Task::none()
@ -571,50 +476,7 @@ impl cosmic::Application for Audio {
}),
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
activation_token_subscription(0).map(Message::Token),
Subscription::run(|| {
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)),
Subscription::run(|| css::watch().map(Message::Subscription)),
])
}
@ -625,94 +487,63 @@ impl cosmic::Application for Audio {
.icon_button(self.output_icon_name())
.on_press_down(Message::TogglePopup);
const WHEEL_STEP: f32 = 5.0; // 5% par cran logique
// Wayland axis_v120 envoie un cran physique en rafale de plusieurs
// ScrollDelta::Pixels (5–8 events ~15–20px), pour ~120px par cran. On
// accumule ces sub-events dans un thread_local et on n'émet qu'au
// passage d'un seuil — sinon `signum()` faisait croire que chaque
// sub-event = un cran, et un seul cran physique faisait chuter le
// volume jusqu'à 0 ("coupe le son").
const PIXEL_THRESHOLD: f32 = 15.0; // px par cran logique
std::thread_local! {
static PIXEL_ACC: std::cell::Cell<f32> = const { std::cell::Cell::new(0.0) };
}
const WHEEL_STEP: f32 = 5.0; // 5% per wheel event
let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
let scroll_vector = match delta {
iced::mouse::ScrollDelta::Lines { y, .. } => {
PIXEL_ACC.with(|a| a.set(0.0));
y * WHEEL_STEP
}
iced::mouse::ScrollDelta::Pixels { y, .. } => {
PIXEL_ACC.with(|acc_cell| {
let acc = acc_cell.get() + y;
let steps = (acc / PIXEL_THRESHOLD).trunc();
acc_cell.set(acc - steps * PIXEL_THRESHOLD);
steps * WHEEL_STEP
})
}
iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1
iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1
};
if scroll_vector == 0.0 {
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);
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| {
// if we have a configure for width and height, we're in a overflow popup
c.width > 0. && c.height > 0.
}))
.then(|| self.playback_buttons());
let mut ret = if let Some(playback_buttons) = playback_buttons
&& !playback_buttons.is_empty()
{
has_playback_buttons = true;
Element::from(match self.core.applet.anchor {
PanelAnchor::Left | PanelAnchor::Right => Element::from(
applet_column::Column::with_children(playback_buttons)
.push(btn)
.align_x(Alignment::Center)
.height(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
self.core
.applet
.autosize_window(
if let Some(playback_buttons) = playback_buttons
&& !playback_buttons.is_empty()
{
match self.core.applet.anchor {
PanelAnchor::Left | PanelAnchor::Right => Element::from(
applet_column::Column::with_children(playback_buttons)
.push(btn)
.align_x(Alignment::Center)
.height(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
),
),
),
PanelAnchor::Top | PanelAnchor::Bottom => {
applet_row::Row::with_children(playback_buttons)
.push(btn)
.align_y(Alignment::Center)
.width(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
)
.into()
}
})
} else {
btn.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
PanelAnchor::Top | PanelAnchor::Bottom => {
applet_row::Row::with_children(playback_buttons)
.push(btn)
.align_y(Alignment::Center)
.width(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
)
.into()
}
}
} else {
btn.into()
},
)
.into()
}
fn view_window(&self, _id: window::Id) -> Element<'_, Message> {
@ -720,21 +551,19 @@ impl cosmic::Application for Audio {
space_xxs, space_s, ..
} = theme::active().cosmic().spacing;
let sink: Option<&str> = self
let sink = self
.model
.sinks
.active()
.map(|pos| self.model.sinks.sorted_display[pos].as_ref());
let source: Option<&str> = self
.active_sink()
.and_then(|pos| self.model.sinks().get(pos));
let source = self
.model
.sources
.active()
.map(|pos| self.model.sources.sorted_display[pos].as_ref());
.active_source()
.and_then(|pos| self.model.sources().get(pos));
let mut audio_content = {
let output_slider = slider(
0..=self.max_sink_volume,
self.model.active_sink.volume,
self.model.sink_volume,
Message::SetSinkVolume,
)
.width(Length::FillPortion(5))
@ -742,7 +571,7 @@ impl cosmic::Application for Audio {
let input_slider = slider(
0..=self.max_source_volume,
self.model.active_source.volume,
self.model.source_volume,
Message::SetSourceVolume,
)
.width(Length::FillPortion(5))
@ -761,7 +590,7 @@ impl cosmic::Application for Audio {
.line_height(24)
.on_press(Message::ToggleSinkMute),
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))
.align_x(Alignment::End)
]
@ -780,7 +609,7 @@ impl cosmic::Application for Audio {
.line_height(24)
.on_press(Message::ToggleSourceMute),
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))
.align_x(Alignment::End)
]
@ -795,7 +624,7 @@ impl cosmic::Application for Audio {
Some(sink) => sink.to_owned(),
None => fl!("no-device"),
},
&self.model.sinks.sorted_display,
self.model.sinks(),
Message::OutputToggle,
Message::SetDefaultSink,
),
@ -806,7 +635,7 @@ impl cosmic::Application for Audio {
Some(source) => source.to_owned(),
None => fl!("no-device"),
},
&self.model.sources.sorted_display,
self.model.sources(),
Message::InputToggle,
Message::SetDefaultSource,
)
@ -862,15 +691,19 @@ impl cosmic::Application for Audio {
}
if let Some(play) = self.is_play() {
control_elements.push(
button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true))
.extra_small()
.class(cosmic::theme::Button::AppletIcon)
.on_press(if play {
Message::MprisRequest(MprisRequest::Play)
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
button::icon(
icon::from_name(if play { PLAY } else { PAUSE })
.size(32)
.symbolic(true),
)
.extra_small()
.class(cosmic::theme::Button::AppletIcon)
.on_press(if play {
Message::MprisRequest(MprisRequest::Play)
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
);
}
if let Some(go_next) = self.go_next(32) {
@ -921,21 +754,20 @@ impl cosmic::Application for Audio {
}
}
fn revealer<'a>(
fn revealer(
open: bool,
title: String,
selected: String,
devices: &'a [Arc<str>],
devices: &[String],
toggle: Message,
mut change: impl FnMut(usize) -> Message + 'static,
) -> widget::Column<'a, Message, crate::Theme, Renderer> {
) -> widget::Column<'static, Message, crate::Theme, Renderer> {
if open {
devices.iter().enumerate().fold(
devices.iter().cloned().enumerate().fold(
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
move |col, (id, name)| {
|col, (id, name)| {
col.push(
text::body(name.as_ref())
.apply(menu_button)
menu_button(text::body(name))
.on_press(change(id))
.width(Length::Fill)
.padding([8, 48]),
@ -953,9 +785,9 @@ fn revealer_head(
selected: String,
toggle: Message,
) -> cosmic::widget::Button<'static, Message> {
cosmic::widget::column::with_capacity(2)
.push(text::body(title).width(Length::Fill))
.push(text::caption(selected))
.apply(menu_button)
.on_press(toggle)
menu_button(column![
text::body(title).width(Length::Fill),
text::caption(selected),
])
.on_press(toggle)
}

View file

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

View file

@ -5,7 +5,7 @@ use cosmic::iced::core::Point;
use cosmic::iced::core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
event::Event,
event::{self, Event},
layout, mouse, overlay, renderer, touch,
widget::{Operation, Tree, tree},
};
@ -307,19 +307,6 @@ fn update<Message: Clone, Theme, Renderer>(
shell: &mut Shell<'_, Message>,
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 !state.is_out_of_bounds {
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;
}
}
}

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applet-battery"
version = "1.3.0"
version = "1.0.2"
edition = "2024"
license = "GPL-3.0-only"
@ -12,7 +12,7 @@ drm = "0.14.1"
futures.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
cosmic.workspace = true
libcosmic.workspace = true
rust-embed.workspace = true
rustc-hash.workspace = true
tokio.workspace = true
@ -24,7 +24,9 @@ zbus.workspace = true
serde.workspace = true
[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]
path = "../../cosmic-settings/subscriptions/settings-daemon"
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/settings-daemon"

View file

@ -1,14 +0,0 @@
power-settings = Strøm- og Batteriindstillinger...
hours = t
until-empty = indtil tom
battery-desc = Reduceret strømforbrug og ydeevne.
max-charge = Forøg dit batteris levetid, ved at angive en maksimal opladningsværdi på 80%
balanced = Balanceret
battery = Batteri
seconds = s
dgpu-running = Diskret GPU er aktiv og kan reducere batterilevetiden
performance = Høj Ydeevne
performance-desc = Høj ydeevne og strømforbrug.
minutes = m
balanced-desc = Standard ydeevne og batteriforbrug.
dgpu-applications = Applikationer som bruger { $gpu_name } diskret GPU

View file

@ -9,6 +9,6 @@ seconds = s
minutes = m
hours = h
until-empty = bis leer
power-settings = Energie- und Akkueinstellungen ...
power-settings = Energie- und Akkueinstellungen...
dgpu-running = Dedizierte GPU ist aktiv und kann die Akkulaufzeit reduzieren
dgpu-applications = Anwendungen, welche die dedizierte GPU { $gpu_name } nutzen

View file

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

View file

@ -1,14 +0,0 @@
power-settings = Power & Battery settings...
hours = h
until-empty = until empty
battery-desc = Reduced power usage and performance.
max-charge = Increase the lifespan of your battery by setting a maximum charge value of 80%
balanced = Balanced
battery = Battery
seconds = s
dgpu-running = Discrete GPU is active and can reduce battery life
performance = High Performance
performance-desc = High performance and power usage.
minutes = m
balanced-desc = Standard performance and battery usage.
dgpu-applications = Applications using { $gpu_name } discrete GPU

View file

@ -1,14 +0,0 @@
battery = Batería
battery-desc = Reduce o consumo de enerxía e o rendemento.
balanced = Balanceado
balanced-desc = Rendemento e consumo de batería estándar.
performance = Alto rendemento
performance-desc = Alto rendemento e maior consumo de enerxía.
max-charge = Aumenta a vida útil da batería limitando a carga máxima ao 80 %
seconds = s
minutes = m
hours = h
until-empty = ata baleirar
power-settings = Axustes de enexía e batería...
dgpu-running = A GPU dedicada está activa e pode reducir a duración da batería
dgpu-applications = Aplicacións que están a usar a GPU dedicada { $gpu_name }

View file

@ -1,13 +1,13 @@
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-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-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
seconds = mp
minutes = p
hours = ó
performance-desc = Nagy teljesítmény és energiafogyasztás
max-charge = Az akkumulátor élettartamának növelése érdekében állítsa a maximális töltési szintet 80%-ra
seconds = másodperc
minutes = perc
hours = óra
until-empty = a lemerülésig
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

View file

@ -1,14 +1,14 @@
battery = Batteria
battery-desc = Prestazioni e consumo energetico ridotti.
balanced = Equilibrato
balanced-desc = Prestazioni e consumo energetico normali.
performance = Prestazioni elevate
performance-desc = Prestazioni elevate e consumo energetico.
max-charge = Aumenta la durata della batteria impostando un valore massimo di carica dell’80%
battery = Risparmio energetico
battery-desc = Consumi minimi, performance ridotta.
balanced = Bilanciato
balanced-desc = Consumi bilanciati, performance dinamica.
performance = Performance
performance-desc = Consumi alti, performance massima.
max-charge = Migliora la durata di vita della batteria caricandola fino all'80%
seconds = s
minutes = m
hours = h
until-empty = finché non è vuoto
until-empty = fino alla scarica
power-settings = Impostazioni energetiche...
dgpu-running = La GPU discreta è attiva e può ridurre la durata della batteria
dgpu-applications = Applicazioni che stanno usando la GPU discreta { $gpu_name }

View file

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

View file

@ -1,14 +1,14 @@
battery = Economia de energia
battery = Bateria
battery-desc = Uso reduzido de energia e desempenho limitado.
balanced = Balanceado
balanced-desc = Desempenho padrão e uso de bateria moderado.
performance = Alto desempenho
performance = Alto Desempenho
performance-desc = Máximo desempenho e alto uso de energia.
max-charge = Aumente a vida útil da sua bateria definindo um valor máximo de carga de 80%
seconds = s
minutes = m
hours = h
until-empty = até ficar descarregada
until-empty = até esvaziar
power-settings = Configurações de energia e bateria...
dgpu-running = A GPU dedicada está ativa e pode reduzir a vida útil da bateria
dgpu-applications = Aplicativos usando a GPU dedicada { $gpu_name }
dgpu-running = GPU discreta está ativa e pode reduzir a vida útil da bateria
dgpu-applications = Aplicativos usando GPU discreta { $gpu_name }

View file

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

View file

@ -1,4 +1,4 @@
battery = 電池
battery = 省電
battery-desc = 降低效能與耗電量。
balanced = 平衡
balanced-desc = 標準效能與耗電量。
@ -6,9 +6,9 @@ performance = 高效能
performance-desc = 高效能與高耗電量。
max-charge = 將電池充電上限設定為 80%,以延長電池使用壽命
seconds = 秒
minutes = 分鍾
hours = 小時
minutes = 分
hours = 時
until-empty = 直到電量耗盡
power-settings = 電源與電池設定…
dgpu-running = 獨立顯示卡已啟用,可能會縮短電池續航力
dgpu-applications = 使用獨立顯示卡 { $gpu_name } 的應用程式
dgpu-applications = 使用 { $gpu_name } 獨立顯示卡的應用程式

View file

@ -40,7 +40,7 @@ use cosmic_settings_upower_subscription::{
};
use rustc_hash::FxHashMap;
use std::{path::PathBuf, time::Duration};
use std::{path::PathBuf, sync::LazyLock, time::Duration};
use tokio::sync::mpsc::UnboundedSender;
// XXX improve
@ -169,24 +169,9 @@ impl CosmicBatteryApplet {
self.charging_limit = Some(limit);
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)]
#[allow(dead_code)]
enum Message {
TogglePopup,
CloseRequested(window::Id),
@ -366,35 +351,23 @@ impl cosmic::Application for CosmicBatteryApplet {
let _ = tx.send(KeyboardBacklightRequest::Get);
}
let new_id = window::Id::unique();
self.popup.replace(new_id);
let popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(),
new_id,
Some((1, 1)),
None,
None,
);
if let Some(tx) = self.power_profile_sender.as_ref() {
let _ = tx.send(PowerProfileRequest::Get);
}
if let Some(tx) = self.update_trigger.as_ref() {
let _ = tx.send(());
}
let mut tasks = vec![cosmic::surface::surface_task(
cosmic::surface::action::app_popup(
|_| Default::default(),
|app: &mut Self| {
let new_id = window::Id::unique();
app.popup.replace(new_id);
let popup_settings = app.core.applet.get_popup_settings(
app.core.main_window_id().unwrap(),
new_id,
Some((1, 1)),
None,
None,
);
popup_settings
},
None,
),
)];
if let Some(tx) = &self.settings_daemon_sender {
let _ = tx.send(settings_daemon::Request::GetDisplayBrightness);
let _ = tx.send(settings_daemon::Request::GetMaxDisplayBrightness);
}
let mut tasks = vec![get_popup(popup_settings)];
// Try again every time a popup is opened
if self.charging_limit.is_none() {
tasks.push(Task::perform(get_charging_limit(), |limit| {
@ -534,7 +507,7 @@ impl cosmic::Application for CosmicBatteryApplet {
Task::none()
}
fn view(&self) -> Element<'_, Message> {
fn view(&self) -> Element<Message> {
let is_horizontal = match self.core.applet.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => true,
PanelAnchor::Left | PanelAnchor::Right => false,
@ -544,9 +517,19 @@ impl cosmic::Application for CosmicBatteryApplet {
let applet_padding = self.core.applet.suggested_padding(true);
let mut children = vec![
icon::from_name(self.panel_icon_name())
.size(suggested_size.0)
.into(),
icon::from_name(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 {
self.icon_name.as_str()
}
} else {
self.icon_name.as_str()
})
.size(suggested_size.0)
.into(),
];
if self.config.show_percentage && !self.no_battery {

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applet-bluetooth"
version = "1.3.0"
version = "1.0.2"
edition = "2024"
license = "GPL-3.0-only"
@ -10,7 +10,7 @@ bluer = { version = "0.17", features = ["bluetoothd", "id"] }
futures.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
cosmic.workspace = true
libcosmic.workspace = true
fastrand = "2.3.0"
rust-embed.workspace = true
rustc-hash.workspace = true

View file

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

View file

@ -1,12 +0,0 @@
cancel = Afbryd
confirm-pin = Bekræft venligst, at følgende pinkode matcher den, der vises på { $deviceName }
bluetooth = Bluetooth
unsuccessful = Parring Mislykkedes
pairable = Parbare
try-again = Prøv Igen
check-device = Sørg for, at { $deviceName } er slået til, inden for rækkevidde og klar til parring.
other-devices = Andre Bluetooth-enheder
connected = Forbundet
confirm = Bekræft
settings = Bluetooth-indstillinger...
discoverable = Synlig

View file

@ -1,6 +1,6 @@
bluetooth = Bluetooth
other-devices = Andere Bluetooth-Geräte
settings = Bluetooth-Einstellungen ...
settings = Bluetooth-Einstellungen...
connected = Verbunden
confirm-pin = Bitte bestätige, dass die folgende PIN mit der auf { $deviceName } angezeigten PIN übereinstimmt
confirm = Bestätigen

View file

@ -1,12 +1,2 @@
cancel = Ακύρωση
confirm = Επιβεβαίωση
bluetooth = Bluetooth
confirm-pin = Επιβεβαιώστε ότι το ακόλουθο PIN είναι ίδιο με αυτό που εμφανίζεται στο { $deviceName }
unsuccessful = Ανεπιτυχής σύζευξη
pairable = Συνδέσιμο
try-again = Δοκιμή ξανά
check-device = Βεβαιωθείτε ότι το { $deviceName } είναι ενεργοποιημένο, βρίσκεται εντός εμβέλειας και είναι έτοιμο για σύζευξη.
other-devices = Άλλες συσκευές Bluetooth
connected = Συνδέθηκε
settings = Ρυθμίσεις Bluetooth...
discoverable = Ορατό

View file

@ -1,12 +0,0 @@
cancel = Cancel
confirm-pin = Please confirm that the following PIN matches the one displayed on { $deviceName }
bluetooth = Bluetooth
unsuccessful = Pairing Unsuccessful
pairable = Pairable
try-again = Try Again
check-device = Make sure { $deviceName } is turned on, in range, and is ready to pair.
other-devices = Other Bluetooth devices
connected = Connected
confirm = Confirm
settings = Bluetooth settings...
discoverable = Discoverable

View file

@ -2,11 +2,11 @@ bluetooth = بلوتوث
other-devices = سایر دستگاه‌های بلوتوث
settings = تنظیمات بلوتوث...
connected = متصل
confirm-pin = لطفاً تأیید کنید که پین زیر با پین نمایش داده شده در { $deviceName } یکسان است.
confirm-pin = لطفاً تأیید کنید که پین زیر با پین نمایش داده شده در {$deviceName} یکسان است.
confirm = تأیید
cancel = لغو
unsuccessful = جفت‌سازی ناموفق بود
check-device = مطمئن شوید که { $deviceName } روشن، در محدوده و آماده جفت‌سازی است.
check-device = مطمئن شوید که {$deviceName} روشن، در محدوده و آماده جفت‌سازی است.
try-again = تلاش دوباره
discoverable = قابل شناسایی
pairable = قابل جفت‌سازی

View file

@ -1,12 +0,0 @@
cancel = Cancelar
bluetooth = Bluetooth
other-devices = Outros dispositivos Bluetooth
settings = Axustes Bluetooth…
connected = Conectado
confirm-pin = Por favor, confirme que o seguinte PIN coincide co que se mostra en{ $deviceName }
confirm = Confirmar
unsuccessful = Enparellamento non exitoso
check-device = Asegurate de que { $deviceName } está acendido, dentro do alcance e listo para emparellar.
try-again = Reintentar
discoverable = Atopable
pairable = Emparellable

View file

@ -2,11 +2,11 @@ bluetooth = ब्लूटूथ
other-devices = अन्य ब्लूटूथ उपकरण
settings = ब्लूटूथ सेटिंग्स...
connected = जुड़े हुए
confirm-pin = कृपया सुनिश्चित करें कि निम्नलिखित PIN { $deviceName } पर प्रदर्शित PIN से मेल खाता है
confirm-pin = कृपया सुनिश्चित करें कि निम्नलिखित PIN {$deviceName} पर प्रदर्शित PIN से मेल खाता है
confirm = पुष्टि करें
cancel = रद्द करें
unsuccessful = पेयरिंग असफल
check-device = सुनिश्चित करें कि { $deviceName } चालू है, रेंज में है, और पेयर करने के लिए तैयार है।
check-device = सुनिश्चित करें कि {$deviceName} चालू है, रेंज में है, और पेयर करने के लिए तैयार है।
try-again = फिर से प्रयास करें
discoverable = खोजने योग्य
pairable = पेयर करने योग्य

View file

@ -1,7 +1,7 @@
bluetooth = Bluetooth
other-devices = Egyéb Bluetooth-eszközök
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 = Megerősítés
cancel = Mégse

View file

@ -2,11 +2,11 @@ bluetooth = Bluetooth
other-devices = Altri dispositivi Bluetooth
settings = Impostazioni Bluetooth...
connected = Connesso
confirm-pin = Per favore conferma che il seguente PIN corrisponda a quello visualizzato nel dispositivo { $deviceName }
confirm-pin = Per favore conferma che il seguente PIN corrisponda a quello visualizzato nel dispositivo {$deviceName}
confirm = Conferma
cancel = Annulla
unsuccessful = Accoppiamento fallito
check-device = Assicurati che il dispositivo { $deviceName } sia acceso, a portata e pronto per l'accoppiamento.
check-device = Assicurati che il dispositivo {$deviceName} sia acceso, a portata e pronto per l'accoppiamento.
try-again = Riprova
discoverable = Visibile
pairable = Accoppiabile
pairable = Accoppiabile

View file

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

View file

@ -1 +0,0 @@
cancel = ᱵᱟᱹᱛᱤᱞ

Some files were not shown because too many files have changed in this diff Show more