Compare commits

..

98 commits

Author SHA1 Message Date
badbe2d009 chore: fige Cargo.lock tel que resolu apres le merge 1.7.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 07:58:51 +02:00
29ed971e64 Merge tag 'epoch-1.7.0' into local/yoda-main-1.3.0
Epoch 1.7.0

# Conflicts:
#	Cargo.lock
2026-08-27 07:52:54 +02:00
Michael Aaron Murphy
fffa4f26cb fix(libcosmic): keyboard focus not visible in nav bar until pointer moves 2026-08-25 21:26:14 +02:00
Alvin Nguyen
2c2dc485aa
feat(bluetooth): add back navigation for multiple Bluetooth adapters (#2154) 2026-08-25 18:02:31 +02:00
Michael Aaron Murphy
98bc6ee32e Epoch 1.7.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-08-25 16:56:55 +02:00
nicole mikołajczyk
d5da6a476d
chore(input): remove unimplemented menu items (#2169) 2026-08-25 02:32:46 +02:00
Michael Murphy
6a9b711a22
i18n: translation update from Hosted Weblate (#2153)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Settings](https://hosted.weblate.org/projects/pop-os/cosmic-settings/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-settings/matrix-auto.svg)
2026-08-24 20:48:52 +02:00
Hosted Weblate
50f85d0f2d
Merge remote-tracking branch 'origin/master' 2026-08-24 20:14:21 +02:00
Hosted Weblate
6612193d74
i18n: translation updates from weblate
Co-authored-by: Alex <aleha@users.noreply.hosted.weblate.org>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hant/
Translation: Pop OS/COSMIC Settings
2026-08-24 20:14:10 +02:00
Jacob Kauffmann
81f973cb26 chore: cargo fmt 2026-08-24 20:09:29 +02:00
nicole mikołajczyk
72f366d8db input: mark move up/down disabled for first/last keyboard laout disabled
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
2026-08-24 20:09:29 +02:00
Hojjat
c7481ba336 chore: update cosmic-comp-config 2026-08-21 20:43:15 +02:00
Hojjat
6e5b09c0d7 feat: add option to toggle shake cursor to find it 2026-08-21 20:43:15 +02:00
fc630ae795 chore: fige Cargo.lock tel que resolu avec les dependances locales
Resolution obtenue avec les patchs de chemin du workspace (libcosmic-yoda,
cosmic-protocols, window_clipboard, cosmic-text). Pas de changement de code,
uniquement le verrou remis en coherence avec l'arbre local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 09:54:41 +02:00
Alexander Anthis
2b43c925a0
fix(displays): pass current refresh rate to cosmic-randr when changing monitor position (#2139) 2026-08-20 23:03:20 +02:00
02eccba3f8 Merge remote-tracking branch 'upstream/master' into local/yoda-main-1.3.0
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	cosmic-settings/Cargo.toml
2026-08-19 21:58:13 +02:00
Michael Murphy
69a4311903
i18n: translation update from Hosted Weblate (#2121) 2026-08-18 14:08:00 +02:00
Hosted Weblate
df752940d8
Merge remote-tracking branch 'origin/master' 2026-08-18 11:10:16 +02:00
Hosted Weblate
ee8ad2c187
i18n: translation updates from weblate
Co-authored-by: Ahrakiel <christopherbatrier@gmail.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Andi Chandler <andi@gowling.com>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jens Peter Nielsen <mail@jp-nielsen.dk>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Martin Meizoso <martinmeizoso@gmail.com>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: Nara Díaz Viñolas <nara.diaz.vinolas@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
Co-authored-by: Sproston <ojspr2007@gmail.com>
Co-authored-by: Steen Rabol <steen@rabol.dev>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: Tristan B. <tristanb0@users.noreply.hosted.weblate.org>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Wilbur Lindqvist <william@willmantrade.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lein <cayin0219@outlook.com>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: reducedradius <137701630+flytothehighest@users.noreply.github.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: the-forest-watches <lucastronks@protonmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: кубик круглый <megarainbow29@gmail.com>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/da/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/en_GB/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/gl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/it/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/nl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hant/
Translation: Pop OS/COSMIC Settings
2026-08-18 11:09:57 +02:00
Alexander Bachmann
6b0164ddaa
feat(sound): per-application volume control (#2138) 2026-08-18 04:20:32 +02:00
981e63a154 feat(printers): niveaux d'encre interroges directement sur l'imprimante
La file driverless ne relaie pas marker-levels: le backend resout l'URI dnssd en point d'acces IPP via ippfind puis lit marker-names/levels/colors via ipptool (timeout 8 s). Le tiroir affiche pastille de couleur, jauge et pourcentage par cartouche; bloc masque si l'imprimante n'expose rien.
2026-08-08 17:01:41 +02:00
c2c7d9ccb1 feat(printers): types de papier et mode de couleur par defaut
Le tiroir de details expose desormais MediaType et ColorModel en plus de PageSize, cupsPrintQuality et Duplex, avec des libellets localises pour les valeurs usuelles (papier ordinaire, photo brillante/mate/satinee, couleur, niveaux de gris...). Persiste via lpadmin -o comme les autres options.
2026-08-08 16:42:48 +02:00
8d528f9e45 feat(printers): options d'impression par defaut (format, qualite, recto-verso)
Le tiroir de details charge les capacites reelles de la file (lpoptions -l) et expose des menus deroulants PageSize, cupsPrintQuality et Duplex. Chaque selection est persistee comme defaut systeme de la file via lpadmin -o puis relue. Les options non supportees par un pilote sont masquees.
2026-08-08 13:34:01 +02:00
aec716fa6a feat(printers): refonte en tiroir de details, filtre des destinations decouvertes, peripheriques dedoublonnes
Interface: liste unique avec etat vide, details/actions/travaux dans un tiroir lateral au titre court (le nom long debordait sur le bouton Fermer). Backend: exclusion des destinations decouvertes dnssd (bit CUPS_PRINTER_DISCOVERED du printer-type) qui ne sont pas de vraies files, dedoublonnage des peripheriques par libelle avec preference dnssd, noms propres pour tous les schemas, suppression tolere une file deja disparue, page de test via lp (auto-detection du template PDF-BANNER).
2026-08-08 12:53:27 +02:00
d5e57cc439 fix(printers): page de test en auto-detection MIME, noms sanitaires compacts, actions en flex_row
La page de test est un template de banniere cups-filters (#PDF-BANNER), pas du PostScript: envoi en application/octet-stream pour laisser CUPS detecter le format. Le sanitiseur supprime crochets/parentheses et compacte les underscores (series [32C998] -> series_32C998). Les actions des details passent en flex_row pour eviter le debordement des boutons; bouton de confirmation du dialogue raccourci.
2026-08-08 10:55:27 +02:00
26dcaa6fd3 feat(printers): page native de gestion des imprimantes (cups_rs + CUPS CLI)
Nouvelle page Imprimantes: liste et etat des files CUPS, ajout (decouverte lpinfo, pilote driverless IPP Everywhere par defaut), suppression, imprimante par defaut, suspension/reprise, page de test et file des travaux avec annulation. Lectures via cups_rs (FFI libcups serialisee), mutations via lpadmin/cupsenable. i18n en/fr, sous-commande 'cosmic-settings printers'.
2026-08-08 10:24:38 +02:00
Alexander Anthis
209ea08f03
build(nix): add dav1d to flake and update flake lock 2026-08-07 20:44:49 +02:00
f9b27c8d4d window-management: restaurer le toggle boutons a gauche (style macOS)
Reprend le contenu de 52ce463 (22 avril), present sur local/yoda-main
mais jamais reporte sur local/yoda-main-1.3.0. La branche 1.3.0 a ete
coupee sans ce commit, si bien que le paquet construit le 31 juillet ne
contenait plus l'option : `strings /usr/bin/cosmic-settings` ne trouvait
aucune occurrence de position-start.

Retablit dans Bureau > Gestion des fenetres > Controles des fenetres le
bouton « Placer les boutons a gauche (style macOS) », le variant de
message WindowControlsPositionStart qui ecrit Start ou End dans le
CosmicTk (com.system76.CosmicTk), et la cle fluent
window-controls.position-start en en et fr.

Le Cargo.toml de 52ce463 n'est volontairement pas repris : son override
[patch] vers libcosmic est obsolete depuis a44a022, qui resout la
dependance via libcosmic-yoda sous le nom externe `cosmic`. L'API cote
libcosmic est intacte (WindowControlsPosition exporte par widget,
config::window_controls_position()), cargo check passe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:40:34 +02:00
a44a022144 fix: depend on libcosmic-yoda under the cosmic extern name
The local libcosmic fork renamed its package to libcosmic-yoda, but this
workspace still asked for `libcosmic` by path, so the whole workspace
failed to resolve.

Naming the dependency key `libcosmic` with `package = "libcosmic-yoda"`
is not enough: a renamed dependency takes its key as the extern name, so
the 67 source files that `use cosmic::` would stop compiling. Key the
dependency `cosmic` instead, matching every other repo in the local
stack, and update the `libcosmic/*` feature strings accordingly.

Also carried in this commit, from earlier work in the tree: the sctk
workspace dependency pinned back to smithay-client-toolkit 0.20.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:14:23 +02:00
manascb1344
823ef166f9
fix(search): don't unload active page during search 2026-07-29 18:24:38 +02:00
akrm al-hakimi
7287257ec9
fix(networking): stop storing the VPN username as a secret (#2116)
This PRs diff fixes #2109 such that a VPN username now gets written to
`vpn.data`, where NetworkManager and the VPN plugins actually read it
from. The existing `vpn.data` dict is read and merged rather than
replaced, so unrelated keys like remote and ca survive the update.

Profiles saved by earlier versions already have the username sitting in
the keyring, so the secret agent filters it out on read as well. Without
that, the stale entry keeps getting replayed to NetworkManager and
affected connections stay broken after upgrading.
2026-07-29 00:29:19 +02:00
Hojjat
25cc9ef31b fix: one pixel gap between outputs due to rounding error 2026-07-28 23:52:32 +02:00
Michael Aaron Murphy
4841c87622 feat(libcosmic): slider widget handle improvements 2026-07-28 23:00:19 +02:00
Michael Aaron Murphy
3fa8d4c322 chore: update dependencies 2026-07-28 23:00:19 +02:00
Michael Murphy
3135b49910
i18n: translation update from Hosted Weblate (#2046)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Settings](https://hosted.weblate.org/projects/pop-os/cosmic-settings/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-settings/horizontal-auto.svg)
2026-07-28 16:43:17 +02:00
Hosted Weblate
fb47e39026
i18n: translation updates from weblate
Co-authored-by: Ahrakiel <christopherbatrier@gmail.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Aman Alam <aalam@users.noreply.hosted.weblate.org>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Kartik Nayak <65574961+kodebarista@users.noreply.github.com>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: Nara Díaz Viñolas <nara.diaz.vinolas@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Saeed <s.zare.org.82@gmail.com>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: Tristan B. <tristanb0@users.noreply.hosted.weblate.org>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Vilius Paliokas <viliuspaliokas@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: reducedradius <137701630+flytothehighest@users.noreply.github.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: кубик круглый <megarainbow29@gmail.com>
Co-authored-by: 麋麓 BigELK176 <BigELK176@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pa/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sat/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hant/
Translation: Pop OS/COSMIC Settings
2026-07-28 07:03:17 +02:00
Emin Can Özen
ca9b7aa138 fix(wallpaper): allow adding multiple images at once
The 'Add image' dialog previously used 'open_file()', which only
accepted a single selection. Switching to 'open_files()' lets users
select multiple wallpaper images at once.

Changes:
- Rename 'AddFile' to 'AddFiles' to reflect new behavior
- Each selected image is loaded and added as batched via 'Task::batch'
- Introduce 'unwrap_dialog_response<T>' helper.

Closes #2077
2026-07-24 19:28:23 +02:00
Leandro Pérez G.
65d0f0c7c8
feat(wallpaper): apply EXIF orientation to wallpapers (#2075)
Same fix as pop-os/cosmic-bg#142, which was asked for there.

`open_image` has two decoding paths, and neither applies the Exif
orientation:
`ImageReader::decode` for the common formats, and
`DynamicImage::from_decoder` in
`decode_jpegxl`. Cameras store portrait photos with the pixels laid out
landscape
and record the rotation in the Exif metadata, so such a photo is shown
rotated in
the wallpaper page, both in the preview and in the thumbnails.

Both paths now read the orientation from the decoder and apply it to the
decoded
image. `into_decoder` does not check the allocation limit against the
size of the
decoded image the way `decode` does, so that check is kept explicitly.

Tested with the 8 Exif orientation values, with images that carry no
Exif metadata
(unchanged), and with the JPEG XL wallpapers from gnome-backgrounds,
which decode
identically.

cosmic-greeter needs a different change: it does not decode images
itself. It
passes the raw file bytes to `widget::image::Handle::from_bytes`
(src/common.rs),
and those are decoded by `iced_graphics` with `image::load_from_memory`,
which
does not apply the orientation either. So the orientation has to be
applied before
the handle is built, or in the toolkit.

- [x] I have disclosed use of any AI generated code in my commit
messages.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

Signed-off-by: Leandro Pérez G. <leandropatodo@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 17:24:25 +02:00
Salvatore Gabriele La Greca
044fc11b2f fix: increased hard-coded wallpapers limit from 100 to 500 2026-07-22 15:41:20 -06:00
Jacob Kauffmann
cc597685f8
docs (QA): Add Sound page tests (#2099)
https://github.com/pop-os/cosmic-settings/issues/2097 really should have
been caught by QA. It appears only the standard regression testing
checklist was performed for
https://github.com/pop-os/cosmic-settings/pull/2063, even though that PR
only modified the Sound page, and we had nothing in the standard
checklist for the Sound page.

To rectify that, this adds a few basic smoke tests for the Sound page in
the standard checklist, to hopefully prevent this from happening again.
(I know we've had this particular problem pop up at least once before,
too, so both sliders working independently is a good thing to have in
the checklist.)

---

- [X] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [X] I understand these changes in full and will be able to respond to
review comments.
- [X] My change is accurately described in the commit message.
- [X] My contribution is tested and working as described.
- [X] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-07-22 12:35:08 -06:00
0186f5de15 chore: bump version to 1.3.0 2026-07-22 15:56:59 +02:00
Michael Aaron Murphy
c2624965bc fix(sound): source volume changes applied to sink 2026-07-21 11:15:52 -06:00
Hojjat
ab5dd5f43a fix: preserve content when rebuilding appearance drawer 2026-07-17 19:29:06 +02:00
jklgrasso
99cf076229 Update cosmic-protocols dep 2026-07-14 12:11:48 -04:00
Akrm Al-Hakimi
04d5f46d0d feat(networking): replace custom NetworkManager backend with nmrs
Switch the networking pages to use nmrs 3.4.0 for NetworkManager
state, device lists, Wi-Fi operations, VPN imports/activation, and
secret-agent handling.

Remove the old cosmic-settings-network-manager-subscription crate,
drop the direct dbus-settings NetworkManager dependencies, and delete
the unused nmcli VPN helper. Keep nm-connection-editor for profile
creation/editing where the UI still depends on it.

Carry Wi-Fi security metadata through the password flow so SAE/WPA3
personal networks can be activated with SAE key management, and make
failed/cancelled auth attempts clear pending connection state
2026-07-14 12:11:48 -04:00
Ashley Wulber
fff1485876
feat: reapply frosted glass feature 2026-07-10 19:06:17 +02:00
Jeremy Soller
ae5c750662
chore: update cosmic-panel-config (#2058)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

Updated to work with https://github.com/pop-os/cosmic-panel/pull/627 and
should be tested with that to ensure everything works and is backwards
compatible.
2026-07-09 11:27:21 -06:00
Vukašin Vojinović
ccbd31f67c chore: update cosmic-panel-config 2026-07-09 17:15:58 +02:00
Hojjat
906725fd56 Revert "feat: frosted glass"
This reverts commit bce5164eea.
2026-07-08 18:06:46 -04:00
Hojjat
8393239125 Revert "fix: update iced"
This reverts commit 2d3bae47bb.
2026-07-08 18:06:46 -04:00
Victoria Brekenfeld
6202344c26 pages/display: Fix reverting orientation changes 2026-07-08 23:34:03 +02:00
Michael Aaron Murphy
b34d204a18 fix(sound): throttle volume sliders by 128ms 2026-07-08 22:20:12 +02:00
Michael Aaron Murphy
d971176aca build: make avif feature optional but enable by default 2026-07-07 16:34:25 -06:00
Michael Aaron Murphy
4d3915c1d7 feat(wallpapers): enable avif support via libdav1d 2026-07-07 16:34:25 -06:00
Jeremy Soller
c46e46b8e3
feat: frosted glass (#2062)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

This should be merged last
2026-07-07 14:19:50 -06:00
Ashley Wulber
2d3bae47bb fix: update iced
popup blur & corner cleanup
2026-07-06 19:50:38 -04:00
Ashley Wulber
bce5164eea feat: frosted glass 2026-07-06 09:46:48 -04:00
Lysander Treumann
9f9311235e
i18n(en): change super-key-action disable to none
=
2026-06-24 23:06:14 +02:00
Veracion
dec2361a40
feat(keyboard): make key names for special character keys translatable 2026-06-24 22:46:03 +02:00
Anthony Lannutti
3bef2b08da
fix(network): validate MAC address length when parsing addresses from network manager
This fixes a bug in the HwAddress struct that allowed invalid MAC addresses to pass validation.

---

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-06-24 17:10:28 +02:00
Michael Aaron Murphy
84729d1dcd
Merge remote-tracking branch 'weblate/master' 2026-06-23 20:19:26 +02:00
Hosted Weblate
ba67d30ce8
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Lysander Treumann <lysander.treumann@gmx.de>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hant/
Translation: Pop OS/COSMIC Settings
2026-06-23 20:03:07 +02:00
Michael Murphy
a4d864dd8f
i18n: translation update from Hosted Weblate (#2016)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Settings](https://hosted.weblate.org/projects/pop-os/cosmic-settings/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-settings/horizontal-auto.svg)
2026-06-23 17:11:52 +02:00
Hosted Weblate
9876623f74
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Arve Eriksson <arve.eriksson@startmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Lysander Treumann <lysander.treumann@gmx.de>
Co-authored-by: Mattias Eriksson <snaggen@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: taijuin Lee <taijuin@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hant/
Translation: Pop OS/COSMIC Settings
2026-06-22 14:03:40 +02:00
Michael Aaron Murphy
5c23ec1b1a fix(default_apps): set text editor associations for application/x-zerosize 2026-06-18 12:12:00 -04:00
JADella94
1dd478aeb2
fix(vpn): handle non-file url on openvpn import
So openvpn import called to_file_path().unwrap(), which panics when the
file picker hands back a non-file:// url (sandboxed portal, etc). the
wireguard branch right above already handles this gracefully, so do the
same: return a Message::Error with a new OpenVpnConfigPath kind instead
of crashing.

Fixes #1896

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

Signed-off-by: James A DellaMorte <dellamorte.james@comcast.net>
Co-authored-by: James A DellaMorte <dellamorte.james@comcast.net>
2026-06-16 22:22:02 +02:00
Michael Murphy
9155a1e902
feat(sound): use cosmic-settings-daemon's varlink API 2026-06-15 21:09:03 +02:00
Ashley Wulber
3a77442dbc fix: check if action is already found before adding it to search results 2026-06-08 11:26:16 -04:00
Ashley Wulber
8cdf308db5 fix: focus inputs on open of context drawer 2026-06-05 15:37:44 -04:00
Anthony Lannutti
4ae20bc1db
feat: add openrc cargo feature for OpenRC bluetooth service management
Coding-Agent: Claude Code
Model: claude-sonnet-4-5
2026-06-04 20:34:57 +02:00
Steven Ceuppens
989a56346a
fix(bluetooth): add passkey/PIN display dialogs for keyboard pairing
Bluetooth keyboards require the host to display a passkey for the user
to type. Previously this was not supported because the agent capability
was set to DisplayYesNo and the DisplayPasskey/DisplayPinCode events
were not handled.

- Change agent capability to KeyboardDisplay
- Add DisplayPasskey dialog with typed-key progress indicator
- Add DisplayPinCode dialog for older keyboards
- Auto-dismiss display dialogs when device reports Paired=true
2026-06-03 23:24:04 +02:00
Michael Aaron Murphy
de5e16d360 fix(default-apps): additional missing apps 2026-06-02 16:30:23 +02:00
Michael Aaron Murphy
2e83e56c09 fix(default-apps): missing and wrong associations
* update the cosmic-mime-apps crate with fixes to app association detection
* actually use cosmic-mime-apps for significantly faster  association lookups
2026-06-01 17:27:28 +02:00
Hojjat
81912bed6c fix: wait for cosmic-settings-daemon and send initial max-brightness 2026-05-28 19:09:55 +02:00
Jeremy Soller
60a7de5adb
i18n: translation update from Hosted Weblate (#2005)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Settings](https://hosted.weblate.org/projects/pop-os/cosmic-settings/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-settings/horizontal-auto.svg)
2026-05-26 09:11:25 -06:00
Hosted Weblate
7e54af1f5b
i18n: translation updates from weblate
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Lysander Treumann <lysander.treumann@gmx.de>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ga/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translation: Pop OS/COSMIC Settings
2026-05-25 21:11:40 +02:00
Vukašin Vojinović
f19da76129 improv(panel): corner radius handling
This ensures that the padding of a panel is taken into account when calculating corner_radius (except when Square), and also prevents various visual issues, like slightly round without gaps having a corner radius of 0.

Also extracts panel config fetching into a helper function.
2026-05-19 18:36:07 +02:00
manascb1344
1f225d19ce refactor: extract sorting logic for startup applications and all apps
- Introduced a new function `sort_entries_by_name` to encapsulate the sorting logic for desktop entries by name, improving code readability and maintainability.
- Updated existing sorting implementations to use the new function, ensuring consistent behavior across the application.
2026-05-19 13:13:34 +02:00
manascb1344
d6bf77bf3a sort startup applications and all apps by name for improved organization 2026-05-19 13:13:34 +02:00
Vukašin Vojinović
6db1e6a17b fix(sound): use go_next_item for device profiles button 2026-05-15 16:26:20 -06:00
Vukašin Vojinović
4b1f388a4f chore: add rustfmt config 2026-05-15 16:26:20 -06:00
Vukašin Vojinović
cb4a0d3a3a chore: update libcosmic 2026-05-15 16:26:20 -06:00
manascb1344
f2c374ff6b Sort VPN connections alphabetically
Adds case-insensitive alphabetical sorting to the VPN connections list.
The connections are now sorted by name when received and before display,
making it easier to find specific VPN profiles.

Fixes #1947
2026-05-15 09:22:51 -06:00
Ashley Wulber
703a934b09 fix: retry connection to settings daemon and allow requests for brightness values 2026-05-12 16:22:00 -04:00
Michael Murphy
a96987091e
i18n: translation update from Hosted Weblate (#1963)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Settings](https://hosted.weblate.org/projects/pop-os/cosmic-settings/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-settings/horizontal-auto.svg)
2026-05-12 17:01:20 +02:00
Hosted Weblate
9b7a9538e6
i18n: translation updates from weblate
Co-authored-by: Baurzhan Muftakhidinov <baurthefirst@gmail.com>
Co-authored-by: BoneNI <bounkirdni@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: David Carvalho <david.snt.carvalho@gmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jim Spentzos <jimspentzos2000@gmail.com>
Co-authored-by: Jiri Grönroos <jiri.gronroos@iki.fi>
Co-authored-by: Julien Brouillard <julienbrouillard1@gmail.com>
Co-authored-by: Jun Hwi Ku <siguning@gmail.com>
Co-authored-by: Nara Díaz Viñolas <nara.diaz.vinolas@gmail.com>
Co-authored-by: Tadas Misiūnas <tadujo@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: jonnysemon <jonnysemon@users.noreply.hosted.weblate.org>
Co-authored-by: lorduskordus <lorduskordus@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: Марко М. Костић <marko.m.kostic@gmail.com>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ar/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/cs/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/de/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fi/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/lt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-settings/zh_Hant/
Translation: Pop OS/COSMIC Settings
2026-05-12 11:11:45 +00:00
manascb1344
d632047ab3 fix(wifi): remove misleading search placeholder 2026-05-12 10:31:19 +02:00
Jeremy Soller
cb58af2be4
chore: add defaults (#1994)
- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-05-04 12:53:00 -06:00
Ashley Wulber
d49a1d1990 chore: add defaults 2026-05-04 13:15:57 -04:00
Jeremy Soller
1a44f8634f
Update libcosmic (#1985)
- Contains fix for wgpu invalidation error with float16 shader
- Updates MSRV to Rust 1.93
- Updates wgpu to v28 & cosmic-text to v0.19

---

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.
2026-04-27 12:32:57 -06:00
Michael Aaron Murphy
f4c7a2c14d
chore: bump version to 1.0.12 2026-04-27 15:29:49 +02:00
Michael Aaron Murphy
c882df3d20
chore: update libcosmic & iced
- Contains fix for wgpu validation error on float16 shader with older
  GPUs.
- Updates wgpu to 28 and cosmic-text to 0.19
- Misc. fixes and improvements for libcosmic widgets
2026-04-27 15:29:44 +02:00
Michael Aaron Murphy
e4c4c0d903
build: set MSRV to Rust 1.93 2026-04-27 15:08:13 +02:00
Anthony Lannutti
fa085f9006
fix: set locale with DBus to fix OpenRC support
This PR is intended to address #1955. At a high level, when using OpenRC with `openrc-settingsd`, the application would panic after trying to run the non-existent `localectl` command. This PR addresses that issue by setting the locale via D-Bus instead of through `localectl`. Additionally, there was a call to `localectl` to get the available locales for the system that was replaced with the more portable and POSIX compliant `locale` command.

- [x] I have disclosed use of any AI generated code in my commit messages.
- [x] I understand these changes in full and will be able to respond to review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.

---
AI Disclosure: This code was generated with the assistance of AI (Claude 3.7 Sonnet)
under human direction and supervision. All code has been reviewed and tested.
2026-04-25 00:07:15 +02:00
Ashley Wulber
78644a32e3
Revert "Changes the PageInner.size field to have no explicit default.… (#1977)
… It is only initialized (not None) when PageInner.panel_config is
initialized. This implementation fails faster and more verbose, when
program is in an unintended state."

This reverts commit ba5fb65b44.

- [x] I have disclosed use of any AI generated code in my commit
messages.
- If you are using an LLM, and do not fully understand the changes it is
making to the code base, do not create a PR.
- In our experience, AI generated code often results in overly complex
code that lacks enough context for a proper fix or feature inclusion.
This results in considerably longer code reviews. Due to this, AI
authored or partially authored PRs may be closed without comment.
- [x] I understand these changes in full and will be able to respond to
review comments.
- [x] My change is accurately described in the commit message.
- [x] My contribution is tested and working as described.
- [x] I have read the [Developer Certificate of
Origin](https://developercertificate.org/) and certify my contribution
under its conditions.

Co-authored-by: Levi Portenier <levi@system76.com>
2026-04-22 14:33:37 -06:00
Tony Wasserka
c04e49e218 fix(wallpapers): follow symlinks when scanning wallpaper directories
On NixOS, /run/current-system/sw/share/backgrounds lists all wallpapers
installed on the system. The folder entries are symbolic links to the actual
PNG files (each in a dedicated nix store path). Previously WalkDir would skip
over these, resulting in cosmic-settings displaying an empty wallpaper list.

By following symbolic links, the list is now properly populated.
2026-04-22 22:10:24 +02:00
Vukašin Vojinović
26f0fb0609 chore(default_apps): reduce code duplication
Also fixes the flex items wrapping too early by setting the FlexRow width to Shrink.
2026-04-22 22:10:06 +02:00
Vukašin Vojinović
317de19eb3 fix(shortcuts): modified count for accessibility and tiling
These weren't being set previously, so they never showed up.
Also updates remaining things here to use ListButton.
2026-04-22 22:10:06 +02:00
280 changed files with 15224 additions and 9804 deletions

View file

@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: install system dependencies
run: sudo apt-get update && sudo apt-get install lld cmake libclang-dev libexpat1-dev libfontconfig-dev libfreetype-dev libpipewire-0.3-dev libpulse-dev pkg-config libxkbcommon-dev libudev-dev libinput-dev libwayland-dev
run: sudo apt-get update && sudo apt-get install lld cmake libclang-dev libdav1d-dev libexpat1-dev libfontconfig-dev libfreetype-dev libpipewire-0.3-dev libpulse-dev pkg-config libxkbcommon-dev libudev-dev libinput-dev libwayland-dev
- uses: actions/checkout@v6
- name: install toolchain
run: rustup toolchain install 1.90.0 --component clippy
@ -38,7 +38,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: install system dependencies
run: sudo apt-get update && sudo apt-get install lld cmake libclang-dev libexpat1-dev libfontconfig-dev libfreetype-dev libpipewire-0.3-dev libpulse-dev pkg-config libxkbcommon-dev libudev-dev libinput-dev libwayland-dev
run: sudo apt-get update && sudo apt-get install lld cmake libclang-dev libdav1d-dev libexpat1-dev libfontconfig-dev libfreetype-dev libpipewire-0.3-dev libpulse-dev pkg-config libxkbcommon-dev libudev-dev libinput-dev libwayland-dev
- uses: actions/checkout@v6
- name: install toolchain
run: rustup show
@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: install system dependencies
run: sudo apt-get update && sudo apt-get install lld cmake libclang-dev libexpat1-dev libfontconfig-dev libfreetype-dev libpipewire-0.3-dev libpulse-dev pkg-config libxkbcommon-dev libudev-dev libinput-dev libwayland-dev just
run: sudo apt-get update && sudo apt-get install lld cmake libclang-dev libdav1d-dev libexpat1-dev libfontconfig-dev libfreetype-dev libpipewire-0.3-dev libpulse-dev pkg-config libxkbcommon-dev libudev-dev libinput-dev libwayland-dev just
- uses: actions/checkout@v6
- name: install toolchain
run: rustup show

15
.zed/settings.json Normal file
View file

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

2949
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,25 @@
[workspace]
members = ["cosmic-settings", "crates/*", "page", "pages/*", "subscriptions/*"]
members = ["cosmic-settings", "page", "pages/*", "subscriptions/*"]
default-members = ["cosmic-settings"]
resolver = "3"
[workspace.package]
rust-version = "1.90"
rust-version = "1.93"
[workspace.dependencies]
cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" }
tokio = { version = "1.49.0", features = ["macros"] }
iced_winit = { git = "https://github.com/pop-os/libcosmic", default-features = false }
cosmic-randr = { path = "../cosmic-randr/lib" }
tokio = { version = "1.53.1", features = ["macros"] }
iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit", default-features = false }
image = { version = "0.25.10", default-features = false, features = [
"jpeg",
"png",
"rayon",
"webp",
"hdr",
] }
[workspace.dependencies.libcosmic]
[workspace.dependencies.cosmic]
package = "libcosmic-yoda"
features = [
"dbus-config",
"desktop",
@ -20,29 +28,29 @@ features = [
"tokio",
"qr_code",
]
git = "https://github.com/pop-os/libcosmic"
path = "/home/lionel/Projets/COSMIC/libcosmic"
[workspace.dependencies.cosmic-config]
git = "https://github.com/pop-os/libcosmic"
path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config"
[workspace.dependencies.cosmic-bg-config]
git = "https://github.com/pop-os/cosmic-bg"
path = "../cosmic-bg/config"
[workspace.dependencies.cosmic-comp-config]
git = "https://github.com/pop-os/cosmic-comp"
path = "../cosmic-comp/cosmic-comp-config"
features = ["output"]
[workspace.dependencies.cosmic-idle-config]
git = "https://github.com/pop-os/cosmic-idle"
path = "../cosmic-idle/cosmic-idle-config"
[workspace.dependencies.cosmic-panel-config]
git = "https://github.com/pop-os/cosmic-panel"
path = "../cosmic-panel/cosmic-panel-config"
[workspace.dependencies.cosmic-randr-shell]
git = "https://github.com/pop-os/cosmic-randr"
path = "../cosmic-randr/shell"
[workspace.dependencies.sctk]
version = "0.20"
version = "0.20.0"
package = "smithay-client-toolkit"
[profile.dev]
@ -59,34 +67,18 @@ inherits = "release"
lto = "off"
debug = true
# [patch.'https://github.com/pop-os/cosmic-text']
# cosmic-text = { git = "https://github.com/pop-os/cosmic-text//", rev = "b017d7c" }
[patch.'https://github.com/pop-os/cosmic-protocols']
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "d0e95be" }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "d0e95be" }
cosmic-protocols = { path = "../cosmic-protocols" }
cosmic-client-toolkit = { path = "../cosmic-protocols/client-toolkit" }
# [patch.'https://github.com/pop-os/cosmic-settings-daemon']
# cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon//", branch = "input_nobuild" }
# For development and testing purposes
[patch.'https://github.com/pop-os/libcosmic']
libcosmic = { path = "../libcosmic" }
cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-theme = { path = "../libcosmic/cosmic-theme" }
iced_futures = { path = "../libcosmic/iced/futures" }
iced_winit = { path = "../libcosmic/iced/winit" }
cosmic-config = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" }
cosmic-theme = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-theme" }
iced_futures = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/futures" }
iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit" }
# libcosmic = { git = "https://github.com/pop-os/libcosmic//" }
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//" }
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//" }
# iced_futures = { git = "https://github.com/pop-os/libcosmic//" }
# iced_winit = { git = "https://github.com/pop-os/libcosmic//" }
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }
# upower_dbus = { path = "../dbus-settings-bindings/upower" }
# nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings//", branch = "nm-secret-agent" }
[patch.'https://github.com/pop-os/cosmic-text.git']
cosmic-text = { path = "../cosmic-text" }
[patch.crates-io]
atspi = { git = "https://github.com/wash2/atspi" }

View file

@ -48,7 +48,13 @@ This document provides a regression testing checklist for COSMIC Settings and th
### Sound
TBD after devices/profiles are fixed.
- [ ] Output & Input volume sliders work independently
- [ ] Output balance slider works & doesn't get reset by volume slider
- [ ] Amplification toggles work as expected
- [ ] Device profiles can be changed & reflect as expected on the main Sound page
- [ ] Applications using sound appear on the Applications sub-page
- [ ] Application volume and mute controls only affect the selected playback stream
- [ ] Closing an playback stream removes it from the Applications sub-page
### Power & Battery

View file

@ -1,20 +1,25 @@
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
# Compile with debug profile
[no-cd]
build-debug *args:
cargo build {{ args }}
cargo build {{args}}
# Compile with release profile
[no-cd]
build-release *args: (build-debug '--release' args)
# Compile with a vendored tarball
[no-cd]
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
# Check for errors and linter warnings
[no-cd]
check *args:
cargo clippy --all-features {{ args }} -- -W clippy::pedantic
cargo clippy --all-features {{args}} -- -W clippy::pedantic
# Runs a check with JSON message format for IDE integration
[no-cd]
check-json: (check '--message-format=json')
# Remove Cargo build artifacts
@ -28,25 +33,38 @@ clean-dist: clean
rm -rf .cargo vendor vendor.tar target
# Run the application for testing purposes
[no-cd]
run *args:
env RUST_LOG=debug RUST_BACKTRACE=full cargo run {{ args }} --release
cargo run {{args}}
# Run `cargo test`
[no-cd]
test *args:
cargo test {{ args }}
cargo test {{args}}
# Vendor Cargo dependencies locally
[no-cd]
vendor:
#!/usr/bin/env bash
mkdir -p .cargo
cargo vendor --locked | head -n -1 > .cargo/config.toml
echo 'directory = "vendor"' >> .cargo/config.toml
tar pcf vendor.tar vendor
rm -rf vendor
echo >> .cargo/config.toml
echo '[env]' >> .cargo/config.toml
if [ -n "${SOURCE_DATE_EPOCH}" ]
then
source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")"
echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml
fi
if [ -n "${SOURCE_GIT_HASH}" ]
then
echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml
fi
tar pcf vendor.tar .cargo vendor
rm -rf .cargo vendor
# Extracts vendored dependencies
[no-cd]
[private]
vendor-extract:
rm -rf vendor
tar pxf vendor.tar

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-settings"
version = "1.0.7"
version = "1.7.0"
edition = "2024"
license = "GPL-3.0-only"
publish = false
@ -16,13 +16,11 @@ ashpd = { version = "0.12", default-features = false, features = [
], optional = true }
chrono = "0.4"
jiff = "0.2"
clap = { version = "4.5.59", features = ["derive"] }
clap = { version = "4.6.4", features = ["derive"] }
color-eyre = "0.6.5"
cosmic-bg-config.workspace = true
cosmic-comp-config = { workspace = true, optional = true }
cosmic-config.workspace = true
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
cosmic-idle-config.workspace = true
cosmic-panel-config = { workspace = true, optional = true }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true }
@ -34,43 +32,37 @@ cosmic-settings-accessibility-subscription = { path = "../subscriptions/accessib
cosmic-settings-a11y-manager-subscription = { path = "../subscriptions/a11y-manager", optional = true }
cosmic-settings-airplane-mode-subscription = { path = "../subscriptions/airplane-mode", optional = true }
cosmic-settings-bluetooth-subscription = { path = "../subscriptions/bluetooth", optional = true }
cosmic-settings-network-manager-subscription = { path = "../subscriptions/network-manager", optional = true }
cosmic-settings-sound = { path = "../pages/sound", optional = true }
cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true }
cosmic-settings-sound-subscription = { path = "../subscriptions/sound", optional = true, features = [
"auto-profile-init",
] }
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
cosmic-settings-daemon-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
cups_rs = { version = "0.3.0", optional = true }
derive_setters = "0.1.9"
dirs = "6.0.0"
eyre = "0.6.12"
freedesktop-desktop-entry = "0.8.1"
futures = "0.3.32"
futures = "0.3.33"
hostname-validator = "1.1.1"
hostname1-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
i18n-embed-fl = "0.10.0"
image = { version = "0.25", default-features = false, features = [
"jpeg",
"png",
"rayon",
"webp",
"hdr",
] }
indexmap = "2.13.0"
i18n-embed-fl = "0.10.1"
image.workspace = true
indexmap = "2.14.0"
itertools = "0.14.0"
itoa = "1.0.17"
libcosmic.workspace = true
itoa = "1.0.18"
cosmic.workspace = true
iced_winit.workspace = true
locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
sysinfo = { version = "=0.38.0", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", optional = true }
mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true }
notify = "8.2.0"
regex = "1.12.3"
nmrs = { version = "3.4.2", optional = true }
regex = "1.13.1"
ron = "0.12"
rust-embed = "8.11.0"
rust-embed = "8.12.0"
sctk = { workspace = true, optional = true }
secure-string = "0.3.0"
serde = { version = "1.0.228", features = ["derive"] }
secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"], optional = true }
serde = { version = "1.0.229", features = ["derive"] }
slab = "0.4.12"
slotmap = "1.1.1"
static_init = "1.0.4"
@ -78,29 +70,36 @@ sunrise = "3.0.0"
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
tokio = { workspace = true, features = ["fs", "io-util", "process", "sync"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
udev = { version = "0.9.3", optional = true }
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
bluez-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
url = "2.5.8"
xkb-data = "0.2.2"
xkeysym = { version = "0.2.1", optional = true }
zbus = { version = "5.13.2", default-features = false, features = [
zbus = { version = "5.18.0", default-features = false, features = [
"tokio",
], optional = true }
zbus_polkit = { version = "5.0.0", optional = true }
fontdb = "0.23.0"
mime = "0.3.17"
rustix = { version = "1.1.3", features = ["process"] }
rustix = { version = "1.1.4", features = ["process"] }
gettext-rs = { version = "0.7.7", features = [
"gettext-system",
], optional = true }
num-traits = "0.2"
pwhash = "1"
which = "8.0.0"
which = "8.0.5"
zlink = "0.5.0"
intmap = "3.1.3"
[dependencies.cosmic-settings-audio-client]
git = "https://github.com/pop-os/cosmic-settings-daemon"
features = ["codec"]
optional = true
[dependencies.icu]
version = "2.1.1"
version = "2.2.0"
features = ["compiled_data"]
[dependencies.i18n-embed]
@ -113,8 +112,11 @@ git = "https://github.com/AerynOS/locales-rs"
optional = true
[features]
default = ["a11y", "linux", "single-instance", "wgpu"]
default = ["a11y", "avif", "linux", "single-instance", "wgpu", "systemd"]
avif = ["image/avif-native"]
gettext = ["dep:gettext-rs"]
systemd = []
openrc = []
# Default features for Linux
linux = [
@ -129,6 +131,7 @@ linux = [
"page-legacy-applications",
"page-networking",
"page-power",
"page-printers",
"page-region",
"page-sound",
"page-users",
@ -167,13 +170,13 @@ page-input = [
]
page-legacy-applications = ["cosmic-comp-config", "dep:cosmic-randr"]
page-networking = [
"dep:cosmic-settings-network-manager-subscription",
"xdg-portal",
"dep:cosmic-dbus-networkmanager",
"dep:nm-secret-agent-manager",
"dep:nmrs",
"dep:secret-service",
"dep:zbus",
]
page-power = ["dep:upower_dbus", "dep:zbus"]
page-printers = ["dep:cups_rs"]
page-region = [
"gettext",
"dep:locales-rs",
@ -181,18 +184,18 @@ page-region = [
"dep:zbus",
"dep:accounts-zbus",
]
page-sound = ["dep:cosmic-settings-sound-subscription"]
page-sound = ["dep:cosmic-settings-audio-client", "dep:cosmic-settings-sound"]
page-users = ["xdg-portal", "dep:accounts-zbus", "dep:zbus", "dep:zbus_polkit"]
page-window-management = ["cosmic-comp-config", "dep:cosmic-settings-config"]
page-workspaces = ["cosmic-comp-config"]
# Other features
a11y = ["libcosmic/a11y"]
a11y = ["cosmic/a11y"]
ashpd = ["dep:ashpd"]
cosmic-comp-config = ["dep:cosmic-comp-config"]
dbus-config = ["libcosmic/dbus-config", "cosmic-config/dbus"]
single-instance = ["libcosmic/single-instance"]
dbus-config = ["cosmic/dbus-config", "cosmic-config/dbus"]
single-instance = ["cosmic/single-instance"]
test = []
wayland = ["libcosmic/wayland", "dep:cosmic-panel-config", "dep:cosmic-randr", "iced_winit/cctk"]
wgpu = ["libcosmic/wgpu"]
xdg-portal = ["ashpd", "libcosmic/xdg-portal"]
wayland = ["cosmic/wayland", "dep:cosmic-panel-config", "dep:cosmic-randr", "iced_winit/wayland"]
wgpu = ["cosmic/wgpu"]
xdg-portal = ["ashpd", "cosmic/xdg-portal"]

View file

@ -1,4 +1,5 @@
use std::{env, fs, path::PathBuf};
use std::path::PathBuf;
use std::{env, fs};
use xdgen::{App, Context, FluentString};
fn main() {

View file

@ -5,7 +5,6 @@ use crate::PageCommands;
use crate::config::Config;
#[cfg(feature = "page-accessibility")]
use crate::pages::accessibility;
use crate::pages::applications;
#[cfg(feature = "page-bluetooth")]
use crate::pages::bluetooth;
use crate::pages::desktop::{self, appearance};
@ -17,28 +16,25 @@ use crate::pages::input;
use crate::pages::networking;
#[cfg(feature = "page-power")]
use crate::pages::power;
#[cfg(feature = "page-printers")]
use crate::pages::printers;
#[cfg(feature = "page-sound")]
use crate::pages::sound;
use crate::pages::{self, system, time};
use crate::pages::{self, applications, system, time};
use crate::subscription::desktop_files;
use crate::widget::{page_title, search_header};
use cosmic::app::context_drawer::ContextDrawer;
use cosmic::app::{Core, Task};
#[cfg(feature = "wayland")]
use cosmic::cctk::{sctk::output::OutputInfo, wayland_client::protocol::wl_output::WlOutput};
use cosmic::{
Element,
app::{Core, Task, context_drawer::ContextDrawer},
iced::{
self, Length, Subscription,
event::{self, PlatformSpecific},
keyboard, window,
},
prelude::*,
surface,
widget::{
button, column, container, icon, id_container, nav_bar, row, scrollable, segmented_button,
settings, text_input,
},
use cosmic::iced::event::{self, PlatformSpecific};
use cosmic::iced::{self, Length, Subscription, keyboard, window};
use cosmic::prelude::*;
use cosmic::widget::{
button, column, container, icon, id_container, nav_bar, row, scrollable, segmented_button,
settings, text_input,
};
use cosmic::{Element, surface};
#[cfg(feature = "cosmic-comp-config")]
use cosmic_comp_config::CosmicCompConfig;
#[cfg(feature = "wayland")]
@ -52,8 +48,9 @@ use desktop::{
#[cfg(feature = "wayland")]
use event::wayland;
use page::Entity;
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::{borrow::Cow, str::FromStr};
use std::str::FromStr;
#[allow(clippy::struct_excessive_bools)]
#[allow(clippy::module_name_repetitions)]
@ -121,6 +118,8 @@ impl SettingsApp {
}
#[cfg(feature = "page-power")]
PageCommands::Power => self.pages.page_id::<power::Page>(),
#[cfg(feature = "page-printers")]
PageCommands::Printers => self.pages.page_id::<printers::Page>(),
#[cfg(feature = "page-region")]
PageCommands::RegionLanguage => self.pages.page_id::<time::region::Page>(),
#[cfg(feature = "page-sound")]
@ -230,6 +229,8 @@ impl cosmic::Application for SettingsApp {
app.insert_page::<power::Page>();
#[cfg(feature = "page-input")]
app.insert_page::<input::Page>();
#[cfg(feature = "page-printers")]
app.insert_page::<printers::Page>();
app.insert_page::<applications::Page>();
app.insert_page::<time::Page>();
app.insert_page::<system::Page>();
@ -583,6 +584,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-sound")]
crate::pages::Message::SoundApplications(message) => {
if let Some(page) = self.pages.page_mut::<sound::applications::Page>() {
return page.update(message).map(Into::into);
}
}
#[cfg(feature = "page-sound")]
crate::pages::Message::SoundDeviceProfiles(message) => {
if let Some(page) = self.pages.page_mut::<sound::device_profiles::Page>() {
@ -659,6 +667,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-printers")]
crate::pages::Message::Printers(message) => {
if let Some(page) = self.pages.page_mut::<printers::Page>() {
return page.update(message).map(Into::into);
}
}
#[cfg(feature = "page-networking")]
crate::pages::Message::Vpn(message) => {
if let Some(page) = self.pages.page_mut::<networking::vpn::Page>() {
@ -1150,6 +1165,9 @@ impl SettingsApp {
}
for page in unload {
if page == self.active_page {
continue;
}
self.loaded_pages.remove(&page);
self.pages.on_leave(page);
}

View file

@ -1,10 +1,8 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
cosmic_config::{self, ConfigGet, ConfigSet},
cosmic_theme::palette::Srgba,
};
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
use cosmic::cosmic_theme::palette::Srgba;
const NAME: &str = "com.system76.CosmicSettings";

View file

@ -1,10 +1,8 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use i18n_embed::{
DefaultLocalizer, LanguageLoader, Localizer,
fluent::{FluentLanguageLoader, fluent_language_loader},
};
use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};
use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};
use rust_embed::RustEmbed;
use std::sync::LazyLock;

View file

@ -7,6 +7,12 @@
#![allow(clippy::cast_lossless)]
#![allow(clippy::too_many_lines)]
#[cfg(not(any(feature = "systemd", feature = "openrc")))]
compile_error!(
"At least one service manager feature must be enabled. \
Enable 'systemd' or 'openrc' for bluetooth service management."
);
pub mod app;
use std::str::FromStr;
@ -16,6 +22,7 @@ pub mod config;
#[macro_use]
pub mod localize;
pub mod pages;
pub mod service_manager;
pub mod subscription;
pub mod theme;
pub mod utils;
@ -24,7 +31,8 @@ pub mod widget;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use cosmic::{app::CosmicFlags, iced::Limits};
use cosmic::app::CosmicFlags;
use cosmic::iced::Limits;
use i18n_embed::DesktopLanguageRequester;
use ron::error::SpannedError;
use serde::{Deserialize, Serialize};
@ -114,6 +122,9 @@ pub enum PageCommands {
/// Power settings page
#[cfg(feature = "page-power")]
Power,
/// Printers settings page
#[cfg(feature = "page-printers")]
Printers,
/// Region & Language settings page
#[cfg(feature = "page-region")]
RegionLanguage,

View file

@ -1,20 +1,15 @@
use std::collections::HashSet;
use std::fmt::Write;
use cosmic::{
Apply,
iced::core::text::Wrapping,
iced::{Element, Length, stream},
surface,
widget::{self, icon, settings, svg, text},
};
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::{Element, Length, stream};
use cosmic::widget::{self, icon, settings, svg, text};
use cosmic::{Apply, surface};
use cosmic_comp_config::{ZoomConfig, ZoomMovement};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::{Action, Binding, shortcuts};
use cosmic_settings_page::{
self as page, Entity,
section::{self, Section},
};
use cosmic_settings_page::section::{self, Section};
use cosmic_settings_page::{self as page, Entity};
use futures::SinkExt;
use slotmap::SlotMap;
use tracing::error;

View file

@ -1,21 +1,17 @@
use cosmic::{
Apply, Element, Task,
cosmic_theme::{CosmicPalette, ThemeBuilder},
iced::core::text::Wrapping,
iced::stream,
surface,
theme::CosmicTheme,
widget::{dropdown, settings, text},
};
use cosmic::cosmic_theme::{CosmicPalette, ThemeBuilder};
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::stream;
use cosmic::theme::CosmicTheme;
use cosmic::widget::{dropdown, settings, text};
use cosmic::{Apply, Element, Task, surface};
use cosmic_comp_config::CosmicCompConfig;
pub use cosmic_comp_config::ZoomMovement;
use cosmic_config::CosmicConfigEntry;
use cosmic_settings_a11y_manager_subscription as cosmic_a11y_manager;
use cosmic_settings_accessibility_subscription as a11y_bus;
use cosmic_settings_daemon_config::CosmicSettingsDaemonConfig;
use cosmic_settings_page::{
self as page, Insert,
section::{self, Section},
};
use cosmic_settings_page::section::{self, Section};
use cosmic_settings_page::{self as page, Insert};
use futures::SinkExt;
use num_traits::FromPrimitive;
use slotmap::SlotMap;
@ -41,11 +37,21 @@ pub struct Page {
daemon_helper: cosmic_config::Config,
dbus_sender: Option<UnboundedSender<a11y_bus::Request>>,
reader_enabled: bool,
comp_config: CosmicCompConfig,
comp_helper: cosmic_config::Config,
}
impl Default for Page {
fn default() -> Self {
let daemon_helper = CosmicSettingsDaemonConfig::config().unwrap();
let comp_helper = cosmic_config::Config::new("com.system76.CosmicComp", 1).unwrap();
let comp_config =
CosmicCompConfig::get_entry(&comp_helper).unwrap_or_else(|(errs, config)| {
for err in errs {
tracing::warn!(?err, "error while reading cosmic-comp config");
}
config
});
Page {
dbus_sender: None,
entity: page::Entity::default(),
@ -70,6 +76,8 @@ impl Default for Page {
.unwrap_or_default(),
daemon_helper,
reader_enabled: false,
comp_config,
comp_helper,
}
}
}
@ -82,6 +90,7 @@ pub enum Message {
ProtocolUnavailable,
Return,
ScreenReaderEnabled(bool),
SetCursorShakeToFind(bool),
SetScreenFilterActive(bool),
SetScreenFilterSelection(ColorFilter),
SetScreenInverted(bool),
@ -192,6 +201,8 @@ pub fn vision() -> section::Section<crate::pages::Message> {
invert_colors = fl!("accessibility", "invert-colors");
color_filters = fl!("accessibility", "color-filters");
color_filter_type = fl!("color-filter");
shake_cursor = fl!("accessibility", "shake-cursor");
shake_cursor_desc = fl!("accessibility", "shake-cursor-desc");
});
Section::default()
@ -281,6 +292,13 @@ pub fn vision() -> section::Section<crate::pages::Message> {
settings::item::builder(&descriptions[color_filter_type]).control(dropdown)
})
.add(
settings::item::builder(&descriptions[shake_cursor])
.description(&descriptions[shake_cursor_desc])
.toggler(page.comp_config.cursor_shake_to_find, |enabled| {
Message::SetCursorShakeToFind(enabled).into()
}),
)
.into()
})
}
@ -421,6 +439,14 @@ impl Page {
Message::Surface(a) => {
return cosmic::task::message(crate::app::Message::Surface(a));
}
Message::SetCursorShakeToFind(enabled) => {
if let Err(err) = self
.comp_config
.set_cursor_shake_to_find(&self.comp_helper, enabled)
{
tracing::error!(?err, "failed to set cursor_shake_to_find");
}
}
Message::SetSoundMono(active) => {
if let Err(err) = self
.daemon_config

View file

@ -2,28 +2,23 @@
// Copyright 2024 bbb651 <bar.ye651@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
borrow::Cow,
collections::{BTreeMap, BTreeSet},
path::{Path, PathBuf},
sync::Arc,
};
use cosmic::{
Apply, Element, Task,
iced::Alignment,
surface,
widget::{self, dropdown, icon, settings},
};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, dropdown, icon, settings};
use cosmic::{Apply, Element, Task, surface};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::shortcuts::SystemActions;
use cosmic_settings_config::shortcuts::action::System;
use cosmic_settings_page::{self as page, Section, section};
use freedesktop_desktop_entry::{
DesktopEntry, Iter as DesktopEntryIter, default_paths, get_languages_from_env,
};
use mime_apps::App;
use freedesktop_desktop_entry::{DesktopEntry, default_paths, get_languages_from_env};
use mime::Mime;
use slotmap::SlotMap;
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::io::SeekFrom;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};
const DROPDOWN_WEB_BROWSER: usize = 0;
const DROPDOWN_FILE_MANAGER: usize = 1;
@ -52,7 +47,7 @@ pub enum Category {
#[derive(Clone, Debug)]
pub enum Message {
SetDefault(Category, usize),
Update(CachedMimeApps),
Update(Arc<CachedMimeApps>),
Surface(surface::Action),
}
@ -68,28 +63,27 @@ impl From<Message> for crate::pages::Message {
}
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct CachedMimeApps {
pub list: mime_apps::List,
pub local_list: mime_apps::List,
pub apps: Vec<AppMeta>,
pub known_mimes: BTreeSet<mime::Mime>,
pub config_path: Box<Path>,
local_list: Option<(mime_apps::List, tokio::fs::File)>,
apps: Vec<AppMeta>,
known_mimes: BTreeSet<mime::Mime>,
}
#[derive(Clone, Debug)]
pub struct AppMeta {
selected: Option<usize>,
app_ids: Vec<String>,
app_ids: Vec<Box<str>>,
apps: Vec<String>,
icons: Vec<icon::Handle>,
}
#[derive(Clone, Debug, Default)]
#[derive(Debug, Default)]
pub struct Page {
on_enter_handle: Option<cosmic::iced::task::Handle>,
mime_apps: Option<CachedMimeApps>,
shortcuts_config: Option<cosmic_config::Config>,
update_config: Option<tokio::task::JoinHandle<()>>,
}
impl page::AutoBind<crate::pages::Message> for Page {}
@ -113,46 +107,45 @@ impl page::Page<crate::pages::Message> for Page {
handle.abort();
}
let config_update_handle = self.update_config.take();
if self.shortcuts_config.is_none() {
self.shortcuts_config = cosmic_settings_config::shortcuts::context().ok();
}
let (task, on_enter_handle) = Task::future(async move {
let mut list = mime_apps::List::default();
list.load_from_paths(&mime_apps::list_paths());
let mut local_list = mime_apps::List::default();
if let Some(path) = mime_apps::local_list_path()
&& let Ok(buffer) = std::fs::read_to_string(&path)
{
local_list.load_from(&buffer);
// Wait for previous copy operation to complete.
if let Some(handle) = config_update_handle {
_ = handle.await;
}
let assocs = mime_apps::associations::by_app();
let local_list = mime_apps::load_user_mimeapps().await.ok();
let mut list = mime_apps::List::default();
list.load_from_paths(&mime_apps::list_paths());
let assocs = mime_apps::associations::load_from_disk(&list);
let apps = vec![
load_defaults(&assocs, &["x-scheme-handler/http"]).await,
load_defaults(&assocs, &["inode/directory"]).await,
load_defaults(&assocs, &["x-scheme-handler/mailto"]).await,
load_defaults(&assocs, &["audio/mp3", "application/ogg", "video/mp4"]).await,
load_defaults(&assocs, &["video/mp4"]).await,
load_defaults(&assocs, &["image/png"]).await,
load_defaults(&assocs, &["text/calendar"]).await,
load_terminal_apps(&assocs).await,
load_defaults(&assocs, &["text/plain"]).await,
load_defaults(&list, &assocs, &["x-scheme-handler/http"]),
load_defaults(&list, &assocs, &["inode/directory"]),
load_defaults(&list, &assocs, &["x-scheme-handler/mailto"]),
load_defaults(
&list,
&assocs,
&["audio/mpeg", "application/ogg", "audio/x-flac", "video/mp4"],
),
load_defaults(&list, &assocs, &["video/mp4"]),
load_defaults(&list, &assocs, &["image/png"]),
load_defaults(&list, &assocs, &["text/calendar"]),
load_terminal_apps(&list, &assocs).await,
load_defaults(&list, &assocs, &["text/plain"]),
];
Message::Update(CachedMimeApps {
Message::Update(Arc::new(CachedMimeApps {
apps,
list,
local_list,
known_mimes: mime_apps::mime_info::mime_types(),
config_path: dirs::config_dir()
.expect("config dir not found")
.join("mimeapps.list")
.into(),
})
}))
.into()
})
.abortable();
@ -193,6 +186,7 @@ impl Page {
"application/ogg",
"application/x-cue",
"application/x-ogg",
"audio/mpeg",
"audio/mp3",
"x-content/audio-cdda",
])
@ -237,13 +231,16 @@ impl Page {
"x-scheme-handler/https",
],
),
Category::TextEditor => (DROPDOWN_TEXT_EDITOR, &["text/plain"]),
Category::TextEditor => (
DROPDOWN_TEXT_EDITOR,
&["application/x-zerosize", "text/plain"],
),
Category::Mime(_mime_type) => return Task::none(),
};
let meta = &mut mime_apps.apps[category_id];
if meta.selected != Some(id) {
if meta.selected.is_none_or(|selected| selected != id) {
meta.selected = Some(id);
let appid = &meta.app_ids[id];
@ -254,24 +251,40 @@ impl Page {
assign_default_terminal(config, appid);
}
for mime in mime_types {
if let Ok(mime) = mime.parse() {
mime_apps
.local_list
.set_default_app(mime, [appid, ".desktop"].concat());
};
if let Some((local_list, local_file)) = mime_apps.local_list.as_mut() {
for mime in mime_types {
if let Ok(mime) = mime.parse() {
tracing::info!(target: "default-apps", ?mime, appid, "setting default for mime");
local_list.set_default_app(mime, [appid, ".desktop"].concat());
};
}
if let Some(config_update_handle) = self.update_config.take() {
config_update_handle.abort();
}
let mut buffer = local_list.to_string();
buffer.push('\n');
if let Ok(mut local_file) =
futures::executor::block_on(local_file.try_clone())
{
self.update_config = Some(tokio::spawn(async move {
tracing::debug!(target: "default-apps", buffer, "writing to mimeapps config");
_ = local_file.seek(SeekFrom::Start(0)).await;
_ = local_file.set_len(buffer.len() as u64).await;
_ = local_file.write_all(buffer.as_bytes()).await;
_ = local_file.flush().await;
_ = local_file.seek(SeekFrom::Start(0)).await;
_ = tokio::process::Command::new("update-desktop-database")
.status()
.await;
}));
}
}
let mut buffer = mime_apps.local_list.to_string();
buffer.push('\n');
_ = std::fs::write(&mime_apps.config_path, buffer);
_ = std::process::Command::new("update-desktop-database").status();
}
}
Message::Update(mime_apps) => {
self.mime_apps = Some(mime_apps);
}
Message::Update(mime_apps) => self.mime_apps = Arc::into_inner(mime_apps),
Message::Surface(a) => {
return cosmic::task::message(crate::app::Message::Surface(a));
}
@ -281,269 +294,83 @@ impl Page {
}
}
fn app_item(meta: &AppMeta, label: String, category: Category) -> widget::FlexRow<'_, Message> {
// Avoid creating popup_dropdown for empty app lists.
// Empty categories are rendered as non-interactive text to prevent Wayland crashes.
settings::flex_item(
label,
if meta.apps.is_empty() {
widget::text(fl!("default-apps", "not-installed")).apply(Element::from)
} else {
dropdown::popup_dropdown(
&meta.apps,
Some(meta.selected.unwrap_or(0)),
move |id| Message::SetDefault(category, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| crate::app::Message::PageMessage(crate::pages::Message::DefaultApps(a)),
)
.icons(Cow::Borrowed(&meta.icons))
.apply(Element::from)
},
)
.align_items(Alignment::Center)
.width(Length::Shrink)
}
fn apps() -> Section<crate::pages::Message> {
Section::default().view::<Page>(move |_binder, page, section| {
let Some(mime_apps) = page.mime_apps.as_ref() else {
return widget::space().into();
};
// Avoid creating popup_dropdown for empty app lists.
// Empty categories are rendered as non-interactive text to prevent Wayland crashes.
settings::section()
settings::section::with_capacity(9)
.title(&section.title)
.add({
let meta = &mime_apps.apps[DROPDOWN_WEB_BROWSER];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "web-browser"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "web-browser"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::WebBrowser, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
.min_item_width(300.0)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_FILE_MANAGER];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "file-manager"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "file-manager"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::FileManager, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_MAIL];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "mail-client"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "mail-client"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::Mail, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_MUSIC];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "music"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "music"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::Audio, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_VIDEO];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "video"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "video"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::Video, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_PHOTO];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "photos"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "photos"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::Image, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_CALENDAR];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "calendar"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "calendar"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::Calendar, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_TERMINAL];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "terminal"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "terminal"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::Terminal, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add({
let meta = &mime_apps.apps[DROPDOWN_TEXT_EDITOR];
if meta.apps.is_empty() {
settings::flex_item(
fl!("default-apps", "text-editor"),
widget::text(fl!("default-apps", "not-installed")),
)
.align_items(Alignment::Center)
} else {
settings::flex_item(
fl!("default-apps", "text-editor"),
dropdown::popup_dropdown(
&meta.apps,
meta.selected,
|id| Message::SetDefault(Category::TextEditor, id),
cosmic::iced::window::Id::RESERVED,
Message::Surface,
|a| {
crate::app::Message::PageMessage(
crate::pages::Message::DefaultApps(a),
)
},
)
.icons(Cow::Borrowed(&meta.icons)),
)
.align_items(Alignment::Center)
}
})
.add(app_item(
&mime_apps.apps[DROPDOWN_WEB_BROWSER],
fl!("default-apps", "web-browser"),
Category::WebBrowser,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_FILE_MANAGER],
fl!("default-apps", "file-manager"),
Category::FileManager,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_MAIL],
fl!("default-apps", "mail-client"),
Category::Mail,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_MUSIC],
fl!("default-apps", "music"),
Category::Audio,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_VIDEO],
fl!("default-apps", "video"),
Category::Video,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_PHOTO],
fl!("default-apps", "photos"),
Category::Image,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_CALENDAR],
fl!("default-apps", "calendar"),
Category::Calendar,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_TERMINAL],
fl!("default-apps", "terminal"),
Category::Terminal,
))
.add(app_item(
&mime_apps.apps[DROPDOWN_TEXT_EDITOR],
fl!("default-apps", "text-editor"),
Category::TextEditor,
))
.apply(Element::from)
.map(crate::pages::Message::DefaultApps)
})
@ -584,57 +411,71 @@ fn assign_default_terminal(config: &cosmic_config::Config, appid: &str) {
}
}
async fn load_defaults(assocs: &BTreeMap<Arc<str>, Arc<App>>, for_mimes: &[&str]) -> AppMeta {
fn load_defaults(
list: &mime_apps::List,
assocs: &mime_apps::Associations,
for_mimes: &[&str],
) -> AppMeta {
let mut unsorted = Vec::new();
let mut current_app = None;
let mut default_app = None;
for for_mime in for_mimes {
let Ok(mime) = for_mime.parse() else {
return AppMeta {
selected: None,
app_ids: Vec::new(),
apps: Vec::new(),
icons: Vec::new(),
};
let Ok(mime) = Mime::from_str(for_mime) else {
continue;
};
let current_app_entry = xdg_mime_query_default(for_mime).await;
let current_appid = current_app_entry
.as_ref()
.and_then(|entry| entry.strip_suffix(".desktop"));
for default in list.default_app_for(&mime).into_iter().flatten() {
let appid = default.strip_suffix(".desktop").unwrap_or(default.as_ref());
if unsorted.is_empty() {
current_app = current_appid.and_then(|appid| assocs.get(appid));
tracing::info!(target: "default-apps", ?mime, appid, "entry for mime");
if default_app.is_none() {
default_app = assocs.by_app_id.get(appid);
if default_app.is_some() {
break;
}
}
}
unsorted.extend(
mime_apps::apps_for_mime(&mime, assocs)
.map(|(app_id, app)| (app_id.clone(), app.clone())),
);
for (appid, app) in mime_apps::apps_for_mime(&mime, assocs) {
if app.icon.is_none() {
continue;
}
if unsorted.iter().any(|(id, _)| id == appid) {
continue;
}
unsorted.push((appid.clone(), app.clone()));
}
}
unsorted.sort_unstable_by_key(|(_, app)| app.name.clone());
unsorted.dedup_by_key(|(app_id, _)| app_id.clone());
let mut selected = None;
let mut app_ids = Vec::new();
let mut apps = Vec::new();
let mut icons = Vec::new();
for (id, (appid, app)) in unsorted.iter().enumerate() {
if let Some(current_app) = current_app
&& app.name.as_ref() == current_app.name.as_ref()
let mut id = 0;
for (appid, app) in unsorted.iter() {
let Some(icon) = app.icon.as_deref() else {
continue;
};
if let Some(default_app) = default_app
&& app.name.as_ref() == default_app.name.as_ref()
{
selected = Some(id);
}
app_ids.push(appid.as_ref().into());
apps.push(app.name.as_ref().into());
icons.push(if app.icon.starts_with('/') {
icon::from_path(PathBuf::from(app.icon.as_ref()))
icons.push(if icon.starts_with('/') {
icon::from_path(PathBuf::from(icon))
} else {
icon::from_name(app.icon.as_ref()).size(20).handle()
icon::from_name(icon).size(20).handle()
});
id += 1;
}
AppMeta {
@ -645,86 +486,55 @@ async fn load_defaults(assocs: &BTreeMap<Arc<str>, Arc<App>>, for_mimes: &[&str]
}
}
async fn xdg_mime_query_default(mime_type: &str) -> Option<String> {
let output = tokio::process::Command::new("xdg-mime")
.args(["query", "default", mime_type])
.output()
.await
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()
.map(|string| string.trim().to_owned())
}
async fn load_terminal_apps(assocs: &BTreeMap<Arc<str>, Arc<App>>) -> AppMeta {
let mut terminals = Vec::new();
let mut current_app = None;
// Get the current default terminal if set
let current_entry = xdg_mime_query_default("x-scheme-handler/terminal").await;
let current_appid = current_entry
.as_ref()
.and_then(|entry| entry.strip_suffix(".desktop"));
// Also check x-terminal-emulator
if current_app.is_none() {
let current_entry = xdg_mime_query_default("application/x-terminal-emulator").await;
let current_appid = current_entry
.as_ref()
.and_then(|entry| entry.strip_suffix(".desktop"));
if let Some(appid) = current_appid {
current_app = assocs.get(appid).cloned();
}
}
// Scan desktop entries for terminal applications
for path in DesktopEntryIter::new(default_paths()) {
if let Ok(bytes) = std::fs::read_to_string(&path)
&& let Ok(entry) = DesktopEntry::from_str(&path, &bytes, None::<&[&str]>)
{
// Check if it's a terminal application
if entry
.categories()
.map(|cats| cats.contains(&"TerminalEmulator"))
.unwrap_or(false)
{
let id = entry.id();
if let Some(app) = assocs.get(id) {
if current_appid.as_ref().map(|c| *c == id).unwrap_or(false) {
current_app = Some(app.clone());
}
terminals.push((Arc::from(id), app.clone()));
}
}
}
}
terminals.sort_unstable_by_key(|(_, app): &(Arc<str>, Arc<App>)| app.name.clone());
terminals.dedup_by(|a, b| a.0 == b.0);
async fn load_terminal_apps(list: &mime_apps::List, assocs: &mime_apps::Associations) -> AppMeta {
let mut selected = None;
let mut app_ids = Vec::new();
let mut apps = Vec::new();
let mut icons = Vec::new();
for (id, (appid, app)) in terminals.iter().enumerate() {
if let Some(ref current_app) = current_app
&& app.name.as_ref() == current_app.name.as_ref()
{
let mut terminals: Vec<_> = assocs
.apps
.iter()
.filter(|app| app.is_terminal && app.icon.is_some())
.collect();
terminals.sort_unstable_by_key(|app| app.name.clone());
'outer: for for_mime in [
"x-scheme-handler/terminal",
"application/x-terminal-emulator",
] {
let Ok(mime) = Mime::from_str(for_mime) else {
continue;
};
for default in list.default_app_for(&mime).into_iter().flatten() {
let appid = default.strip_suffix(".desktop").unwrap_or(default.as_ref());
for (id, app) in terminals.iter().enumerate() {
if app.appid.as_ref() == appid {
selected = Some(id);
break 'outer;
}
}
}
}
for (id, app) in terminals.into_iter().enumerate() {
let Some(icon) = app.icon.as_deref() else {
continue;
};
if selected.is_none() && app.appid.as_ref() == "com.system76.CosmicTerm" {
selected = Some(id);
}
app_ids.push(appid.to_string());
app_ids.push(app.appid.clone());
apps.push(app.name.as_ref().into());
icons.push(if app.icon.starts_with('/') {
icon::from_path(PathBuf::from(app.icon.as_ref()))
icons.push(if icon.starts_with('/') {
icon::from_path(PathBuf::from(icon))
} else {
icon::from_name(app.icon.as_ref()).size(20).handle()
icon::from_name(icon).size(20).handle()
});
}

View file

@ -1,25 +1,17 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
process::ExitStatus,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use std::process::ExitStatus;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use cosmic::{
Apply, Element, Task,
cosmic_config::{self, ConfigGet, ConfigSet},
iced::stream,
surface,
widget::{self, dropdown, settings, text},
};
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
use cosmic::iced::stream;
use cosmic::widget::{self, dropdown, settings, text};
use cosmic::{Apply, Element, Task, surface};
use cosmic_comp_config::{EavesdroppingKeyboardMode, XwaylandDescaling, XwaylandEavesdropping};
use cosmic_randr_shell::List;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use futures::SinkExt;
use slotmap::SlotMap;
use tokio::sync::oneshot;

View file

@ -1,7 +1,8 @@
use cosmic::app::ContextDrawer;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::text_input::focus;
use cosmic::widget::{button, icon, settings, text};
use cosmic::{Apply, Element, Task, widget};
use cosmic::{Apply, Element, Task, task, widget};
use cosmic_settings_page::section::Entity;
use cosmic_settings_page::{self as page, Content, Info, Section};
use freedesktop_desktop_entry::DesktopEntry;
@ -9,8 +10,12 @@ use itertools::Itertools;
use slotmap::{Key, SlotMap};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::LazyLock;
use tracing::error;
pub static ADD_APPLICATION_SEARCH: LazyLock<widget::Id> =
LazyLock::new(|| widget::Id::new("ADD_APPLICATION_SEARCH"));
#[derive(Clone, Debug)]
pub struct CachedApps {
apps: HashMap<DirectoryType, Vec<DesktopEntry>>,
@ -51,6 +56,7 @@ pub enum Message {
ShowApplicationSidebar(DirectoryType),
UpdateApplications(CachedApps),
UpdateStartupApplications(CachedApps),
FocusAddApplicationSearch,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
@ -116,6 +122,7 @@ impl page::Page<crate::pages::Message> for Page {
let search = widget::search_input("", &self.application_search)
.on_input(|i| Message::ApplicationSearch(i).into())
.on_clear(Message::ApplicationSearch(String::new()).into())
.id(ADD_APPLICATION_SEARCH.clone())
.apply(Element::from);
Some(
@ -142,8 +149,11 @@ impl page::Page<crate::pages::Message> for Page {
let user_entries =
freedesktop_desktop_entry::Iter::new(user_dirs.into_iter()).entries(Some(&locales));
let mut user_entries_vec = user_entries.collect_vec();
sort_entries_by_name(&mut user_entries_vec, &locales);
let mut apps_hash = HashMap::with_capacity(1);
apps_hash.insert(DirectoryType::User, user_entries.collect_vec());
apps_hash.insert(DirectoryType::User, user_entries_vec);
Message::UpdateStartupApplications(CachedApps {
apps: apps_hash,
@ -217,7 +227,8 @@ impl Page {
}
Message::ShowApplicationSidebar(directory_type) => {
self.context = Some(Context::AddApplication(directory_type));
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity))
.chain(task::message(Message::FocusAddApplicationSearch));
}
Message::AddStartupApplication(directory_type, app) => {
let mut file_name = app.clone().appid;
@ -246,6 +257,10 @@ impl Page {
if let Some(target_apps) = target_apps {
let mut new_apps = target_apps.clone();
new_apps.push(app.clone());
sort_entries_by_name(
&mut new_apps,
&cached_startup_apps.locales,
);
cached_startup_apps
.apps
@ -312,6 +327,26 @@ impl Page {
self.target_directory_type = None;
self.context = None;
}
Message::FocusAddApplicationSearch => {
// retry until the widget is in the tree and focused or the dialog is removed.
if matches!(self.context, Some(Context::AddApplication(_))) {
return cosmic::iced::runtime::task::widget(
cosmic::iced::core::widget::operation::focusable::find_focused(),
)
.collect()
.then(|id| {
if id
.first()
.is_some_and(|id| *id == ADD_APPLICATION_SEARCH.clone())
{
Task::none()
} else {
focus(ADD_APPLICATION_SEARCH.clone())
.chain(task::message(Message::FocusAddApplicationSearch))
}
});
}
}
_ => {}
}
@ -433,6 +468,14 @@ fn apps() -> Section<crate::pages::Message> {
})
}
fn sort_entries_by_name(entries: &mut [DesktopEntry], locales: &[String]) {
entries.sort_by_cached_key(|e| {
e.name(locales)
.map(|n| n.to_lowercase())
.unwrap_or_else(|| e.appid.to_lowercase())
});
}
fn get_all_apps(locales: Vec<String>) -> Vec<DesktopEntry> {
let mut dedupe = HashSet::new();
@ -473,5 +516,7 @@ fn get_all_apps(locales: Vec<String>) -> Vec<DesktopEntry> {
dedupe.insert(app_id.to_owned());
}
sort_entries_by_name(&mut result, &locales);
result
}

View file

@ -3,7 +3,8 @@
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::{Alignment, Length, color};
use cosmic::widget::{self, settings, space::horizontal as horizontal_space, text};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::{self, settings, text};
use cosmic::{Apply, Element, Task, theme};
use cosmic_settings_bluetooth_subscription::*;
use cosmic_settings_page::{self as page, Section, section};
@ -11,28 +12,31 @@ use futures::channel::oneshot;
use futures::{SinkExt, StreamExt};
use slotmap::SlotMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use zbus::zvariant::OwnedObjectPath;
#[cfg(test)]
use crate::service_manager::MockServiceManager;
use crate::service_manager::ServiceManagerHandle;
static BLUETOOTH_PAGE_LABEL: LazyLock<String> = LazyLock::new(|| fl!("bluetooth"));
enum Dialog {
// RequestAuthorization {
// device: OwnedObjectPath,
// response: oneshot::Sender<bool>,
// },
RequestConfirmation {
device: String,
passkey: u32,
response: oneshot::Sender<bool>,
},
// RequestPasskey {
// device: OwnedObjectPath,
// response: oneshot::Sender<Option<u32>>,
// },
// RequestPinCode {
// device: OwnedObjectPath,
// response: oneshot::Sender<Option<String>>,
// },
DisplayPasskey {
device: String,
passkey: u32,
entered: u16,
},
DisplayPinCode {
device: String,
pincode: String,
},
}
#[derive(Default)]
@ -160,6 +164,8 @@ pub struct Page {
service_is_active: bool,
subscription: Option<tokio::sync::oneshot::Sender<()>>,
service_manager: ServiceManagerHandle,
}
impl page::Page<crate::pages::Message> for Page {
@ -169,6 +175,19 @@ impl page::Page<crate::pages::Message> for Page {
.description(fl!("xdg-entry-bluetooth-comment"))
}
fn header(&self) -> Option<Element<'_, crate::pages::Message>> {
if self.model.adapters.len() > 1 {
let (_, adapter) = self.model.get_selected_adapter()?;
return Some(crate::widget::sub_page_header(
&adapter.alias,
BLUETOOTH_PAGE_LABEL.as_str(),
Message::SelectAdapter(None).into(),
));
}
None
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
@ -255,6 +274,85 @@ impl page::Page<crate::pages::Message> for Page {
Some(dialog)
}
Dialog::DisplayPasskey {
device,
passkey,
entered,
} => {
let description = widget::text::body(fl!(
"bluetooth-display-passkey",
"description",
device = device
))
.wrapping(Wrapping::Word);
let passkey_str = format!("{passkey:06}");
let entered = *entered as usize;
let pin = widget::text::title1(passkey_str)
.width(Length::Fill)
.align_x(Alignment::Center)
.wrapping(Wrapping::None);
let mut control = widget::column::with_capacity(3).push(description).push(pin);
// Only show the key progress counter when the device actually
// reports keypress notifications. Most keyboards don't support
// this, so entered stays at 0 for the entire pairing process.
if entered > 0 {
let progress = widget::text::body(format!("{entered} / 6 keys entered"))
.width(Length::Fill)
.align_x(Alignment::Center)
.wrapping(Wrapping::None);
control = control.push(progress);
}
let control = control.spacing(theme::spacing().space_xxs);
let cancel_button =
widget::button::standard(fl!("cancel")).on_press(Message::PinCancel);
let dialog = widget::dialog()
.title(fl!("bluetooth-display-passkey"))
.control(control)
.secondary_action(cancel_button)
.apply(Element::from)
.map(Into::into);
Some(dialog)
}
Dialog::DisplayPinCode { device, pincode } => {
let description = widget::text::body(fl!(
"bluetooth-display-pin",
"description",
device = device
))
.wrapping(Wrapping::Word);
let pin = widget::text::title1(pincode.clone())
.width(Length::Fill)
.align_x(Alignment::Center)
.wrapping(Wrapping::None);
let control = widget::column::with_capacity(2)
.push(description)
.push(pin)
.spacing(theme::spacing().space_xxs);
let cancel_button =
widget::button::standard(fl!("cancel")).on_press(Message::PinCancel);
let dialog = widget::dialog()
.title(fl!("bluetooth-display-pin"))
.control(control)
.secondary_action(cancel_button)
.apply(Element::from)
.map(Into::into);
Some(dialog)
}
}
}
}
@ -309,6 +407,30 @@ impl From<Event> for Message {
}
impl Page {
fn update_heading(&mut self) {
self.heading = if let Some((_, adapter)) = self.model.get_selected_adapter() {
fl!(
"bluetooth",
"status",
aliases = format!("“{}”", adapter.alias)
)
} else {
fl!(
"bluetooth",
"status",
aliases = self
.model
.adapters
.values()
.map(|adapter| format!("“{}”", adapter.alias))
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>()
.join(", ")
)
};
}
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::Message> {
let span = tracing::span!(tracing::Level::INFO, "bluetooth::update");
let _span = span.enter();
@ -354,28 +476,7 @@ impl Page {
Event::SetAdapters(adapters) => {
let select_adapter = self.model.set_adapters(adapters);
if let Some((_, adapter)) = self.model.get_selected_adapter() {
self.heading = fl!(
"bluetooth",
"status",
aliases = format!("“{}”", adapter.alias)
);
} else {
self.heading = fl!(
"bluetooth",
"status",
aliases = self
.model
.adapters
.values()
.map(|adapter| format!("“{}”", adapter.alias))
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>()
.join(", ")
);
}
self.update_heading();
if let Some(adapter) = select_adapter {
return cosmic::task::message(Message::SelectAdapter(Some(adapter)));
@ -401,6 +502,19 @@ impl Page {
}
Event::UpdatedDevice(path, update) => {
// Dismiss passkey/pin display dialogs when pairing completes,
// since BlueZ may not always send Cancel after a successful pair.
if update
.iter()
.any(|u| matches!(u, DeviceUpdate::Paired(true)))
&& matches!(
self.dialog,
Some(Dialog::DisplayPasskey { .. } | Dialog::DisplayPinCode { .. })
)
{
self.dialog = None;
}
if let Some(existing) = self.model.devices.get_mut(&path) {
tracing::debug!("Device {} updated", existing.address);
existing.update(update);
@ -434,7 +548,18 @@ impl Page {
}
Event::DBusServiceUnknown => {
self.bluez_service_unknown = true;
// D-Bus activation for org.bluez is absent. On systemd this usually
// means bluez isn't installed; on OpenRC it may be installed but lack
// the D-Bus service file. Check the service manager to disambiguate.
if self.service_manager.is_installed() {
// Service manager confirms the service exists — query its real state.
self.bluez_service_unknown = false;
self.service_is_active = self.service_manager.is_active();
self.service_is_enabled = self.service_manager.is_enabled();
} else {
// Genuinely not installed — let status() show the unknown message.
self.bluez_service_unknown = true;
}
}
Event::Agent(message) => {
@ -464,6 +589,32 @@ impl Page {
});
}
bluez_zbus::agent1::Message::DisplayPasskey {
device,
passkey,
entered,
} => {
let device = self.model.devices.get(&device).map_or_else(
|| device.to_string(),
|device| device.alias_or_addr().to_owned(),
);
self.dialog = Some(Dialog::DisplayPasskey {
device,
passkey,
entered,
});
}
bluez_zbus::agent1::Message::DisplayPinCode { device, pincode } => {
let device = self.model.devices.get(&device).map_or_else(
|| device.to_string(),
|device| device.alias_or_addr().to_owned(),
);
self.dialog = Some(Dialog::DisplayPinCode { device, pincode });
}
bluez_zbus::agent1::Message::RequestPasskey { response, .. } => {
_ = response.send(None);
}
@ -552,8 +703,8 @@ impl Page {
}
Message::DBusConnect(connection) => {
self.service_is_active = systemd::is_bluetooth_active();
self.service_is_enabled = systemd::is_bluetooth_enabled();
self.service_is_active = self.service_manager.is_active();
self.service_is_enabled = self.service_manager.is_enabled();
self.connection = Some(connection.clone());
let get_adapters_fut = get_adapters(connection.clone());
@ -608,6 +759,7 @@ impl Page {
Message::SelectAdapter(adapter_maybe) => {
tracing::debug!("Adapter selected: {adapter_maybe:?}");
self.model.selected_adapter = adapter_maybe;
self.update_heading();
self.model.update_status();
let Some(connection) = self.connection.as_ref() else {
tracing::error!("No DBus connection ready");
@ -685,8 +837,9 @@ impl Page {
}
Message::ServiceActivate => {
return cosmic::task::future(async {
systemd::activate_bluetooth().await;
let activate_future = self.service_manager.activate();
return cosmic::task::future(async move {
activate_future.await;
tokio::time::sleep(Duration::from_secs(3)).await;
match zbus::Connection::system().await {
@ -697,8 +850,9 @@ impl Page {
}
Message::ServiceEnable => {
return cosmic::task::future(async {
systemd::enable_bluetooth().await;
let enable_future = self.service_manager.enable();
return cosmic::task::future(async move {
enable_future.await;
tokio::time::sleep(Duration::from_secs(3)).await;
match zbus::Connection::system().await {
@ -757,7 +911,7 @@ fn status() -> Section<crate::pages::Message> {
return bluetooth_service_issue(
fl!("bluetooth", "inactive"),
fl!("activate"),
Message::ServiceEnable,
Message::ServiceActivate,
);
}
@ -1010,36 +1164,110 @@ fn multiple_adapter() -> Section<crate::pages::Message> {
impl page::AutoBind<crate::pages::Message> for Page {}
mod systemd {
use futures::FutureExt;
pub fn activate_bluetooth() -> impl Future<Output = ()> + Send {
tokio::process::Command::new("pkexec")
.args(["systemctl", "start", "bluetooth"])
.status()
.map(|_| ())
}
pub fn enable_bluetooth() -> impl Future<Output = ()> + Send {
tokio::process::Command::new("pkexec")
.args(["systemctl", "enable", "--now", "bluetooth"])
.status()
.map(|_| ())
}
pub fn is_bluetooth_enabled() -> bool {
std::process::Command::new("systemctl")
.args(["is-enabled", "bluetooth"])
.status()
.map(|status| status.success())
.unwrap_or(true)
}
pub fn is_bluetooth_active() -> bool {
std::process::Command::new("systemctl")
.args(["is-active", "bluetooth"])
.status()
.map(|status| status.success())
.unwrap_or(true)
impl Page {
#[cfg(test)]
fn with_service_manager(service_manager: ServiceManagerHandle) -> Self {
Self {
service_manager,
..Page::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cosmic_settings_page::Page as _;
fn adapter(path: &str, alias: &str) -> (OwnedObjectPath, Adapter) {
(
OwnedObjectPath::try_from(path).unwrap(),
Adapter {
alias: alias.to_owned(),
..Adapter::default()
},
)
}
fn page_with_two_adapters() -> Page {
let mut page = Page::default();
let adapters = [
adapter("/org/bluez/hci0", "Adapter 0"),
adapter("/org/bluez/hci1", "Adapter 1"),
]
.into_iter()
.collect();
let _task = page.update(Message::BluetoothEvent(Event::SetAdapters(adapters)));
page
}
#[test]
fn bluetooth_header_is_absent_at_adapter_chooser() {
let page = page_with_two_adapters();
assert!(page.header().is_none());
}
#[test]
fn bluetooth_header_tracks_adapter_selection_and_clearing() {
let mut page = page_with_two_adapters();
let selected = OwnedObjectPath::try_from("/org/bluez/hci1").unwrap();
let _task = page.update(Message::SelectAdapter(Some(selected)));
assert!(page.header().is_some());
assert!(page.heading.contains("Adapter 1"));
let _task = page.update(Message::SelectAdapter(None));
assert!(page.header().is_none());
assert!(page.heading.contains("Adapter 0"));
assert!(page.heading.contains("Adapter 1"));
}
#[test]
fn test_dbus_service_unknown_with_installed_service_queries_manager() {
let bluetooth = MockServiceManager::new(true, false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown));
assert!(page.service_is_enabled);
assert!(!page.service_is_active);
assert!(!page.bluez_service_unknown);
}
#[test]
fn test_dbus_service_unknown_with_uninstalled_service_sets_bluez_unknown() {
let bluetooth = MockServiceManager::new(true, true).with_installed(false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown));
assert!(page.bluez_service_unknown);
assert!(!page.service_is_enabled);
assert!(!page.service_is_active);
}
#[test]
fn test_dbus_service_unknown_clears_previously_set_bluez_service_unknown() {
// Simulate a stale state and verify the handler re-checks via is_installed().
let bluetooth = MockServiceManager::new(true, true);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
page.bluez_service_unknown = true;
let _task = page.update(Message::BluetoothEvent(Event::DBusServiceUnknown));
assert!(!page.bluez_service_unknown);
}
#[tokio::test]
async fn test_service_activate_calls_through_to_service_manager() {
let bluetooth = MockServiceManager::new(false, false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::ServiceActivate);
}
#[tokio::test]
async fn test_service_enable_calls_through_to_service_manager() {
let bluetooth = MockServiceManager::new(false, false);
let mut page = Page::with_service_manager(ServiceManagerHandle::new(Box::new(bluetooth)));
let _task = page.update(Message::ServiceEnable);
}
}

View file

@ -2,12 +2,11 @@ use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::config::CosmicTk;
use cosmic::cosmic_config::{Config, ConfigSet};
use cosmic::cosmic_theme::Spacing;
use cosmic::iced::Alignment;
use cosmic::iced::core::{Color, Length};
use cosmic::widget::{
ColorPickerModel, color_picker::ColorPickerUpdate, container, flex_row, settings, text,
};
use cosmic::{Apply, Task};
use cosmic::{Element, widget};
use cosmic::widget::color_picker::ColorPickerUpdate;
use cosmic::widget::{ColorPickerModel, container, flex_row, row, settings, slider, text};
use cosmic::{Apply, Element, Task, task, widget};
use cosmic_config::ConfigGet;
use std::sync::Arc;
use tracing::error;
@ -15,11 +14,8 @@ use tracing::error;
use crate::app;
use crate::widget::color_picker_context_view;
use super::{
ContextView, Message, font_config, icon_themes,
icon_themes::{IconHandles, IconThemes},
theme_manager,
};
use super::icon_themes::{IconHandles, IconThemes};
use super::{ContextView, Message, font_config, icon_themes, theme_manager};
pub struct Content {
context_view: Option<ContextView>,
@ -40,6 +36,12 @@ pub struct Content {
icon_themes: IconThemes,
icon_handles: IconHandles,
tk_config: Option<Config>,
frosted: u8,
glass: f32,
frosted_system_interface: bool,
frosted_windows: bool,
frosted_panel: bool,
frosted_applets: bool,
comp_config: cosmic_config::Config,
#[cfg(feature = "cosmic-comp-config")]
@ -94,7 +96,7 @@ impl From<&theme_manager::Manager> for Content {
application_background: ColorPickerModel::new(
&*HEX,
&*RGB,
Some(theme.background.base.into()),
Some(theme.background(false).base.into()),
theme_manager.get_color(&ContextView::ApplicationBackground),
),
container_background: ColorPickerModel::new(
@ -106,7 +108,7 @@ impl From<&theme_manager::Manager> for Content {
interface_text: ColorPickerModel::new(
&*HEX,
&*RGB,
Some(theme.background.on.into()),
Some(theme.background(false).on.into()),
theme_manager.get_color(&ContextView::InterfaceText),
),
control_component: ColorPickerModel::new(
@ -122,6 +124,12 @@ impl From<&theme_manager::Manager> for Content {
theme_manager.get_color(&ContextView::AccentWindowHint),
),
font_config: font_config::Model::new(),
frosted: theme.frosted as u8,
glass: theme.alpha_map.guess_offset(),
frosted_system_interface: theme.frosted_system_interface,
frosted_windows: theme.frosted_windows,
frosted_panel: theme.frosted_panel,
frosted_applets: theme.frosted_applets,
icons_fetched: false,
icon_global: cosmic::config::apply_theme_global(),
icon_fetch_handle: None,
@ -145,6 +153,16 @@ impl Content {
}
}
pub fn preserve_from(&mut self, previous: &mut Content) {
self.font_config
.take_families_from(&mut previous.font_config);
self.icons_fetched = previous.icons_fetched;
self.icon_theme_active = previous.icon_theme_active;
self.icon_themes = std::mem::take(&mut previous.icon_themes);
self.icon_handles = std::mem::take(&mut previous.icon_handles);
}
pub fn update_font(
&mut self,
message: FontMessage,
@ -285,6 +303,40 @@ impl Content {
Task::none()
}
pub fn update_frosted_system_interface(&mut self, v: bool) -> Task<app::Message> {
self.frosted_system_interface = v;
Task::none()
}
pub fn update_frosted_windows(&mut self, v: bool) -> Task<app::Message> {
self.frosted_windows = v;
Task::none()
}
pub fn update_frosted_panel(&mut self, v: bool) -> Task<app::Message> {
self.frosted_panel = v;
Task::none()
}
pub fn update_frosted_applets(&mut self, v: bool) -> Task<app::Message> {
self.frosted_applets = v;
Task::none()
}
pub fn update_blur(&mut self, v: u8) -> Task<app::Message> {
self.frosted = v;
Task::none()
}
pub fn update_glass(&mut self, v: f32) -> Task<app::Message> {
self.glass = v;
Task::none()
}
pub fn on_open(&mut self, context_view: &ContextView) -> Task<app::Message> {
match *context_view {
ContextView::IconsAndToolkit => {
@ -300,6 +352,7 @@ impl Content {
}
ContextView::MonospaceFont | ContextView::SystemFont => {
self.font_config.reset();
return task::message(super::Message::FocusFontInput);
}
_ => {}
}
@ -331,7 +384,7 @@ impl Content {
self.application_background = ColorPickerModel::new(
&*HEX,
&*RGB,
Some(manager.theme().background.base.into()),
Some(manager.theme().background(false).base.into()),
manager.get_color(&ContextView::ApplicationBackground),
);
self.custom_accent = ColorPickerModel::new(
@ -349,7 +402,7 @@ impl Content {
self.interface_text = ColorPickerModel::new(
&*HEX,
&*RGB,
Some(manager.theme().background.on.into()),
Some(manager.theme().background(false).on.into()),
manager.get_color(&ContextView::InterfaceText),
);
self.control_component = ColorPickerModel::new(
@ -475,6 +528,10 @@ impl Content {
self.shadow_and_corners(),
crate::pages::Message::CloseContextDrawer,
),
ContextView::FrostedGlass => context_drawer(
self.frosted_glass(),
crate::pages::Message::CloseContextDrawer,
),
})
}
@ -558,4 +615,66 @@ impl Content {
.apply(Element::from)
.map(crate::pages::Message::Appearance)
}
pub fn frosted_glass(&self) -> Element<'_, crate::pages::Message> {
settings::section()
.add(
settings::item::builder(fl!("style", "frosted-panels"))
.description(fl!("style", "frosted-panels-desc"))
.toggler(self.frosted_panel, Message::FrostedPanel),
)
.add(
settings::item::builder(fl!("style", "frosted-applets"))
.description(fl!("style", "frosted-applets-desc"))
.toggler(self.frosted_applets, Message::FrostedApplets),
)
.add(
settings::item::builder(fl!("style", "frosted-system-interface"))
.description(fl!("style", "frosted-system-interface-desc"))
.toggler(
self.frosted_system_interface,
Message::FrostedSystemInterface,
),
)
.add(
settings::item::builder(fl!("style", "frosted-windows"))
.description(fl!("style", "frosted-windows-desc"))
.toggler(self.frosted_windows, Message::FrostedWindows),
)
.add(
settings::item::builder(fl!("style", "frosted-thickness")).flex_control({
row::with_children(vec![
text::body(fl!("style", "less")).into(),
slider(0..=13, self.frosted, Message::Blur)
.width(Length::Fill)
.apply(cosmic::widget::container)
.max_width(250)
.into(),
text::body(fl!("style", "more")).into(),
])
.align_y(Alignment::Center)
.spacing(8)
.width(Length::Fill)
}),
)
.add(
settings::item::builder(fl!("style", "glass-opacity")).flex_control({
row::with_children(vec![
text::body(fl!("style", "less")).into(),
slider(0.0..=1.0, self.glass, Message::Glass)
.step(0.05)
.width(Length::Fill)
.apply(cosmic::widget::container)
.max_width(250)
.into(),
text::body(fl!("style", "more")).into(),
])
.align_y(Alignment::Center)
.spacing(8)
.width(Length::Fill)
}),
)
.apply(Element::from)
.map(crate::pages::Message::Appearance)
}
}

View file

@ -1,13 +1,10 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use cosmic::{
Apply, Element, Task,
config::{CosmicTk, FontConfig},
widget,
};
use cosmic::config::{CosmicTk, FontConfig};
use cosmic::{Apply, Element, Task, widget};
use cosmic_config::ConfigSet;
use crate::app;
@ -15,6 +12,8 @@ use crate::widget::selection_context_item;
use super::{ContextView, Message, drawer};
pub static FONT_SEARCH: LazyLock<widget::Id> = LazyLock::new(|| widget::Id::new("FONT_SEARCH"));
const INTERFACE_FONT: &str = "interface_font";
const MONOSPACE_FONT: &str = "monospace_font";
@ -94,6 +93,11 @@ impl Model {
self.font_filter.clear();
}
pub fn take_families_from(&mut self, other: &mut Model) {
self.interface_font_families = std::mem::take(&mut other.interface_font_families);
self.monospace_font_families = std::mem::take(&mut other.monospace_font_families);
}
pub fn font_loaded(
&mut self,
mono: Vec<Arc<str>>,
@ -167,6 +171,7 @@ impl Model {
.on_clear(Message::DrawerFont(drawer::FontMessage::Search(
String::new(),
)))
.id(FONT_SEARCH.clone())
.apply(Element::from)
.map(crate::pages::Message::Appearance)
}

View file

@ -1,14 +1,13 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{collections::BTreeMap, path::PathBuf};
use std::collections::BTreeMap;
use std::path::PathBuf;
use super::Message;
use cosmic::{
Element,
iced::{Background, Length},
widget::{button, icon, text},
};
use cosmic::Element;
use cosmic::iced::{Background, Length};
use cosmic::widget::{button, icon, text};
use tokio::io::AsyncBufReadExt;
const ICON_PREV_N: usize = 6;

View file

@ -16,23 +16,24 @@ use cosmic::app::ContextDrawer;
use cosmic::config::CosmicTk;
use cosmic::cosmic_config::{Config, ConfigSet, CosmicConfigEntry};
use cosmic::cosmic_theme::palette::{FromColor, Hsv, Srgb};
use cosmic::cosmic_theme::{CornerRadii, Density, ThemeBuilder, ThemeMode};
use cosmic::cosmic_theme::{CornerRadii, Density, Roundness, ThemeBuilder, ThemeMode};
#[cfg(feature = "xdg-portal")]
use cosmic::dialog::file_chooser::{self, FileFilter};
use cosmic::iced::Subscription;
use cosmic::iced::core::{Alignment, Length};
use cosmic::widget::{
button, color_picker::ColorPickerUpdate, container, row, settings, space::horizontal, text,
};
use cosmic::{Apply, Element, Task, widget};
use cosmic::widget::color_picker::ColorPickerUpdate;
use cosmic::widget::space::horizontal;
use cosmic::widget::text_input::focus;
use cosmic::widget::{button, container, row, settings, text};
use cosmic::{Apply, Element, Task, task, widget};
#[cfg(feature = "wayland")]
use cosmic_panel_config::CosmicPanelConfig;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use ron::ser::PrettyConfig;
use slotmap::{Key, SlotMap};
use crate::app;
use crate::pages::desktop::appearance::font_config::FONT_SEARCH;
#[derive(Clone, Copy, Debug)]
pub enum ContextView {
@ -40,6 +41,7 @@ pub enum ContextView {
ApplicationBackground,
ContainerBackground,
ControlComponent,
FrostedGlass,
#[cfg(feature = "cosmic-comp-config")]
ShadowAndCorners,
CustomAccent,
@ -139,6 +141,7 @@ pub enum Message {
#[cfg(feature = "xdg-portal")]
ExportSuccess,
FocusFontInput,
GapSize(u32),
#[cfg(feature = "xdg-portal")]
ImportError,
@ -157,6 +160,12 @@ pub enum Message {
UseDefaultWindowHint(bool),
WindowHintSize(u32),
Daytime(bool),
Blur(u8),
Glass(f32),
FrostedPanel(bool),
FrostedApplets(bool),
FrostedSystemInterface(bool),
FrostedWindows(bool),
}
impl From<Message> for crate::app::Message {
@ -171,56 +180,6 @@ impl From<Message> for crate::pages::Message {
}
}
#[derive(Debug, Clone, Copy)]
pub enum Roundness {
Round,
SlightlyRound,
Square,
}
impl From<Roundness> for CornerRadii {
fn from(value: Roundness) -> Self {
match value {
Roundness::Round => CornerRadii {
radius_0: [0.0; 4],
radius_xs: [4.0; 4],
radius_s: [8.0; 4],
radius_m: [16.0; 4],
radius_l: [32.0; 4],
radius_xl: [160.0; 4],
},
Roundness::SlightlyRound => CornerRadii {
radius_0: [0.0; 4],
radius_xs: [2.0; 4],
radius_s: [8.0; 4],
radius_m: [8.0; 4],
radius_l: [8.0; 4],
radius_xl: [8.0; 4],
},
Roundness::Square => CornerRadii {
radius_0: [0.0; 4],
radius_xs: [2.0; 4],
radius_s: [2.0; 4],
radius_m: [2.0; 4],
radius_l: [2.0; 4],
radius_xl: [2.0; 4],
},
}
}
}
impl From<CornerRadii> for Roundness {
fn from(value: CornerRadii) -> Self {
if (value.radius_m[0] - 16.0).abs() < 0.01 {
Self::Round
} else if (value.radius_m[0] - 8.0).abs() < 0.01 {
Self::SlightlyRound
} else {
Self::Square
}
}
}
impl Page {
#[allow(clippy::too_many_lines)]
pub fn update(&mut self, message: Message) -> Task<app::Message> {
@ -241,6 +200,9 @@ impl Page {
Message::DrawerOpen(context_view) => {
self.context_view = Some(context_view);
let mut previous =
std::mem::replace(&mut self.drawer, drawer::Content::from(&self.theme_manager));
self.drawer.preserve_from(&mut previous);
tasks.push(cosmic::task::message(
crate::app::Message::OpenContextDrawer(self.entity),
));
@ -289,8 +251,8 @@ impl Page {
#[cfg(feature = "wayland")]
tokio::task::spawn(async move {
Self::update_panel_radii(r);
Self::update_dock_padding(r);
Self::update_panel_radii(r);
});
}
@ -556,6 +518,86 @@ impl Page {
self.drawer.reset(&self.theme_manager);
}
}
Message::FocusFontInput => {
// retry until the widget is in the tree and focused or the dialog is removed.
if matches!(
self.context_view,
Some(ContextView::SystemFont | ContextView::MonospaceFont)
) {
return cosmic::iced::runtime::task::widget(
cosmic::iced::core::widget::operation::focusable::find_focused(),
)
.collect()
.then(|id| {
if id.first().is_some_and(|id| *id == FONT_SEARCH.clone()) {
Task::none()
} else {
focus(FONT_SEARCH.clone()).chain(task::message(Message::FocusFontInput))
}
});
}
}
Message::Blur(b) => {
let Ok(blur_strength) = b.try_into() else {
tracing::error!("Invalid blur strength: {b}");
return Task::none();
};
theme_staged = self.theme_manager.set_frosted(blur_strength).map(|_| {
self.drawer.reset(&self.theme_manager);
theme_manager::ThemeStaged::Current
});
tasks.push(self.drawer.update_blur(blur_strength as u8));
}
Message::Glass(glass) => {
let mut alpha_map = cosmic::cosmic_theme::AlphaMap::default();
let glass_offset_min = alpha_map.extremely_high_2;
let glass_offset_max = 1.0 - alpha_map.extremely_low;
let offset = (glass - 0.5) / 0.5
* if glass < 0.5 {
glass_offset_min
} else {
glass_offset_max
};
alpha_map = alpha_map.offset(offset);
// TODO optimize and apply to both light and dark themes
theme_staged = self.theme_manager.set_glass(alpha_map).map(|_| {
self.drawer.reset(&self.theme_manager);
theme_manager::ThemeStaged::Current
});
tasks.push(self.drawer.update_glass(glass));
}
Message::FrostedSystemInterface(v) => {
theme_staged = self.theme_manager.set_frosted_system_interface(v).map(|_| {
self.drawer.reset(&self.theme_manager);
theme_manager::ThemeStaged::Current
});
tasks.push(self.drawer.update_frosted_system_interface(v));
}
Message::FrostedWindows(v) => {
theme_staged = self.theme_manager.set_frosted_windows(v).map(|_| {
self.drawer.reset(&self.theme_manager);
theme_manager::ThemeStaged::Current
});
tasks.push(self.drawer.update_frosted_windows(v));
}
Message::FrostedPanel(v) => {
theme_staged = self.theme_manager.set_frosted_panel(v).map(|_| {
self.drawer.reset(&self.theme_manager);
theme_manager::ThemeStaged::Current
});
tasks.push(self.drawer.update_frosted_panel(v));
}
Message::FrostedApplets(v) => {
theme_staged = self.theme_manager.set_frosted_applets(v).map(|_| {
self.drawer.reset(&self.theme_manager);
theme_manager::ThemeStaged::Current
});
tasks.push(self.drawer.update_frosted_applets(v));
}
}
let mut tasks = cosmic::Task::batch(tasks);
@ -570,113 +612,66 @@ impl Page {
}
// TODO: cache panel and dock configs so that they needn't be re-read
#[cfg(feature = "wayland")]
fn load_panel_config(name: &str) -> Option<(Config, CosmicPanelConfig)> {
let helper = CosmicPanelConfig::cosmic_config(name).ok()?;
let config = CosmicPanelConfig::get_entry(&helper).ok()?;
(config.name == name).then_some((helper, config))
}
#[cfg(feature = "wayland")]
pub fn update_panel_radii(roundness: Roundness) {
let panel_config_helper = CosmicPanelConfig::cosmic_config("Panel").ok();
let dock_config_helper = CosmicPanelConfig::cosmic_config("Dock").ok();
let corner_radii: CornerRadii = roundness.into();
let radius = corner_radii.radius_xl[0] as u32;
let mut panel_config = panel_config_helper.as_ref().and_then(|config_helper| {
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
(panel_config.name == "Panel").then_some(panel_config)
});
for name in ["Panel", "Dock"] {
let Some((helper, mut config)) = Self::load_panel_config(name) else {
continue;
};
let mut dock_config = dock_config_helper.as_ref().and_then(|config_helper| {
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
(panel_config.name == "Dock").then_some(panel_config)
});
if let Some(panel_config_helper) = panel_config_helper.as_ref()
&& let Some(panel_config) = panel_config.as_mut()
{
let radii = if panel_config.anchor_gap {
let cornder_radii: CornerRadii = roundness.into();
cornder_radii.radius_xl[0] as u32
} else if matches!(roundness, Roundness::Round) && !panel_config.expand_to_edges {
12
let new_radius = if config.anchor_gap {
radius
} else if !config.expand_to_edges {
radius.min(12)
} else {
0
};
if let Err(why) = panel_config.set_border_radius(panel_config_helper, radii) {
tracing::error!(?why, "Error updating panel corner radii");
}
}
if let Some(dock_config_helper) = dock_config_helper.as_ref()
&& let Some(dock_config) = dock_config.as_mut()
{
let radii = if dock_config.anchor_gap {
let cornder_radii: CornerRadii = roundness.into();
cornder_radii.radius_xl[0] as u32
} else if matches!(roundness, Roundness::Round) && !dock_config.expand_to_edges {
12
} else {
0
};
if let Err(why) = dock_config.set_border_radius(dock_config_helper, radii) {
tracing::error!(?why, "Error updating dock corner radii");
if let Err(why) = config.set_border_radius(&helper, new_radius) {
tracing::error!(?why, "Error updating {name} corner radii");
}
}
}
#[cfg(feature = "wayland")]
pub fn update_dock_padding(roundness: Roundness) {
let dock_config_helper = CosmicPanelConfig::cosmic_config("Dock").ok();
let Some((helper, mut config)) = Self::load_panel_config("Dock") else {
return;
};
let mut dock_config = dock_config_helper.as_ref().and_then(|config_helper| {
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
(panel_config.name == "Dock").then_some(panel_config)
});
let padding = match roundness {
Roundness::Round | Roundness::SlightlyRound => 4,
Roundness::Square => 0,
};
if let Some(dock_config_helper) = dock_config_helper.as_ref()
&& let Some(dock_config) = dock_config.as_mut()
{
let padding = match roundness {
Roundness::Round => 4,
Roundness::SlightlyRound => 4,
Roundness::Square => 0,
};
if let Err(why) = dock_config.set_padding(dock_config_helper, padding) {
tracing::error!(?why, "Error updating dock padding");
}
if let Err(why) = config.set_padding(&helper, padding) {
tracing::error!(?why, "Error updating dock padding");
}
}
// TODO: cache panel and dock configs so that they needn't be re-read
#[cfg(feature = "wayland")]
pub fn update_panel_spacing(density: Density) {
let spacing: cosmic::cosmic_theme::Spacing = density.into();
let space_none = spacing.space_none;
let panel_config_helper = CosmicPanelConfig::cosmic_config("Panel").ok();
let dock_config_helper = CosmicPanelConfig::cosmic_config("Dock").ok();
let mut panel_config = panel_config_helper.as_ref().and_then(|config_helper| {
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
(panel_config.name == "Panel").then_some(panel_config)
});
let mut dock_config = dock_config_helper.as_ref().and_then(|config_helper| {
let panel_config = CosmicPanelConfig::get_entry(config_helper).ok()?;
(panel_config.name == "Dock").then_some(panel_config)
});
let space_none = spacing.space_none as u32;
if let Some(panel_config_helper) = panel_config_helper.as_ref()
&& let Some(panel_config) = panel_config.as_mut()
{
let update = panel_config.set_spacing(panel_config_helper, space_none as u32);
if let Err(err) = update {
tracing::error!(?err, "Error updating panel spacing");
for name in ["Panel", "Dock"] {
let Some((helper, mut config)) = Self::load_panel_config(name) else {
continue;
};
if let Err(err) = config.set_spacing(&helper, space_none) {
tracing::error!(?err, "Error updating {name} spacing");
}
};
if let Some(dock_config_helper) = dock_config_helper.as_ref()
&& let Some(dock_config) = dock_config.as_mut()
{
let update = dock_config.set_spacing(dock_config_helper, space_none as u32);
if let Err(err) = update {
tracing::error!(?err, "Error updating dock spacing");
}
};
}
}
fn can_reset(&self) -> bool {

View file

@ -1,9 +1,12 @@
use cosmic::iced::core::{Alignment, Length, text::Wrapping};
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::core::{Alignment, Length};
use cosmic::widget::icon::{from_name, icon};
use cosmic::widget::{button, container, settings, text};
use cosmic::{Apply, Element};
use cosmic_settings_page::Section;
use crate::pages::desktop::appearance::ContextView;
use super::{Message, Page, Roundness};
#[allow(clippy::too_many_lines)]
@ -12,6 +15,7 @@ pub fn section() -> Section<crate::pages::Message> {
round = fl!("style", "round");
slightly_round = fl!("style", "slightly-round");
square = fl!("style", "square");
frosted_glass = fl!("style", "frosted-glass");
});
let dark_round_style = from_name("illustration-appearance-dark-style-round").handle();
@ -136,6 +140,10 @@ pub fn section() -> Section<crate::pages::Message> {
)
.center_x(Length::Fill),
)
.add(crate::widget::go_next_item(
&descriptions[frosted_glass],
Message::DrawerOpen(ContextView::FrostedGlass),
))
.apply(Element::from)
.map(crate::pages::Message::Appearance)
})

View file

@ -1,8 +1,8 @@
use cosmic::cosmic_config::{Config, ConfigSet, CosmicConfigEntry};
use cosmic::cosmic_theme::palette::{Srgb, Srgba};
use cosmic::cosmic_theme::{
CornerRadii, DARK_THEME_BUILDER_ID, LIGHT_THEME_BUILDER_ID, Spacing, Theme, ThemeBuilder,
ThemeMode,
AlphaMap, BlurStrength, CornerRadii, DARK_THEME_BUILDER_ID, LIGHT_THEME_BUILDER_ID, Spacing,
Theme, ThemeBuilder, ThemeMode,
};
use cosmic::iced::core::Color;
@ -74,7 +74,7 @@ impl From<(Option<Config>, Option<Config>, Option<Vec<Srgba>>)> for ThemeCustomi
.success(theme.success.base.color)
.warning(theme.warning.base.color)
.neutral_tint(theme.palette.neutral_5.color)
.text_tint(theme.background.on.color);
.text_tint(theme.background(false).on.color);
theme_builder.gaps = theme.gaps;
@ -160,20 +160,6 @@ impl Default for Manager {
impl Manager {
pub fn build_theme(&mut self, stage: ThemeStaged) -> Task<app::Message> {
macro_rules! theme_transaction {
($config:ident, $current_theme:ident, $new_theme:ident, { $($name:ident;)+ }) => {
let tx = $config.transaction();
$(
if $current_theme.$name != $new_theme.$name {
_ = tx.set(stringify!($name), $new_theme.$name.clone());
}
)+
_ = tx.commit();
}
}
let map_data_fn = |customizer: &ThemeCustomizer| {
(customizer.builder.0.clone(), customizer.theme.1.clone())
};
@ -199,32 +185,80 @@ impl Manager {
cosmic::task::future(async move {
for (builder, config) in data.by_ref() {
if let Some(config) = config {
let current_theme = match Theme::get_entry(&config) {
let cur = match Theme::get_entry(&config) {
Ok(theme) => theme,
Err((_errs, theme)) => theme,
};
let new_theme = builder.build();
theme_transaction!(config, current_theme, new_theme, {
accent;
accent_button;
background;
button;
destructive;
destructive_button;
link_button;
icon_button;
palette;
primary;
secondary;
shade;
success;
text_button;
warning;
warning_button;
window_hint;
accent_text;
});
let new = builder.build();
// do this without the macro
let tx = config.transaction();
if cur.accent != new.accent {
_ = tx.set("accent", new.accent.clone());
}
if cur.accent_button != new.accent_button {
_ = tx.set("accent_button", new.accent_button.clone());
}
if cur.background(false) != new.background(false) {
_ = tx.set("background", new.background(false).clone());
}
if cur.background(true) != new.background(true) {
_ = tx.set("transparent_background", new.background(true).clone());
}
if cur.button != new.button {
_ = tx.set("button", new.button.clone());
}
if cur.destructive != new.destructive {
_ = tx.set("destructive", new.destructive.clone());
}
if cur.destructive_button != new.destructive_button {
_ = tx.set("destructive_button", new.destructive_button.clone());
}
if cur.link_button != new.link_button {
_ = tx.set("link_button", new.link_button.clone());
}
if cur.icon_button != new.icon_button {
_ = tx.set("icon_button", new.icon_button.clone());
}
if cur.palette != new.palette {
_ = tx.set("palette", new.palette.clone());
}
if cur.primary(false) != new.primary(false) {
_ = tx.set("primary", new.primary(false).clone());
}
if cur.secondary(false) != new.secondary(false) {
_ = tx.set("secondary", new.secondary(false).clone());
}
if cur.primary(true) != new.primary(true) {
_ = tx.set("transparent_primary", new.primary(true).clone());
}
if cur.secondary(true) != new.secondary(true) {
_ = tx.set("transparent_secondary", new.secondary(true).clone());
}
if cur.shade != new.shade {
_ = tx.set("shade", new.shade.clone());
}
if cur.success != new.success {
_ = tx.set("success", new.success.clone());
}
if cur.text_button != new.text_button {
_ = tx.set("text_button", new.text_button.clone());
}
if cur.warning != new.warning {
_ = tx.set("warning", new.warning.clone());
}
if cur.warning_button != new.warning_button {
_ = tx.set("warning_button", new.warning_button.clone());
}
if cur.window_hint != new.window_hint {
_ = tx.set("window_hint", new.window_hint.clone());
}
if cur.accent_text != new.accent_text {
_ = tx.set("accent_text", new.accent_text.clone());
}
_ = tx.commit();
}
}
@ -360,6 +394,39 @@ impl Manager {
Some(ThemeStaged::Both)
}
pub fn set_frosted(&mut self, frosted: BlurStrength) -> Option<ThemeStaged> {
self.dark.set_frosted(frosted)?;
self.light.set_frosted(frosted)?;
Some(ThemeStaged::Both)
}
pub fn set_glass(&mut self, alpha_map: AlphaMap) -> Option<ThemeStaged> {
self.dark.set_glass(alpha_map)?;
self.light.set_glass(alpha_map)?;
Some(ThemeStaged::Both)
}
pub fn set_frosted_panel(&mut self, enabled: bool) -> Option<ThemeStaged> {
self.dark.set_frosted_panel(enabled)?;
self.light.set_frosted_panel(enabled)?;
Some(ThemeStaged::Both)
}
pub fn set_frosted_applets(&mut self, enabled: bool) -> Option<ThemeStaged> {
self.dark.set_frosted_applets(enabled)?;
self.light.set_frosted_applets(enabled)?;
Some(ThemeStaged::Both)
}
pub fn set_frosted_system_interface(&mut self, enabled: bool) -> Option<ThemeStaged> {
self.dark.set_frosted_system_interface(enabled)?;
self.light.set_frosted_system_interface(enabled)?;
Some(ThemeStaged::Both)
}
pub fn set_frosted_windows(&mut self, enabled: bool) -> Option<ThemeStaged> {
self.dark.set_frosted_windows(enabled)?;
self.light.set_frosted_windows(enabled)?;
Some(ThemeStaged::Both)
}
pub fn get_color(&self, context: &ContextView) -> Option<Color> {
match *context {
ContextView::CustomAccent => self.custom_accent().map(Color::from),
@ -540,6 +607,81 @@ impl ThemeCustomizer {
Some(ThemeStaged::Current)
}
pub fn set_frosted(&mut self, frosted: BlurStrength) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;
self.builder.0.set_frosted(config, frosted).ok()?;
self.theme
.0
.set_frosted(self.theme.1.as_ref()?, frosted)
.ok()?;
Some(ThemeStaged::Current)
}
pub fn set_glass(&mut self, alpha_map: AlphaMap) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;
self.builder.0.set_alpha_map(config, alpha_map).ok()?;
self.theme
.0
.set_alpha_map(self.theme.1.as_ref()?, alpha_map)
.ok()?;
Some(ThemeStaged::Current)
}
pub fn set_frosted_windows(&mut self, enabled: bool) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;
self.builder.0.set_frosted_windows(config, enabled).ok()?;
self.theme
.0
.set_frosted_windows(self.theme.1.as_ref()?, enabled)
.ok()?;
Some(ThemeStaged::Current)
}
pub fn set_frosted_system_interface(&mut self, enabled: bool) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;
self.builder
.0
.set_frosted_system_interface(config, enabled)
.ok()?;
self.theme
.0
.set_frosted_system_interface(self.theme.1.as_ref()?, enabled)
.ok()?;
Some(ThemeStaged::Current)
}
pub fn set_frosted_applets(&mut self, enabled: bool) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;
self.builder.0.set_frosted_applets(config, enabled).ok()?;
self.theme
.0
.set_frosted_applets(self.theme.1.as_ref()?, enabled)
.ok()?;
Some(ThemeStaged::Current)
}
pub fn set_frosted_panel(&mut self, enabled: bool) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;
self.builder.0.set_frosted_panel(config, enabled).ok()?;
self.theme
.0
.set_frosted_panel(self.theme.1.as_ref()?, enabled)
.ok()?;
Some(ThemeStaged::Current)
}
// set active hints is set on all themes to be consistent between dark & light themes.
pub fn set_active_hint(&mut self, active_hint: u32) -> Option<ThemeStaged> {
let config = self.builder.1.as_ref()?;

View file

@ -1,22 +1,16 @@
use cosmic::{
Apply, Element, Task,
app::ContextDrawer,
cosmic_config::CosmicConfigEntry,
iced::{Alignment, Length},
widget::{button, container, row},
};
use cosmic::app::ContextDrawer;
use cosmic::cosmic_config::CosmicConfigEntry;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, container, row};
use cosmic::{Apply, Element, Task};
use cosmic_panel_config::CosmicPanelConfig;
use cosmic_settings_page::{self as page, Section, section};
use slotmap::{Key, SlotMap};
use std::borrow::Cow;
use crate::{
app,
pages::{
self,
desktop::panel::applets_inner::{self, AppletsPage, ContextDrawerVariant, lists},
},
};
use crate::app;
use crate::pages::desktop::panel::applets_inner::{self, AppletsPage, ContextDrawerVariant, lists};
use crate::pages::{self};
pub(crate) struct Page {
inner: applets_inner::Page,

View file

@ -1,11 +1,8 @@
use std::collections::HashMap;
use cosmic::Apply;
use cosmic::{
Element, Task,
cosmic_config::{ConfigSet, CosmicConfigEntry},
widget::{settings, text},
};
use cosmic::cosmic_config::{ConfigSet, CosmicConfigEntry};
use cosmic::widget::{settings, text};
use cosmic::{Apply, Element, Task};
use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelContainerConfig};
use cosmic_settings_page::{self as page, Section, section};
use slotmap::SlotMap;

View file

@ -10,23 +10,21 @@ use cosmic::iced;
use cosmic::iced::core::clipboard::IconSurface;
use cosmic::widget::{Column, button, column, container, icon, list_column, row, text, text_input};
use cosmic::{
Apply, Element,
cosmic_config::{Config, CosmicConfigEntry},
iced::core::{
Clipboard, Shell, Widget, layout, renderer,
widget::{Operation, Tree, tree},
},
iced::runtime::{Task, core::id::Id},
iced::{
Alignment, Border, Color, Length, Point, Rectangle, Size, Vector, core::window, event,
mouse, overlay, touch,
},
theme,
use cosmic::cosmic_config::{Config, CosmicConfigEntry};
use cosmic::iced::core::widget::{Operation, Tree, tree};
use cosmic::iced::core::{Clipboard, Shell, Widget, layout, renderer, window};
use cosmic::iced::runtime::Task;
use cosmic::iced::runtime::core::id::Id;
use cosmic::iced::{
Alignment, Border, Color, Length, Point, Rectangle, Size, Vector, event, mouse, overlay, touch,
};
use cosmic::{Apply, Element, theme};
use std::borrow::Cow;
use std::fmt::Debug;
use std::mem;
use std::path::{Path, PathBuf};
use std::{borrow::Cow, fmt::Debug, mem, sync::LazyLock};
use std::sync::LazyLock;
use crate::{app, pages};
use cosmic_panel_config::CosmicPanelConfig;

View file

@ -1,12 +1,11 @@
use cosmic::{
Element, Task,
cctk::sctk::reexports::client::{Proxy, backend::ObjectId, protocol::wl_output::WlOutput},
cosmic_config::{self, CosmicConfigEntry},
cosmic_theme::Density,
iced::{Alignment, Length},
surface,
widget::{button, container, dropdown, row, settings, slider, space, text},
};
use cosmic::cctk::sctk::reexports::client::Proxy;
use cosmic::cctk::sctk::reexports::client::backend::ObjectId;
use cosmic::cctk::sctk::reexports::client::protocol::wl_output::WlOutput;
use cosmic::cosmic_config::{self, CosmicConfigEntry};
use cosmic::cosmic_theme::{Density, Roundness};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, container, dropdown, row, settings, slider, space, text};
use cosmic::{Element, Task, surface};
use cosmic::Apply;
use cosmic_config::ConfigSet;
@ -15,16 +14,15 @@ use cosmic_panel_config::{
CosmicPanelOuput, PanelAnchor, PanelSize,
};
use cosmic_settings_page::{self as page, Section};
use std::{collections::HashMap, time::Duration};
use crate::pages::desktop::appearance::Roundness;
use std::collections::HashMap;
use std::time::Duration;
pub struct PageInner {
pub(crate) config_helper: Option<cosmic_config::Config>,
pub(crate) panel_config: Option<CosmicPanelConfig>,
pub size: Option<PanelSize>,
pub opacity: f32,
pub opacity_changing: bool,
pub size: PanelSize,
pub outputs: Vec<String>,
pub anchors: Vec<String>,
pub backgrounds: Vec<String>,
@ -40,9 +38,9 @@ impl Default for PageInner {
Self {
config_helper: Option::default(),
panel_config: Option::default(),
size: Option::default(),
opacity: 0.0,
opacity_changing: false,
size: PanelSize::M,
outputs: vec![fl!("all-displays")],
anchors: vec![
Anchor(PanelAnchor::Left).to_string(),
@ -120,7 +118,7 @@ pub(crate) fn behavior_and_position<
.title(&section.title)
.add(
settings::item::builder(&descriptions[autohide_label])
.toggler(panel_config.autohide.is_some(), Message::AutoHidePanel),
.toggler(panel_config.autohide_enabled(), Message::AutoHidePanel),
)
.add(settings::item(
&descriptions[position],
@ -209,9 +207,7 @@ pub(crate) fn style<
text::body(fl!("small")).into(),
slider(
0..=4,
match inner.size.as_ref().expect(
"PageInner.size is None even though PageInner.panel_config is Some",
) {
match inner.size {
PanelSize::XS => 0,
PanelSize::S => 1,
PanelSize::M => 2,
@ -506,7 +502,7 @@ impl PageInner {
if let Err(err) = default.write_entry(config) {
tracing::error!(?err, "Error resetting panel config.");
}
self.size = Some(default.size.clone());
self.size.clone_from(&default.size);
self.system_default = Some(default.clone());
self.panel_config.clone_from(&self.system_default);
} else {
@ -546,18 +542,10 @@ impl PageInner {
Message::AutoHidePanel(enabled) => {
if enabled {
_ = panel_config.set_exclusive_zone(helper, false);
_ = panel_config.set_autohide(
helper,
Some(AutoHide {
wait_time: 1000,
transition_time: 200,
handle_size: 4,
unhide_delay: 200,
}),
);
_ = panel_config.set_autohide(helper, AutoHide::OnOverlap);
} else {
_ = panel_config.set_exclusive_zone(helper, true);
_ = panel_config.set_autohide(helper, None);
_ = panel_config.set_autohide(helper, AutoHide::Never);
}
}
Message::PanelAnchor(i) => {
@ -591,30 +579,21 @@ impl PageInner {
}
let theme = cosmic::theme::system_preference();
let theme = theme.cosmic();
let radius = theme.corner_radii;
let roundness: Roundness = radius.into();
let new_radius;
if enabled {
let radii = theme.corner_radii.radius_xl[0] as u32;
new_radius = radii;
} else if matches!(roundness, Roundness::Round) && !panel_config.expand_to_edges {
new_radius = 12;
let radius = theme.corner_radii.radius_xl[0] as u32;
let new_radius = if enabled {
radius
} else if !panel_config.expand_to_edges {
radius.min(12)
} else {
new_radius = 0;
}
0
};
_ = panel_config.set_border_radius(helper, new_radius).unwrap();
}
Message::PanelSize(size) => {
self.size = Some(size);
self.size = size;
}
Message::PanelSizeCommit => {
_ = panel_config.set_size(
helper,
self.size
.as_ref()
.expect("PageInner.size is None even though it should be Some, since PageInner.panel_config is Some")
.clone()
);
_ = panel_config.set_size(helper, self.size.clone());
// Reset any size overrides the user might have set
_ = panel_config.set_size_center(helper, None);
_ = panel_config.set_size_wings(helper, None);
@ -632,17 +611,14 @@ impl PageInner {
let theme = cosmic::theme::system_preference();
let theme = theme.cosmic();
let radius = theme.corner_radii;
let roundness: Roundness = radius.into();
let new_radius;
if panel_config.anchor_gap {
let radii = theme.corner_radii.radius_xl[0] as u32;
new_radius = radii;
} else if matches!(roundness, Roundness::Round) && !enabled {
new_radius = 12;
let radius = theme.corner_radii.radius_xl[0] as u32;
let new_radius = if panel_config.anchor_gap {
radius
} else if !enabled {
radius.min(12)
} else {
new_radius = 0;
}
0
};
_ = panel_config.set_border_radius(helper, new_radius).unwrap();
}
Message::OpacityRequest(opacity) => {
@ -677,7 +653,7 @@ impl PageInner {
}
}
Message::PanelConfig(c) => {
self.size = Some(c.size.clone());
self.size = c.size.clone();
self.panel_config = Some(*c);
return Task::none();
}

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use cosmic::{Task, cosmic_config::CosmicConfigEntry};
use cosmic::Task;
use cosmic::cosmic_config::CosmicConfigEntry;
use cosmic_panel_config::{CosmicPanelConfig, CosmicPanelContainerConfig};
use cosmic_settings_page::{self as page, Section, section};
use slotmap::SlotMap;
@ -79,7 +80,6 @@ impl Default for Page {
// If the config is not present, it will be created with the default values and the name will not match
(panel_config.name == "Panel").then_some(panel_config)
});
let size = panel_config.as_ref().map(|c| c.size.clone());
let system_default = cosmic::cosmic_config::Config::system(
&format!("{}.Panel", cosmic_panel_config::NAME),
CosmicPanelConfig::VERSION,
@ -99,7 +99,6 @@ impl Default for Page {
inner: PageInner {
config_helper,
panel_config,
size,
container_config,
outputs_map: HashMap::new(),
system_default,

View file

@ -7,37 +7,25 @@ pub mod widgets;
pub use config::Config;
use url::Url;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cosmic::app::ContextDrawer;
#[cfg(feature = "xdg-portal")]
use cosmic::dialog::file_chooser;
use cosmic::{
Apply, Element, Task,
iced::core::text::{Ellipsize, EllipsizeHeightLimit},
widget::{ColorPickerModel, color_picker::ColorPickerUpdate, icon},
};
use cosmic::{app::ContextDrawer, iced::runtime::core::image::Handle as ImageHandle};
use cosmic::{
iced::Subscription,
widget::{
button, dropdown, list_column, row,
segmented_button::{self, SingleSelectModel},
settings,
space::horizontal as horizontal_space,
tab_bar, text,
},
};
use cosmic::{
iced::{Alignment, Color, Length, window},
surface,
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::runtime::core::image::Handle as ImageHandle;
use cosmic::iced::{Alignment, Color, Length, Subscription, window};
use cosmic::widget::color_picker::ColorPickerUpdate;
use cosmic::widget::segmented_button::{self, SingleSelectModel};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::{
ColorPickerModel, button, dropdown, icon, list_column, row, settings, tab_bar, text,
};
use cosmic::{Apply, Element, Task, surface};
use cosmic_bg_config::Source;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use cosmic_settings_wallpaper::{self as wallpaper, Entry, ScalingMode};
use image::imageops::FilterType::Lanczos3;
use image::{ImageBuffer, Rgba};
@ -77,7 +65,7 @@ pub enum Message {
AddFolder(Arc<Result<Url, file_chooser::Error>>),
/// Adds a new image file the system wallpaper folder.
#[cfg(feature = "xdg-portal")]
AddFile(Arc<Result<Url, file_chooser::Error>>),
AddFiles(Arc<Result<Vec<Url>, file_chooser::Error>>),
/// Cache currently displayed image.
CacheDisplayImage,
/// Selects an option in the category dropdown menu.
@ -799,11 +787,11 @@ impl Page {
.title(fl!("wallpaper", "image-dialog"))
.accept_label(fl!("dialog-add"))
.modal(false)
.open_file()
.open_files()
.await
.map(|response| response.url().to_owned());
.map(|response| response.urls().to_vec());
let message = Message::AddFile(Arc::new(dialog_result));
let message = Message::AddFiles(Arc::new(dialog_result));
let page_message = crate::pages::Message::DesktopWallpaper(message);
crate::Message::PageMessage(page_message)
});
@ -874,6 +862,11 @@ impl Page {
#[cfg(feature = "xdg-portal")]
Message::AddFolder(result) => {
let path = match dialog_response(result) {
DialogResponse::Paths(_) => {
// TODO find if there is a use of multi folder addition.
tracing::warn!("unexpected multi-path response for folder dialog");
return Task::none();
}
DialogResponse::Path(path) => path,
DialogResponse::Error(why) => {
tracing::error!(why, "dialog response error");
@ -899,27 +892,30 @@ impl Page {
}
#[cfg(feature = "xdg-portal")]
Message::AddFile(result) => {
let path = match dialog_response(result) {
DialogResponse::Path(path) => path,
Message::AddFiles(result) => {
let paths = match dialog_response_multi(result) {
DialogResponse::Paths(paths) => paths,
DialogResponse::Path(path) => vec![path], // not really but for the sake of consistency..
DialogResponse::Error(why) => {
tracing::error!(why, "dialog response error");
return Task::none();
}
};
if path.is_file() {
let tasks = paths.into_iter().filter(|path| path.is_file()).map(|path| {
tracing::info!(?path, "opening custom image");
// Loads a single custom image and its thumbnail for display in the backgrounds view.
return cosmic::Task::future(async move {
cosmic::Task::future(async move {
let result = wallpaper::load_image_with_thumbnail(path);
let message = Message::ImageAdd(result.map(Arc::new));
let page_message = crate::pages::Message::DesktopWallpaper(message);
crate::Message::PageMessage(page_message)
});
}
})
});
return Task::batch(tasks);
}
Message::Event(event) => match event {
@ -1429,27 +1425,33 @@ pub fn settings() -> Section<crate::pages::Message> {
enum DialogResponse {
Error(String),
Path(PathBuf),
Paths(Vec<PathBuf>),
}
#[cfg(feature = "xdg-portal")]
fn unwrap_dialog_response<T>(result: Arc<Result<T, file_chooser::Error>>) -> Result<T, String> {
let Some(result) = Arc::into_inner(result) else {
return Err(String::from("Arc::into_inner returned None"));
};
result.map_err(|why| {
let mut source: &dyn std::error::Error = &why;
let mut string = format!("open dialog subscription errored\n cause: {source}");
while let Some(new_source) = source.source() {
string.push_str(&format!("\n cause: {new_source}"));
source = new_source;
}
string
})
}
#[cfg(feature = "xdg-portal")]
fn dialog_response(result: Arc<Result<Url, file_chooser::Error>>) -> DialogResponse {
let Some(result) = Arc::into_inner(result) else {
return DialogResponse::Error(String::from("Arc::into_inner returned None"));
};
let selection = match result {
Ok(response) => response,
Err(why) => {
let mut source: &dyn std::error::Error = &why;
let mut string = format!("open dialog subscription errored\n cause: {source}");
while let Some(new_source) = source.source() {
string.push_str(&format!("\n cause: {new_source}"));
source = new_source;
}
return DialogResponse::Error(string);
}
let selection = match unwrap_dialog_response(result) {
Ok(selection) => selection,
Err(why) => return DialogResponse::Error(why),
};
let Ok(path) = selection.to_file_path() else {
@ -1458,3 +1460,22 @@ fn dialog_response(result: Arc<Result<Url, file_chooser::Error>>) -> DialogRespo
DialogResponse::Path(path)
}
#[cfg(feature = "xdg-portal")]
fn dialog_response_multi(result: Arc<Result<Vec<Url>, file_chooser::Error>>) -> DialogResponse {
let selections = match unwrap_dialog_response(result) {
Ok(selections) => selections,
Err(why) => return DialogResponse::Error(why),
};
let mut paths = Vec::with_capacity(selections.len());
for selection in selections {
let Ok(path) = selection.to_file_path() else {
tracing::warn!("not a valid file path: {}", selection.path());
continue;
};
paths.push(path);
}
DialogResponse::Paths(paths)
}

View file

@ -3,8 +3,8 @@
use super::Message;
use cosmic::iced::Radius;
use cosmic::iced::core::Border;
use cosmic::iced::core::{Background, Color, Degrees, Length, gradient::Linear};
use cosmic::iced::core::gradient::Linear;
use cosmic::iced::core::{Background, Border, Color, Degrees, Length};
use cosmic::iced::runtime::core::image::Handle as ImageHandle;
use cosmic::prelude::*;
use cosmic::widget::{Space, button, container};

View file

@ -1,18 +1,14 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{
Apply, Element,
iced::Length,
surface,
widget::{self, settings},
};
use cosmic::iced::Length;
use cosmic::widget::{self, settings};
use cosmic::{Apply, Element, surface};
use cosmic_comp_config::CosmicCompConfig;
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::{Action, Binding, Shortcuts, shortcuts};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use slotmap::SlotMap;
use tracing::error;
@ -96,7 +92,7 @@ impl Default for Page {
fl!("super-key", "launcher"),
fl!("super-key", "workspaces"),
fl!("super-key", "applications"),
fl!("super-key", "disable"),
fl!("super-key", "none"),
],
super_key_active: super_key_active_config(),
comp_config,
@ -247,7 +243,7 @@ pub fn window_management() -> Section<crate::pages::Message> {
_launcher = fl!("super-key", "launcher");
_workspaces = fl!("super-key", "workspaces");
_applications = fl!("super-key", "applications");
_disable = fl!("super-key", "disable");
_none = fl!("super-key", "none");
edge_gravity = fl!("edge-gravity");
});

View file

@ -3,15 +3,11 @@
// TODO make settings work
use cosmic::{
Apply, Element,
cosmic_config::{self, ConfigGet, ConfigSet},
surface,
widget::{self, settings},
};
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
use cosmic::widget::{self, settings};
use cosmic::{Apply, Element, surface};
use cosmic_comp_config::workspace::{Action, WorkspaceConfig, WorkspaceLayout, WorkspaceMode};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use slotmap::SlotMap;
use tracing::error;

View file

@ -5,11 +5,9 @@ use cosmic::Renderer;
use cosmic::iced::core::renderer::Quad;
use cosmic::iced::core::widget::{Tree, tree};
use cosmic::iced::core::{
self as core, Border, Clipboard, Element, Layout, Length, Rectangle, Renderer as IcedRenderer,
Shell, Size, Widget,
self as core, Border, Clipboard, Element, Layout, Length, Point, Rectangle,
Renderer as IcedRenderer, Shell, Size, Widget, alignment, layout, mouse, renderer, text, touch,
};
use cosmic::iced::core::{Point, layout, mouse, renderer, touch};
use cosmic::iced::core::{alignment, text};
use cosmic::widget::segmented_button::{self, SingleSelectModel};
use cosmic_randr_shell::{self as randr, OutputKey};
use randr::Transform;
@ -246,11 +244,13 @@ impl<Message: Clone> Widget<Message, cosmic::Theme, Renderer> for Arrangement<'_
if let Some(ref on_placement) = self.on_placement {
shell.publish(on_placement(
output_key,
((region.x - state.max_dimensions.0 - bounds.x) * UNIT_PIXELS) as i32,
((region.x - state.max_dimensions.0 - bounds.x) * UNIT_PIXELS).round()
as i32,
((region.y
- (state.max_dimensions.1 / VERTICAL_DISPLAY_OVERHEAD)
- bounds.y)
* UNIT_PIXELS) as i32,
* UNIT_PIXELS)
.round() as i32,
));
}

View file

@ -20,9 +20,10 @@ use cosmic_settings_page::{self as page, Section, section};
use futures::SinkExt;
use indexmap::Equivalent;
use slotmap::{Key, SecondaryMap, SlotMap};
use std::collections::BTreeMap;
use std::process::ExitStatus;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock};
use std::{collections::BTreeMap, process::ExitStatus};
use tokio::sync::oneshot;
static DPI_SCALES: &[u32] = &[50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300];
@ -912,8 +913,8 @@ impl Page {
Some(orientation) => self.set_dialog(
Randr::Transform(match orientation {
1 => Transform::Rotate90,
2 => Transform::Flipped180,
3 => Transform::Flipped270,
2 => Transform::Rotate180,
3 => Transform::Rotate270,
_ => Transform::Normal,
}),
&request,
@ -1102,6 +1103,12 @@ impl Page {
.arg(itoa::Buffer::new().format(x))
.arg("--pos-y")
.arg(itoa::Buffer::new().format(y))
.arg("--refresh")
.arg(format!(
"{}.{:03}",
current.refresh_rate / 1000,
current.refresh_rate % 1000
))
.arg(name)
.arg(itoa::Buffer::new().format(current.size.0))
.arg(itoa::Buffer::new().format(current.size.1));

View file

@ -5,50 +5,96 @@ pub mod shortcuts;
use std::cmp;
use cosmic::{
Apply, Element, Task,
app::{ContextDrawer, context_drawer},
cosmic_config::{self, ConfigSet},
iced::{Alignment, Length},
theme,
widget::{self, ListColumn, button, container, icon, list, row, settings},
};
use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::cosmic_config::{self, ConfigSet};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, ListColumn, button, container, icon, list, row, settings};
use cosmic::{Apply, Element, Task, theme};
use cosmic_comp_config::{KeyboardConfig, NumlockState, XkbConfig};
use cosmic_settings_page::{self as page, Section, section};
use itertools::Itertools;
use slotmap::{DefaultKey, Key, SlotMap};
static COMPOSE_OPTIONS: &[(&str, &str)] = &[
// ("Left Alt", "compose:lalt"), XXX?
("Right Alt", "compose:ralt"),
("Left Super", "compose:lwin"),
("Right Super", "compose:rwin"),
("Menu key", "compose:menu"),
("Right Ctrl", "compose:rctrl"),
("Caps Lock", "compose:caps"),
("Scroll Lock", "compose:sclk"),
("Print Screen", "compose:prsc"),
];
/// Contains all options for mapping a [SpecialKey].
/// The available options differ for each key being mapped.
enum SpecialKeyAlternative {
AltLeft,
AltRight,
Ctrl,
CtrlRight,
SwapWithCtrl,
Super,
SuperLeft,
SuperRight,
CapsLock,
ScrollLock,
MenuKey,
Backspace,
Escape,
SwapWithEscape,
PrintScreen,
None,
}
impl SpecialKeyAlternative {
fn display_text(&self) -> String {
match self {
Self::AltLeft => fl!("keyboard-special-char", "alt-left"),
Self::AltRight => fl!("keyboard-special-char", "alt-right"),
Self::Ctrl => fl!("keyboard-special-char", "ctrl"),
Self::CtrlRight => fl!("keyboard-special-char", "ctrl-right"),
Self::SwapWithCtrl => fl!("keyboard-special-char", "swap-with-ctrl"),
Self::Super => fl!("keyboard-special-char", "super"),
Self::SuperLeft => fl!("keyboard-special-char", "super-left"),
Self::SuperRight => fl!("keyboard-special-char", "super-right"),
Self::CapsLock => fl!("keyboard-special-char", "caps"),
Self::ScrollLock => fl!("keyboard-special-char", "scroll-lock"),
Self::MenuKey => fl!("keyboard-special-char", "menu"),
Self::Backspace => fl!("keyboard-special-char", "backspace"),
Self::Escape => fl!("keyboard-special-char", "escape"),
Self::SwapWithEscape => fl!("keyboard-special-char", "swap-with-escape"),
Self::PrintScreen => fl!("keyboard-special-char", "print-screen"),
Self::None => fl!("keyboard-special-char", "none"),
}
}
}
static ALTERNATE_CHARACTER_OPTIONS: &[(&str, &str)] = &[
("Left Alt", "lv3:lalt_switch"),
("Right Alt", "lv3:ralt_switch"),
("Left Super", "lv3:lwin_switch"),
("Right Super", "lv3:rwin_switch"),
("Menu key", "lv3:menu_switch"),
/// The second/right value is the key identifier, which is how it will be saved to the configuration.
/// These values are different for each [SpecialKey], even for the same [SpecialKeyAlternative].
/// The identifier starts with a prefix defined by [SpecialKey::prefixes].
type SpecialKeyOption = (SpecialKeyAlternative, &'static str);
/// Available options for [SpecialKey::Compose].
static COMPOSE_OPTIONS: &[SpecialKeyOption] = &[
// ("Left Alt", "compose:lalt"), XXX?
(SpecialKeyAlternative::AltRight, "compose:ralt"),
(SpecialKeyAlternative::SuperLeft, "compose:lwin"),
(SpecialKeyAlternative::SuperRight, "compose:rwin"),
(SpecialKeyAlternative::MenuKey, "compose:menu"),
(SpecialKeyAlternative::CtrlRight, "compose:rctrl"),
(SpecialKeyAlternative::CapsLock, "compose:caps"),
(SpecialKeyAlternative::ScrollLock, "compose:sclk"),
(SpecialKeyAlternative::PrintScreen, "compose:prsc"),
];
/// Available options for [SpecialKey::AlternateCharacters].
static ALTERNATE_CHARACTER_OPTIONS: &[SpecialKeyOption] = &[
(SpecialKeyAlternative::AltLeft, "lv3:lalt_switch"),
(SpecialKeyAlternative::AltRight, "lv3:ralt_switch"),
(SpecialKeyAlternative::SuperLeft, "lv3:lwin_switch"),
(SpecialKeyAlternative::SuperRight, "lv3:rwin_switch"),
(SpecialKeyAlternative::MenuKey, "lv3:menu_switch"),
// ("Right Ctrl", "lv3:"), XXX
("Caps Lock", "lv3:caps_switch"),
(SpecialKeyAlternative::CapsLock, "lv3:caps_switch"),
// ("Scroll Lock", "lv3:"), XXX
// ("Print Screen", "lv3"), XXX
];
static CAPS_LOCK_OPTIONS: &[(&str, &str)] = &[
("Escape", "caps:escape"),
("Swap with Escape", "caps:swapescape"),
("Backspace", "caps:backspace"),
("Super", "caps:super"),
("Control", "caps:ctrl_modifier"),
("Swap with Control", "ctrl:swapcaps"),
/// Available options for [SpecialKey::CapsLock].
static CAPS_LOCK_OPTIONS: &[SpecialKeyOption] = &[
(SpecialKeyAlternative::Escape, "caps:escape"),
(SpecialKeyAlternative::SwapWithEscape, "caps:swapescape"),
(SpecialKeyAlternative::Backspace, "caps:backspace"),
(SpecialKeyAlternative::Super, "caps:super"),
(SpecialKeyAlternative::Ctrl, "caps:ctrl_modifier"),
(SpecialKeyAlternative::SwapWithCtrl, "ctrl:swapcaps"),
];
#[derive(Clone, Debug)]
@ -72,8 +118,8 @@ pub enum SourceContext {
MoveDown(DefaultKey),
MoveUp(DefaultKey),
Remove(DefaultKey),
Settings(DefaultKey),
ViewLayout(DefaultKey),
// Settings(DefaultKey),
// ViewLayout(DefaultKey),
}
pub type Locale = String;
@ -146,9 +192,9 @@ pub enum SpecialKey {
impl SpecialKey {
pub fn title(self) -> String {
match self {
Self::Compose => "Compose".to_string(),
Self::AlternateCharacters => "Alternate Characters".to_string(),
Self::CapsLock => "Caps Lock".to_string(),
Self::Compose => fl!("keyboard-special-char", "compose"),
Self::AlternateCharacters => fl!("keyboard-special-char", "alternate"),
Self::CapsLock => fl!("keyboard-special-char", "caps"),
}
}
@ -165,6 +211,7 @@ fn popover_menu_row(
id: DefaultKey,
label: String,
message: impl Fn(DefaultKey) -> SourceContext + 'static,
disabled: bool,
) -> cosmic::Element<'static, Message> {
let spacing = theme::spacing();
widget::text::body(label)
@ -173,37 +220,54 @@ fn popover_menu_row(
.padding([spacing.space_xxxs, spacing.space_xs])
.width(Length::Fill)
.class(theme::Button::MenuItem)
.on_press(Message::SourceContext(message(id)))
.on_press_maybe(if disabled {
None
} else {
Some(Message::SourceContext(message(id)))
})
.apply(Element::from)
}
fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> {
fn popover_menu(
id: DefaultKey,
is_first: bool,
is_last: bool,
) -> cosmic::Element<'static, Message> {
widget::column::with_children([
popover_menu_row(
id,
fl!("keyboard-sources", "move-up"),
SourceContext::MoveUp,
is_first,
),
popover_menu_row(
id,
fl!("keyboard-sources", "move-down"),
SourceContext::MoveDown,
is_last,
),
widget::divider::horizontal::default()
.apply(widget::container)
.padding(8)
.into(),
// popover_menu_row(
// id,
// fl!("keyboard-sources", "settings"),
// SourceContext::Settings,
// false,
// ),
// popover_menu_row(
// id,
// fl!("keyboard-sources", "view-layout"),
// SourceContext::ViewLayout,
// false,
// ),
popover_menu_row(
id,
fl!("keyboard-sources", "settings"),
SourceContext::Settings,
fl!("keyboard-sources", "remove"),
SourceContext::Remove,
false,
),
popover_menu_row(
id,
fl!("keyboard-sources", "view-layout"),
SourceContext::ViewLayout,
),
popover_menu_row(id, fl!("keyboard-sources", "remove"), SourceContext::Remove),
])
.width(Length::Fixed(200.0))
.apply(widget::container)
@ -212,7 +276,12 @@ fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> {
.into()
}
fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Message> {
fn popover_button(
id: DefaultKey,
expanded: bool,
is_first: bool,
is_last: bool,
) -> cosmic::Element<'static, Message> {
let on_press = Message::ExpandInputSourcePopover(if expanded { None } else { Some(id) });
let button = button::icon(icon::from_name("view-more-symbolic"))
@ -222,7 +291,7 @@ fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Me
if expanded {
widget::popover(button)
.position(widget::popover::Position::Bottom)
.popup(popover_menu(id))
.popup(popover_menu(id, is_first, is_last))
.on_close(Message::ExpandInputSourcePopover(None))
.into()
} else {
@ -234,14 +303,16 @@ fn input_source(
id: DefaultKey,
description: &str,
expanded_source_popover: Option<DefaultKey>,
is_first: bool,
is_last: bool,
) -> cosmic::Element<'_, Message> {
let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id);
settings::item(description, popover_button(id, expanded)).into()
settings::item(description, popover_button(id, expanded, is_first, is_last)).into()
}
fn special_char_radio_row<'a>(
desc: &'a str,
desc: String,
value: Option<&'static str>,
current_value: Option<&'a str>,
) -> list::ListButton<'a, Message> {
@ -475,15 +546,13 @@ impl Page {
let _removed = self.active_layouts.remove(pos);
self.update_xkb_config();
}
}
} // SourceContext::Settings(_id) => {
// eprintln!("settings not implemented");
// }
SourceContext::Settings(_id) => {
eprintln!("settings not implemented");
}
SourceContext::ViewLayout(_id) => {
eprintln!("view layout not implemented");
}
// SourceContext::ViewLayout(_id) => {
// eprintln!("view layout not implemented");
// }
}
}
@ -621,14 +690,22 @@ impl Page {
let mut list = cosmic::widget::list_column();
if matches!(special_key, SpecialKey::CapsLock) {
list = list.add(special_char_radio_row("Caps Lock", None, current));
list = list.add(special_char_radio_row(
SpecialKeyAlternative::CapsLock.display_text(),
None,
current,
));
} else {
list = list.add(special_char_radio_row("None", None, current));
list = list.add(special_char_radio_row(
SpecialKeyAlternative::None.display_text(),
None,
current,
));
}
list = options
.iter()
.map(|(desc, id)| special_char_radio_row(desc, Some(id), current))
.map(|(key, id)| special_char_radio_row(key.display_text(), Some(id), current))
.fold(list, ListColumn::add);
widget::column::with_capacity(2)
@ -693,12 +770,17 @@ fn input_sources() -> Section<crate::pages::Message> {
// TODO Need something more custom, with drag and drop
let mut section = settings::section().title(&section.title);
for id in &page.active_layouts {
for (position, id) in page.active_layouts.iter().enumerate() {
if let Some((_locale, _variant, description, _source)) =
page.keyboard_layouts.get(*id)
{
section =
section.add(input_source(*id, description, page.expanded_source_popover));
section = section.add(input_source(
*id,
description,
page.expanded_source_popover,
position == 0,
position + 1 == page.active_layouts.len(),
));
}
}

View file

@ -8,7 +8,7 @@ use cosmic::iced::keyboard::{Key, Location, Modifiers};
use cosmic::iced::platform_specific::shell::wayland::commands::keyboard_shortcuts_inhibit;
use cosmic::iced::platform_specific::shell::wayland::keymap;
use cosmic::iced::{self, Alignment, Length};
use cosmic::widget::{self, button, icon, settings, text};
use cosmic::widget::{self, button, icon, list, settings, text};
use cosmic::{Apply, Element, Task, theme};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::shortcuts::{self, Action, Binding, Shortcuts};
@ -652,7 +652,8 @@ impl Model {
modifiers,
..
}) => {
use cosmic::iced::keyboard::{Key, key::Named};
use cosmic::iced::keyboard::Key;
use cosmic::iced::keyboard::key::Named;
if matches!(
key,
@ -669,7 +670,8 @@ impl Model {
location,
..
}) => {
use cosmic::iced::keyboard::{Key, key::Named};
use cosmic::iced::keyboard::Key;
use cosmic::iced::keyboard::key::Named;
if matches!(
key,
@ -865,13 +867,11 @@ fn context_drawer<'a>(
}
/// Display a shortcut as a list item
fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<'_, ShortcutMessage> {
#[derive(Copy, Clone, Debug)]
enum LocalMessage {
Remove,
Show,
}
fn shortcut_item(
custom: bool,
id: usize,
data: &ShortcutModel,
) -> list::ListButton<'_, ShortcutMessage> {
let bindings = data
.bindings
.iter()
@ -880,7 +880,7 @@ fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<'_, S
.map(|(_, shortcut)| text::body(shortcut.binding.to_string()).into())
.collect::<Vec<_>>();
let shortcuts: Element<LocalMessage> = if bindings.is_empty() {
let shortcuts: Element<ShortcutMessage> = if bindings.is_empty() {
text::body(fl!("disabled")).into()
} else {
widget::column::with_children(bindings)
@ -900,7 +900,7 @@ fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<'_, S
.push(icon::from_name("go-next-symbolic").size(16))
.push_maybe(custom.then(|| {
widget::button::icon(icon::from_name("edit-delete-symbolic"))
.on_press(LocalMessage::Remove)
.on_press(ShortcutMessage::DeleteShortcut(id))
}))
.align_y(Alignment::Center)
.spacing(8);
@ -909,14 +909,7 @@ fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<'_, S
.flex_control(control)
.align_items(Alignment::Center)
.spacing(16)
.apply(widget::container)
.class(theme::Container::List)
.apply(widget::button::custom)
.class(theme::Button::Transparent)
.on_press(LocalMessage::Show)
.apply(Element::from)
.map(move |message| match message {
LocalMessage::Show => ShortcutMessage::ShowShortcut(id, data.description.clone()),
LocalMessage::Remove => ShortcutMessage::DeleteShortcut(id),
})
.width(Length::Shrink)
.apply(list::button)
.on_press(ShortcutMessage::ShowShortcut(id, data.description.clone()))
}

View file

@ -590,7 +590,8 @@ impl page::Page<crate::pages::Message> for Page {
&self,
core: &cosmic::Core,
) -> cosmic::iced::Subscription<crate::pages::Message> {
use cosmic::iced::{self, event::listen_with};
use cosmic::iced::event::listen_with;
use cosmic::iced::{self};
cosmic::iced::Subscription::batch(vec![
if self.add_shortcut.active
@ -605,7 +606,8 @@ impl page::Page<crate::pages::Message> for Page {
modifiers,
..
}) => {
use cosmic::iced::keyboard::{Key, key::Named};
use cosmic::iced::keyboard::Key;
use cosmic::iced::keyboard::key::Named;
if matches!(
key,
Key::Named(Named::Super | Named::Alt | Named::Control | Named::Shift)
@ -624,7 +626,8 @@ impl page::Page<crate::pages::Message> for Page {
location,
..
}) => {
use cosmic::iced::keyboard::{Key, key::Named};
use cosmic::iced::keyboard::Key;
use cosmic::iced::keyboard::key::Named;
if matches!(
key,
Key::Named(Named::Super | Named::Alt | Named::Control | Named::Shift)

View file

@ -2,8 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only
use super::{ShortcutMessage, ShortcutModel};
use cosmic::app::ContextDrawer;
use cosmic::iced::platform_specific::shell::wayland::commands::keyboard_shortcuts_inhibit;
use cosmic::{Element, Task, app::ContextDrawer};
use cosmic::{Element, Task};
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_config::shortcuts::action::ResizeDirection;
use cosmic_settings_page::{self as page, Section, section};

View file

@ -15,16 +15,16 @@ pub mod tiling;
use cosmic::app::ContextDrawer;
use cosmic::iced::Length;
use cosmic::widget::{self, icon, settings, text};
use cosmic::{Apply, Element, Task, theme};
use cosmic::widget::list::ListButton;
use cosmic::widget::{self, settings, text};
use cosmic::{Apply, Element, Task};
use cosmic_config::ConfigGet;
use cosmic_settings_config::Binding;
use cosmic_settings_config::shortcuts::action::{
Direction, FocusDirection, Orientation, ResizeDirection,
};
use cosmic_settings_config::shortcuts::{self, Action, Shortcuts};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use itertools::Itertools;
use shortcuts::action::System as SystemAction;
use slab::Slab;
@ -222,11 +222,13 @@ impl Page {
self.search.localized = SecondaryMap::new();
self.search.input = String::new();
self.search_model.on_clear();
self.modified.custom = 0;
self.modified.accessibility = 0;
self.modified.manage_windows = 0;
self.modified.move_windows = 0;
self.modified.nav = 0;
self.modified.system = 0;
self.modified.window_tiling = 0;
self.modified.custom = 0;
}
fn reload_search(&mut self) {
@ -408,11 +410,13 @@ impl Search {
.fold(
Slab::with_capacity(self.actions.len()),
|mut slab, (_, action)| {
slab.insert(ShortcutModel::new(
&self.defaults,
&self.shortcuts,
action.clone(),
));
if !slab.iter().any(|(_, model)| &model.action == action) {
slab.insert(ShortcutModel::new(
&self.defaults,
&self.shortcuts,
action.clone(),
));
}
slab
},
@ -497,32 +501,18 @@ fn shortcuts() -> Section<crate::pages::Message> {
}
/// Display a category as a list item
fn category_item(category: Category, name: &str, modified: u16) -> Element<'_, Message> {
let icon = icon::from_name("go-next-symbolic").size(16);
let control = if modified == 0 {
Element::from(icon)
} else {
widget::row::with_capacity(2)
.push(text::body(fl!("modified", count = modified)))
.push(icon)
.into()
};
settings::item::builder(name)
.control(control)
.spacing(16)
.apply(widget::container)
.class(theme::Container::List)
.apply(widget::button::custom)
.class(theme::Button::Transparent)
.width(Length::Fill)
.on_press(Message::Category(category))
.into()
fn category_item(category: Category, name: &str, modified: u16) -> ListButton<'_, Message> {
crate::widget::go_next_with_item(
name,
(modified > 0).then(|| text::body(fl!("modified", count = modified)).apply(Element::from)),
Message::Category(category),
)
}
fn action_category(action: &Action) -> Option<Category> {
Some(if manage_windows::actions().contains(action) {
Some(if accessibility::actions().contains(action) {
Category::Accessibility
} else if manage_windows::actions().contains(action) {
Category::ManageWindow
} else if move_window::actions().contains(action) {
Category::MoveWindow
@ -530,6 +520,8 @@ fn action_category(action: &Action) -> Option<Category> {
Category::Nav
} else if system::actions().contains(action) {
Category::System
} else if tiling::actions().contains(action) {
Category::WindowTiling
} else {
return None;
})

View file

@ -2,8 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only
//
use super::{ShortcutMessage, ShortcutModel};
use cosmic::app::ContextDrawer;
use cosmic::iced::platform_specific::shell::wayland::commands::keyboard_shortcuts_inhibit;
use cosmic::{Element, Task, app::ContextDrawer};
use cosmic::{Element, Task};
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_config::shortcuts::action::Direction;
use cosmic_settings_page::{self as page, Section, section};

View file

@ -2,8 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only
use super::{ShortcutMessage, ShortcutModel};
use cosmic::app::ContextDrawer;
use cosmic::iced::platform_specific::shell::wayland::commands::keyboard_shortcuts_inhibit;
use cosmic::{Element, Task, app::ContextDrawer};
use cosmic::{Element, Task};
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_config::shortcuts::action::System as SystemAction;
use cosmic_settings_page::{self as page, Section, section};

View file

@ -2,8 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only
use super::{ShortcutMessage, ShortcutModel};
use cosmic::app::ContextDrawer;
use cosmic::iced::platform_specific::shell::wayland::commands::keyboard_shortcuts_inhibit;
use cosmic::{Element, Task, app::ContextDrawer};
use cosmic::{Element, Task};
use cosmic_settings_config::shortcuts::Action;
use cosmic_settings_config::shortcuts::action::Orientation;
use cosmic_settings_page::{self as page, Section, section};

View file

@ -2,10 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::app;
use cosmic::{
Task,
cosmic_config::{self, ConfigGet, ConfigSet},
};
use cosmic::Task;
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
use cosmic_comp_config::input::{
AccelConfig, AccelProfile, ClickMethod, InputConfig, ScrollConfig, ScrollMethod, TapButtonMap,
TapConfig,

View file

@ -5,8 +5,7 @@ use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, row, settings, text};
use cosmic::{Apply, Element};
use cosmic_comp_config::input::AccelProfile;
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use slotmap::SlotMap;
use super::Message;

View file

@ -8,8 +8,7 @@ use cosmic::{Apply, Element};
use cosmic_comp_config::CosmicCompConfig;
use cosmic_comp_config::input::{AccelProfile, ClickMethod, ScrollMethod};
use cosmic_comp_config::workspace::{WorkspaceConfig, WorkspaceLayout};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use slotmap::SlotMap;
use tracing::error;

View file

@ -17,6 +17,8 @@ pub mod input;
pub mod networking;
#[cfg(feature = "page-power")]
pub mod power;
#[cfg(feature = "page-printers")]
pub mod printers;
#[cfg(feature = "page-sound")]
pub mod sound;
pub mod system;
@ -82,11 +84,15 @@ pub enum Message {
PanelApplet(desktop::panel::applets_inner::Message),
#[cfg(feature = "page-power")]
Power(power::Message),
#[cfg(feature = "page-printers")]
Printers(printers::Message),
#[cfg(feature = "page-region")]
Region(time::region::Message),
#[cfg(feature = "page-sound")]
Sound(sound::Message),
#[cfg(feature = "page-sound")]
SoundApplications(sound::applications::Message),
#[cfg(feature = "page-sound")]
SoundDeviceProfiles(sound::device_profiles::Message),
StartupApps(applications::startup_apps::Message),
#[cfg(feature = "page-users")]

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,24 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod backend;
pub mod vpn;
pub mod wifi;
pub mod wired;
use std::{ffi::OsStr, process::Stdio, sync::Arc};
use std::process::Stdio;
use std::sync::Arc;
use anyhow::Context;
use cosmic::{Apply, Element, Task, widget};
use cosmic_dbus_networkmanager::{
interface::enums::{DeviceState, DeviceType},
nm::NetworkManager,
};
use cosmic_settings_network_manager_subscription as network_manager;
use cosmic_settings_page::{self as page, Section, section};
use futures::{SinkExt, StreamExt};
use secure_string::SecureString;
use slotmap::SlotMap;
use self::backend as network_manager;
use self::backend::devices::{DeviceState, DeviceType};
pub type SecretSender = Arc<tokio::sync::Mutex<Option<tokio::sync::oneshot::Sender<SecureString>>>>;
static NM_CONNECTION_EDITOR: &str = "nm-connection-editor";
@ -37,8 +37,8 @@ pub struct Page {
pub enum Message {
/// An error occurred.
Error(String),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Open the wifi settings page with the selected device.
OpenPage {
page: page::Entity,
@ -143,6 +143,7 @@ impl page::Page<crate::pages::Message> for Page {
fl!("network-device-state", "unavailable")
}
DeviceState::Unknown => fl!("network-device-state", "unknown"),
DeviceState::Other(_) => fl!("network-device-state", "unknown"),
DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"),
},
"preferences-wireless-symbolic",
@ -190,6 +191,7 @@ impl page::Page<crate::pages::Message> for Page {
fl!("network-device-state", "unplugged")
}
DeviceState::Unknown => fl!("network-device-state", "unknown"),
DeviceState::Other(_) => fl!("network-device-state", "unknown"),
DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"),
},
"preferences-wired-symbolic",
@ -225,9 +227,9 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
if self.nm_task.is_none() {
return cosmic::Task::future(async move {
zbus::Connection::system()
nmrs::NetworkManager::new()
.await
.context("failed to create system dbus connection")
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
@ -310,33 +312,47 @@ impl Page {
Task::none()
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, nm: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let network_manager = match NetworkManager::new(&conn).await {
Ok(n) => n,
Err(why) => {
tracing::error!(
why = why.to_string(),
"failed to connect to network_manager"
);
return futures::future::pending().await;
match network_manager::devices::list(&nm, |_| true).await {
Ok(devices) => {
_ = sender
.send(crate::pages::Message::Networking(Message::UpdateDevices(
devices.into_iter().map(Arc::new).collect(),
)))
.await;
}
};
Err(why) => {
_ = sender
.send(crate::pages::Message::Networking(Message::Error(
why.to_string(),
)))
.await;
}
}
let mut devices_changed =
std::pin::pin!(network_manager.receive_devices_changed().await.then(
|_| async {
match network_manager::devices::list(&conn, |_| true).await {
Ok(devices) => Message::UpdateDevices(
let Ok(events) = nm.network_events().await else {
return futures::future::pending().await;
};
let devices_changed = events.filter_map(|event| async {
match event {
Ok(nmrs::NetworkEvent::DeviceChanged { .. })
| Ok(nmrs::NetworkEvent::SettingsChanged(_))
| Ok(nmrs::NetworkEvent::NetworkManagerRestarted) => {
match network_manager::devices::list(&nm, |_| true).await {
Ok(devices) => Some(Message::UpdateDevices(
devices.into_iter().map(Arc::new).collect(),
),
Err(why) => Message::Error(why.to_string()),
)),
Err(why) => Some(Message::Error(why.to_string())),
}
}
));
Ok(_) => None,
Err(why) => Some(Message::Error(why.to_string())),
}
});
futures::pin_mut!(devices_changed);
while let Some(message) = devices_changed.next().await {
_ = sender
@ -353,16 +369,6 @@ impl Page {
}
}
async fn nm_add_vpn_file<P: AsRef<OsStr>>(type_: &str, path: P) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args(["connection", "import", "type", type_, "file"])
.arg(path)
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}
async fn nm_add_wired() -> Result<(), String> {
nm_connection_editor(&["--type=802-3-ethernet", "-c"]).await
}

View file

@ -1,30 +1,27 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod nmcli;
use std::collections::HashMap;
use std::sync::{Arc, LazyLock};
use anyhow::Context;
use cosmic::dialog::file_chooser::FileFilter;
use cosmic::task;
use cosmic::{
Apply, Element, Task,
iced::core::text::Wrapping,
iced::{Alignment, Length},
widget::{self, icon, space::horizontal as horizontal_space, text_input::focus},
};
use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag};
use cosmic_settings_network_manager_subscription::{
self as network_manager, NetworkManagerState, UUID, current_networks::ActiveConnectionInfo,
};
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::text_input::focus;
use cosmic::widget::{self, icon};
use cosmic::{Apply, Element, Task, task};
use cosmic_settings_page::{self as page, Section, section};
use futures::{FutureExt, SinkExt, StreamExt};
use indexmap::IndexMap;
use secure_string::SecureString;
use tokio::sync::Mutex;
use super::backend as network_manager;
use super::backend::current_networks::ActiveConnectionInfo;
use super::backend::nm_secret_agent::{self, PasswordFlag};
use super::backend::{NetworkManagerState, UUID};
use crate::pages::networking::SecretSender;
pub static SECURE_INPUT_VPN: LazyLock<widget::Id> = LazyLock::new(widget::Id::unique);
@ -60,8 +57,8 @@ pub enum Message {
NetworkManager(network_manager::Event),
/// An update from the secret agent
SecretAgent(network_manager::nm_secret_agent::Event),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Updates the password text input
PasswordUpdate(SecureString),
/// Refresh devices and their connection profiles
@ -95,6 +92,7 @@ pub enum ErrorKind {
ConnectionEditor,
ConnectionSettings,
DbusConnection,
OpenVpnConfigPath,
UpdatingState,
WireGuardConfigPath,
WireGuardDevice,
@ -109,6 +107,7 @@ impl ErrorKind {
ErrorKind::ConnectionEditor => fl!("vpn-error", "connection-editor"),
ErrorKind::ConnectionSettings => fl!("vpn-error", "connection-settings"),
ErrorKind::DbusConnection => fl!("dbus-connection-error"),
ErrorKind::OpenVpnConfigPath => fl!("vpn-error", "openvpn-config-path"),
ErrorKind::UpdatingState => fl!("vpn-error", "updating-state"),
ErrorKind::WireGuardConfigPath => fl!("vpn-error", "wireguard-config-path"),
ErrorKind::WireGuardDevice => fl!("vpn-error", "wireguard-device"),
@ -179,7 +178,7 @@ pub enum VpnDialog {
#[derive(Debug)]
pub struct NmState {
conn: zbus::Connection,
conn: nmrs::NetworkManager,
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
active_conns: Vec<ActiveConnectionInfo>,
devices: Vec<network_manager::devices::DeviceInfo>,
@ -369,20 +368,22 @@ impl page::Page<crate::pages::Message> for Page {
let (tx, rx) = tokio::sync::mpsc::channel(4);
self.secret_tx = Some(tx);
if self.nm_task.is_none() {
return cosmic::Task::batch([cosmic::task::future(async move {
zbus::Connection::system()
.await
.context("failed to create system dbus connection")
.map_or_else(
|why| Message::Error(ErrorKind::DbusConnection, why.to_string()),
Message::NetworkManagerConnect,
)
}),
cosmic::Task::stream(
cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", rx),
)
.map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))),
]);
return cosmic::Task::batch([
cosmic::task::future(async move {
nmrs::NetworkManager::new()
.await
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(ErrorKind::DbusConnection, why.to_string()),
Message::NetworkManagerConnect,
)
}),
cosmic::Task::stream(nm_secret_agent::secret_agent_stream(
"com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent",
rx,
))
.map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))),
]);
}
cosmic::Task::none()
@ -428,7 +429,19 @@ impl Page {
}
}
Message::KnownConnections(connections) => {
self.known_connections = connections;
let mut connections: Vec<_> = connections.into_iter().collect();
connections.sort_by(|a, b| {
let name_a = match &a.1 {
ConnectionSettings::Vpn(s) => s.id.as_str(),
ConnectionSettings::Wireguard { id } => id.as_str(),
};
let name_b = match &b.1 {
ConnectionSettings::Vpn(s) => s.id.as_str(),
ConnectionSettings::Wireguard { id } => id.as_str(),
};
name_a.to_lowercase().cmp(&name_b.to_lowercase())
});
self.known_connections = connections.into_iter().collect();
}
Message::UpdateDevices(devices) => {
self.update_devices(devices);
@ -481,9 +494,8 @@ impl Page {
Message::WireGuardConfig => {
if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() {
return cosmic::task::future(async move {
let new_path = path.replace(&filename, &device);
_ = std::fs::rename(&path, &new_path);
match super::nm_add_vpn_file("wireguard", new_path).await {
let _ = filename;
match network_manager::import_wireguard(path, &device).await {
Ok(_) => Message::Refresh,
Err(why) => Message::Error(ErrorKind::Config, why.to_string()),
}
@ -496,18 +508,12 @@ impl Page {
if let Some(settings) = self.known_connections.get(&uuid) {
let settings = match settings {
ConnectionSettings::Vpn(settings) => settings,
ConnectionSettings::Wireguard { id } => {
let connection_name = id.clone();
return cosmic::task::future(async move {
if let Err(why) = nmcli::connect(&connection_name).await {
return Message::Error(
ErrorKind::Connect,
format!("failed to connect to WireGuard VPN: {why}"),
);
}
Message::Refresh
});
ConnectionSettings::Wireguard { .. } => {
if let Some(NmState { ref sender, .. }) = self.nm_state {
_ = sender
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
}
return Task::none();
}
};
@ -527,28 +533,10 @@ impl Page {
return task::message(Message::FocusSecureInput);
}
_ => {
let connection_name = settings.id.clone();
let username = settings.username.clone();
return cosmic::task::future(async move {
if let Err(why) = nmcli::connect(&connection_name).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((
ErrorKind::Connect,
format!("failed to connect to VPN: {why}"),
)),
id: connection_name.clone(),
uuid,
username: username.clone(),
description: None,
password: SecureString::from(""),
password_hidden: true,
// TODO grab from the current dialog
tx: Arc::new(Mutex::new(None)),
});
}
Message::Refresh
});
if let Some(NmState { ref sender, .. }) = self.nm_state {
_ = sender
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
}
}
}
}
@ -615,89 +603,65 @@ impl Page {
};
if let VpnDialog::Password {
id,
id: _,
uuid,
username,
password,
tx,
..
} = dialog
&& let Some(NmState { ref sender, .. }) = self.nm_state
{
let username_unwrapped = username.clone().unwrap_or_default();
let task = self.activate_with_password(
id.clone(),
uuid.clone(),
username_unwrapped.clone(),
password.clone(),
);
let username = username.clone().unwrap_or_default();
let sec_tx = self.secret_tx.clone();
return task
.then(move |_| {
let sec_tx = sec_tx.clone();
let uuid = uuid.clone();
let username = username.clone();
let password = password.clone();
let tx = tx.clone();
let id = id.clone();
Task::future(async move {
let mut guard = tx.lock().await;
if let Some(sender) = guard.take() {
let _ = sender.send(password);
} else {
// apply password and username then
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) =
tokio::sync::oneshot::channel();
if let Err(err) = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([
// username and password
(
"username".to_string(),
username.clone().unwrap_or_default().into(),
),
("password".to_string(), password.clone()),
]),
applied_tx,
})
.await
{
tracing::error!(%err, "failed to apply secret");
}
// wait max 1s for the applied signal
if let Err(err) = tokio::time::timeout(
std::time::Duration::from_secs(1),
applied_rx,
)
.await
{
tracing::error!(%err, "failed to apply secret");
}
}
// activate
if let Err(why) = nmcli::connect(&id).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((
ErrorKind::Connect,
format!("failed to connect to VPN: {why}"),
)),
id: id.clone(),
uuid,
username: username.clone(),
description: None,
let nm_sender = sender.clone();
return Task::future(async move {
let mut guard = tx.lock().await;
if let Some(sender) = guard.take() {
let _ = sender.send(password);
} else {
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
if let Err(err) = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([(
"password".to_string(),
password,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
}
)]),
applied_tx,
})
.await
{
tracing::error!(%err, "failed to apply secret");
}
if let Err(err) = tokio::time::timeout(
std::time::Duration::from_secs(1),
applied_rx,
)
.await
{
tracing::error!(%err, "failed to apply secret");
}
}
Message::Refresh
})
})
.map(crate::app::Message::from);
if !username.is_empty() {
_ = nm_sender.unbounded_send(
network_manager::Request::SetVpnUsername {
uuid: uuid.clone(),
username,
},
);
}
_ = nm_sender
.unbounded_send(network_manager::Request::ActivateVpn(uuid));
}
Message::Refresh
})
.map(crate::app::Message::from);
}
}
Message::RetryWithPassword => {
@ -706,53 +670,49 @@ impl Page {
};
if let VpnDialog::Password {
id,
id: _,
uuid,
username,
password,
..
} = dialog
&& let Some(NmState { ref sender, .. }) = self.nm_state
{
let username_unwrapped = username.unwrap_or_default();
let username = username.unwrap_or_default();
let sec_tx = self.secret_tx.clone();
let task = self.activate_with_password(
id.clone(),
uuid.clone(),
username_unwrapped.clone(),
password.clone(),
);
return task
.then(move |_| {
let sec_tx = sec_tx.clone();
let uuid = uuid.clone();
let username = username_unwrapped.clone();
let password = password.clone();
Task::future(async move {
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
let _ = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([
// username and password
("username".to_string(), username.clone().into()),
("password".to_string(), password.clone()),
]),
applied_tx,
})
.await;
// wait max 1s for the applied signal
let _ = tokio::time::timeout(
std::time::Duration::from_secs(1),
applied_rx,
)
let nm_sender = sender.clone();
return Task::future(async move {
if let Some(sec_tx) = sec_tx {
let (applied_tx, applied_rx) = tokio::sync::oneshot::channel();
let _ = sec_tx
.send(nm_secret_agent::Request::SetSecrets {
setting_name: "vpn".to_string(),
uuid: uuid.to_string(),
secrets: HashMap::from_iter([(
"password".to_string(),
password,
)]),
applied_tx,
})
.await;
let _ =
tokio::time::timeout(std::time::Duration::from_secs(1), applied_rx)
.await;
}
Message::Activate(uuid)
})
})
.map(crate::app::Message::from);
}
if !username.is_empty() {
_ = nm_sender.unbounded_send(
network_manager::Request::SetVpnUsername {
uuid: uuid.clone(),
username,
},
);
}
_ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid));
Message::Refresh
})
.map(crate::app::Message::from);
}
}
Message::UsernameUpdate(user) => {
@ -859,56 +819,14 @@ impl Page {
Task::none()
}
fn activate_with_password(
&mut self,
connection_name: String,
uuid: Arc<str>,
username: String,
password: SecureString,
) -> Task<Message> {
cosmic::task::future(async move {
if let Err(why) = nmcli::set_username(&connection_name, &username).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((ErrorKind::WithPassword("username"), why.to_string())),
id: connection_name.clone(),
uuid,
username: Some(username),
description: None,
password,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
}
if let Err(why) = nmcli::add_fallback(&connection_name).await {
return Message::VpnDialogError(VpnDialog::Password {
error: Some((ErrorKind::Config, why.to_string())),
id: connection_name.clone(),
uuid,
username: Some(username),
password,
description: None,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
}
Message::Refresh
})
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let watchers = std::pin::pin!(async move {
futures::join!(
network_manager::watch(conn.clone(), tx.clone()),
network_manager::active_conns::watch(conn.clone(), tx.clone()),
network_manager::devices::watch(conn, true, tx)
)
network_manager::watch(conn, tx).await;
});
let forwarder = std::pin::pin!(async move {
@ -1005,7 +923,20 @@ fn devices_view() -> Section<crate::pages::Message> {
widget::text::body(fl!("no-vpn")).into(),
])));
} else {
let known_networks = page.known_connections.iter().fold(
let mut known_connections: Vec<_> = page.known_connections.iter().collect();
known_connections.sort_by(|a, b| {
let name_a = match a.1 {
ConnectionSettings::Vpn(s) => s.id.as_str(),
ConnectionSettings::Wireguard { id } => id.as_str(),
};
let name_b = match b.1 {
ConnectionSettings::Vpn(s) => s.id.as_str(),
ConnectionSettings::Wireguard { id } => id.as_str(),
};
name_a.to_lowercase().cmp(&name_b.to_lowercase())
});
let known_networks = known_connections.into_iter().fold(
vpn_connections,
|networks, (uuid, connection)| {
let id = match connection {
@ -1114,7 +1045,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
.into()
}
fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
@ -1123,7 +1054,7 @@ fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard);
@ -1174,8 +1105,16 @@ fn add_network() -> Task<crate::app::Message> {
return Message::AddWireGuardDevice(device, filename.to_owned(), path);
} else {
super::nm_add_vpn_file("openvpn", response.url().to_file_path().unwrap())
let Ok(path) = response.url().to_file_path() else {
return Message::Error(
ErrorKind::OpenVpnConfigPath,
fl!("vpn-error", "openvpn-config-path-desc"),
);
};
network_manager::import_openvpn(path)
.await
.map_err(|why| why.to_string())
};
match result {
@ -1190,108 +1129,96 @@ fn add_network() -> Task<crate::app::Message> {
.apply(cosmic::task::future)
}
fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
let settings = async move {
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
_ = settings.load_connections(&[]).await;
let settings = settings
// Get a list of known connections.
.list_connections()
.await?
// Prepare for wrapping in a concurrent stream.
.into_iter()
.map(|conn| async move { conn })
// Create a concurrent stream for each connection.
.apply(futures::stream::FuturesOrdered::from_iter)
// Concurrently fetch settings for each connection, and filter for VPN.
.filter_map(|conn| async move {
let settings = conn.get_settings().await.ok()?;
let connection = settings.get("connection")?;
match connection
.get("type")?
.downcast_ref::<String>()
.ok()?
.as_str()
{
"vpn" => (),
"wireguard" => {
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
return Some((Arc::from(uuid), ConnectionSettings::Wireguard { id }));
}
_ => return None,
}
let vpn = settings.get("vpn")?;
let id = connection.get("id")?.downcast_ref::<String>().ok()?;
let uuid = connection.get("uuid")?.downcast_ref::<String>().ok()?;
let (connection_type, username, password_flag) = vpn
.get("data")
.and_then(|data| data.downcast_ref::<zbus::zvariant::Dict>().ok())
.map(|dict| {
let (mut connection_type, mut password_flag) = (None, None);
let mut username = vpn
.get("user-name")
.and_then(|u| u.downcast_ref::<String>().ok());
if dict
.get::<String, String>(&String::from("connection-type"))
.ok()
.flatten()
.as_deref()
// may be "password" or "password-tls"
.is_some_and(|p| p.starts_with("password"))
{
connection_type = Some(ConnectionType::Password);
username = Some(username.unwrap_or_default());
password_flag = dict
.get::<String, String>(&String::from("password-flags"))
.ok()
.flatten()
.and_then(|value| match value.as_str() {
"0" => Some(PasswordFlag::None),
"1" => Some(PasswordFlag::AgentOwned),
"2" => Some(PasswordFlag::NotSaved),
"4" => Some(PasswordFlag::NotRequired),
_ => None,
});
}
(connection_type, username, password_flag)
})
.unwrap_or_default();
Some((
Arc::from(uuid),
ConnectionSettings::Vpn(VpnConnectionSettings {
id,
connection_type,
password_flag,
username,
}),
))
})
// Reduce the settings list into
.fold(IndexMap::new(), |mut set, (uuid, data)| async move {
set.insert(uuid, data);
set
})
.await;
Ok::<_, zbus::Error>(settings)
};
fn connection_settings(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let settings = async move {
let vpns = conn.list_vpn_connections().await?;
let mut settings = IndexMap::new();
for vpn in vpns {
let uuid = Arc::from(vpn.uuid.as_str());
let data = match vpn.vpn_type {
nmrs::VpnType::WireGuard { .. } => ConnectionSettings::Wireguard { id: vpn.id },
nmrs::VpnType::OpenVpn {
connection_type,
user_name,
password_flags,
..
} => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: user_name,
connection_type: connection_type
.filter(|ct| {
matches!(
ct,
nmrs::OpenVpnConnectionType::Password
| nmrs::OpenVpnConnectionType::PasswordTls
)
})
.map(|_| ConnectionType::Password),
password_flag: Some(password_flag(password_flags.0)),
}),
nmrs::VpnType::OpenConnect {
user_name,
password_flags,
..
}
| nmrs::VpnType::StrongSwan {
user_name,
password_flags,
..
}
| nmrs::VpnType::Pptp {
user_name,
password_flags,
..
}
| nmrs::VpnType::L2tp {
user_name,
password_flags,
..
} => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: user_name,
connection_type: Some(ConnectionType::Password),
password_flag: Some(password_flag(password_flags.0)),
}),
nmrs::VpnType::Generic {
user_name,
password_flags,
..
} => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: user_name,
connection_type: Some(ConnectionType::Password),
password_flag: Some(password_flag(password_flags.0)),
}),
_ => ConnectionSettings::Vpn(VpnConnectionSettings {
id: vpn.id,
username: None,
connection_type: None,
password_flag: None,
}),
};
settings.insert(uuid, data);
}
Ok::<_, nmrs::ConnectionError>(settings)
};
settings.await.map_or_else(
|why| Message::Error(ErrorKind::ConnectionSettings, why.to_string()),
Message::KnownConnections,
)
})
}
fn password_flag(value: u32) -> PasswordFlag {
match value {
0 => PasswordFlag::None,
1 => PasswordFlag::AgentOwned,
2 => PasswordFlag::NotSaved,
4 => PasswordFlag::NotRequired,
_ => PasswordFlag::AgentOwned,
}
}

View file

@ -1,38 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::Apply;
use std::process::Stdio;
pub async fn set_username(connection_name: &str, username: &str) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args(["con", "mod", connection_name, "vpn.user-name", username])
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}
pub async fn add_fallback(connection_name: &str) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args([
"con",
"mod",
connection_name,
"+vpn.data",
"data-ciphers=AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC:AES-128-CBC",
])
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}
pub async fn connect(connection_name: &str) -> Result<(), String> {
tokio::process::Command::new("nmcli")
.args(["con", "up", connection_name])
.stderr(Stdio::piped())
.output()
.await
.apply(crate::utils::map_stderr_output)
}

View file

@ -1,31 +1,27 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{BTreeMap, BTreeSet},
sync::{Arc, LazyLock},
};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, LazyLock};
use anyhow::Context;
use cosmic::{
Apply, Element, Task,
app::ContextDrawer,
iced::core::text::Wrapping,
iced::{Alignment, Length, widget::operation::focus_next},
task,
widget::{self, column, icon, space::horizontal, text_input::focus},
};
use cosmic_settings_network_manager_subscription::{
self as network_manager, NetworkManagerState,
available_wifi::{AccessPoint, NetworkType},
current_networks::ActiveConnectionInfo,
nm_secret_agent,
};
use cosmic::app::ContextDrawer;
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::widget::operation::focus_next;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::space::horizontal;
use cosmic::widget::text_input::focus;
use cosmic::widget::{self, column, icon};
use cosmic::{Apply, Element, Task, task};
use cosmic_settings_page::{self as page, Section, section};
use futures::{SinkExt, StreamExt};
use secure_string::SecureString;
use tokio::sync::Mutex;
use super::backend as network_manager;
use super::backend::available_wifi::{AccessPoint, NetworkType};
use super::backend::current_networks::ActiveConnectionInfo;
use super::backend::{NetworkManagerState, nm_secret_agent};
use crate::pages::networking::SecretSender;
pub static SECURE_INPUT_WIFI: LazyLock<widget::Id> = LazyLock::new(widget::Id::unique);
@ -58,8 +54,8 @@ pub enum Message {
NetworkManager(network_manager::Event),
/// An update from the secret agent
SecretAgent(network_manager::nm_secret_agent::Event),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Request an auth dialog
PasswordRequest(network_manager::SSID),
/// Update the password from the dialog
@ -105,6 +101,7 @@ enum WiFiDialog {
ssid: network_manager::SSID,
identity: Option<String>,
password: SecureString,
network_type: NetworkType,
password_hidden: bool,
tx: SecretSender,
},
@ -144,7 +141,7 @@ pub struct Page {
#[derive(Debug)]
pub struct NmState {
conn: zbus::Connection,
conn: nmrs::NetworkManager,
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
state: network_manager::NetworkManagerState,
devices: Vec<network_manager::devices::DeviceInfo>,
@ -297,19 +294,23 @@ impl page::Page<crate::pages::Message> for Page {
let (tx, rx) = tokio::sync::mpsc::channel(4);
self.secret_tx = Some(tx);
if self.nm_task.is_none() {
return Task::batch(vec![cosmic::Task::future(async move {
zbus::Connection::system()
.await
.context("failed to create system dbus connection")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
)
.apply(crate::pages::Message::WiFi)
}), cosmic::Task::stream(
cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", rx),
)
.map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m)))]);
return Task::batch(vec![
cosmic::Task::future(async move {
nmrs::NetworkManager::new()
.await
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
)
.apply(crate::pages::Message::WiFi)
}),
cosmic::Task::stream(nm_secret_agent::secret_agent_stream(
"com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent",
rx,
))
.map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m))),
]);
}
Task::none()
@ -355,15 +356,22 @@ impl Page {
}
match req {
network_manager::Request::Authenticate { ssid, identity, .. } => {
network_manager::Request::Authenticate {
ssid,
identity,
network_type,
..
} => {
if success {
self.connecting.remove(ssid.as_str());
} else {
self.connecting.remove(ssid.as_str());
// Request to retry
self.dialog = Some(WiFiDialog::Password {
ssid: ssid.into(),
identity,
password: SecureString::from(""),
network_type,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
@ -380,11 +388,13 @@ impl Page {
if success || matches!(network_type, NetworkType::Open) {
self.connecting.remove(ssid.as_ref());
} else {
self.connecting.remove(ssid.as_ref());
self.dialog = Some(WiFiDialog::Password {
ssid,
identity: matches!(network_type, NetworkType::EAP)
.then(String::new),
password: SecureString::from(""),
network_type,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
@ -452,6 +462,7 @@ impl Page {
let qr_string = if let Some(ref pass) = password {
let security = match security_type {
NetworkType::PskOrSae => "WPA",
NetworkType::Sae => "WPA",
NetworkType::EAP => "WPA",
NetworkType::Open => "",
};
@ -524,6 +535,7 @@ impl Page {
ssid,
identity: matches!(ap.network_type, NetworkType::EAP).then(String::new),
password: SecureString::from(""),
network_type: ap.network_type,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
});
@ -547,6 +559,7 @@ impl Page {
ssid,
identity,
password,
network_type,
tx,
..
} = dialog
@ -565,6 +578,7 @@ impl Page {
ssid: ssid.to_string(),
identity,
password,
network_type,
secret_tx,
interface,
});
@ -617,6 +631,7 @@ impl Page {
}
Message::Disconnect(ssid) => {
self.close_popup_and_apply_updates();
self.connecting.remove(ssid.as_ref());
if let Some(nm) = self.nm_state.as_mut() {
_ = nm
.sender
@ -630,6 +645,7 @@ impl Page {
Message::Forget(ssid) => {
self.dialog = None;
self.close_popup_and_apply_updates();
self.connecting.remove(ssid.as_ref());
if let Some(nm) = self.nm_state.as_mut() {
_ = nm
.sender
@ -651,6 +667,9 @@ impl Page {
}
}
Message::WiFiEnable(enable) => {
if !enable {
self.connecting.clear();
}
if let Some(nm) = self.nm_state.as_mut() {
_ = nm
.sender
@ -659,7 +678,9 @@ impl Page {
}
}
Message::CancelDialog => {
self.dialog = None;
if let Some(WiFiDialog::Password { ssid, .. }) = self.dialog.take() {
self.connecting.remove(ssid.as_ref());
}
}
Message::Error(why) => {
tracing::error!(why);
@ -718,15 +739,20 @@ impl Page {
password: previous,
password_hidden: true,
identity: matches!(ap.network_type, NetworkType::EAP).then(String::new),
network_type: ap.network_type,
tx,
});
return task::message(Message::FocusSecureInput);
}
nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => {
self.dialog = self
.dialog
.take()
.filter(|d| !matches!(d, &WiFiDialog::Password { .. }));
match self.dialog.take() {
Some(WiFiDialog::Password { ssid, .. }) => {
self.connecting.remove(ssid.as_ref());
}
other => {
self.dialog = other;
}
}
}
nm_secret_agent::Event::Failed(error) => {
tracing::error!(%error, "secret agent failure");
@ -734,15 +760,18 @@ impl Page {
ssid,
password,
identity,
network_type,
..
}) = self.dialog.take()
{
self.connecting.remove(ssid.as_ref());
self.dialog = Some(WiFiDialog::Password {
password,
password_hidden: true,
tx: Arc::new(Mutex::new(None)),
ssid,
identity,
network_type,
});
return task::message(Message::FocusSecureInput);
}
@ -773,19 +802,14 @@ impl Page {
Task::none()
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let watchers = std::pin::pin!(async move {
futures::join!(
network_manager::watch(conn.clone(), tx.clone()),
network_manager::active_conns::watch(conn.clone(), tx.clone()),
network_manager::wireless_enabled::watch(conn.clone(), tx.clone()),
network_manager::watch_connections_changed(conn, tx)
);
network_manager::watch(conn, tx).await;
});
let forwarder = std::pin::pin!(async move {
@ -835,6 +859,12 @@ impl Page {
/// Withholds updates if the view more popup is displayed.
fn update_state(&mut self, state: NetworkManagerState) {
for active in &state.active_conns {
if let ActiveConnectionInfo::WiFi { name, .. } = active {
self.connecting.remove(name.as_str());
}
}
if let Some(ref mut nm_state) = self.nm_state {
if self.view_more_popup.is_some() {
self.withheld_state = Some(state);
@ -1176,10 +1206,9 @@ fn devices_view() -> Section<crate::pages::Message> {
// Search input (only shown when 15+ networks)
if show_search {
let search_input =
widget::search_input(fl!("type-to-search"), &page.search_query)
.on_input(Message::SearchQuery)
.on_clear(Message::SearchQuery(String::new()));
let search_input = widget::search_input("", &page.search_query)
.on_input(Message::SearchQuery)
.on_clear(Message::SearchQuery(String::new()));
visible_section = visible_section.push(search_input);
}
@ -1275,51 +1304,9 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
.into()
}
fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
let settings = async move {
let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?;
_ = settings.load_connections(&[]).await;
let settings = settings
// Get a list of known connections.
.list_connections()
.await?
// Prepare for wrapping in a concurrent stream.
.into_iter()
.map(|conn| async move { conn })
// Create a concurrent stream for each connection.
.apply(futures::stream::FuturesOrdered::from_iter)
// Concurrently fetch settings for each connection.
.filter_map(|conn| async move {
conn.get_settings()
.await
.map(network_manager::Settings::new)
.ok()
})
// Reduce the settings list into a SSID->UUID map.
.fold(BTreeMap::new(), |mut set, settings| async move {
if let Some(ref wifi) = settings.wifi
&& let Some(ssid) = wifi
.ssid
.clone()
.and_then(|ssid| String::from_utf8(ssid).ok())
&& let Some(ref connection) = settings.connection
&& let Some(uuid) = connection.uuid.clone()
{
set.insert(ssid.into(), uuid.into());
return set;
}
set
})
.await;
Ok::<_, zbus::Error>(settings)
};
fn connection_settings(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
settings
network_manager::wifi_connection_settings(conn)
.await
.context("failed to get connection settings")
.map_or_else(
@ -1330,7 +1317,7 @@ fn connection_settings(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
pub fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
@ -1339,7 +1326,7 @@ pub fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
pub fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
pub fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi);

View file

@ -1,22 +1,23 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{collections::BTreeSet, sync::Arc};
use std::collections::BTreeSet;
use std::sync::Arc;
use anyhow::Context;
use cosmic::{
Apply, Element, Task,
iced::core::text::Wrapping,
iced::{Alignment, Length},
widget::{self, icon, space::horizontal as horizontal_space},
};
use cosmic_dbus_networkmanager::interface::enums::DeviceState;
use cosmic_settings_network_manager_subscription::{
self as network_manager, NetworkManagerState, current_networks::ActiveConnectionInfo,
};
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::{self, icon};
use cosmic::{Apply, Element, Task};
use cosmic_settings_page::{self as page, Section, section};
use futures::{SinkExt, StreamExt};
use super::backend as network_manager;
use super::backend::NetworkManagerState;
use super::backend::current_networks::ActiveConnectionInfo;
use super::backend::devices::DeviceState;
pub type ConnectionId = Arc<str>;
#[derive(Clone, Debug)]
@ -33,8 +34,8 @@ pub enum Message {
Error(String),
/// An update from the network manager daemon
NetworkManager(network_manager::Event),
/// Successfully connected to the system dbus.
NetworkManagerConnect(zbus::Connection),
/// Successfully connected to NetworkManager.
NetworkManagerConnect(nmrs::NetworkManager),
/// Refresh devices and their connection profiles
Refresh,
/// Create a dialog to ask for confirmation of removal.
@ -92,7 +93,7 @@ pub struct Page {
#[derive(Debug)]
pub struct NmState {
conn: zbus::Connection,
conn: nmrs::NetworkManager,
sender: futures::channel::mpsc::UnboundedSender<network_manager::Request>,
active_conns: Vec<ActiveConnectionInfo>,
devices: Vec<Arc<network_manager::devices::DeviceInfo>>,
@ -155,9 +156,9 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
if self.nm_task.is_none() {
return cosmic::task::future(async move {
zbus::Connection::system()
nmrs::NetworkManager::new()
.await
.context("failed to create system dbus connection")
.context("failed to connect to NetworkManager")
.map_or_else(
|why| Message::Error(why.to_string()),
Message::NetworkManagerConnect,
@ -357,18 +358,14 @@ impl Page {
Task::none()
}
fn connect(&mut self, conn: zbus::Connection) -> Task<crate::app::Message> {
fn connect(&mut self, conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
if self.nm_task.is_none() {
let (canceller, task) =
crate::utils::forward_event_loop(move |mut sender| async move {
let (tx, mut rx) = futures::channel::mpsc::channel(1);
let watchers = std::pin::pin!(async move {
futures::join!(
network_manager::watch(conn.clone(), tx.clone()),
network_manager::active_conns::watch(conn.clone(), tx.clone()),
network_manager::devices::watch(conn, true, tx)
)
network_manager::watch(conn, tx).await;
});
let forwarder = std::pin::pin!(async move {
@ -623,7 +620,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> {
.into()
}
fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_state(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
match NetworkManagerState::new(&conn).await {
Ok(state) => Message::UpdateState(state),
@ -632,7 +629,7 @@ fn update_state(conn: zbus::Connection) -> Task<crate::app::Message> {
})
}
fn update_devices(conn: zbus::Connection) -> Task<crate::app::Message> {
fn update_devices(conn: nmrs::NetworkManager) -> Task<crate::app::Message> {
cosmic::task::future(async move {
let filter =
|device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet);

View file

@ -1,7 +1,9 @@
use futures::{FutureExt, Stream, StreamExt, future::join_all};
use futures::future::join_all;
use futures::{FutureExt, Stream, StreamExt};
use jiff::{Span, SpanRelativeTo, SpanRound, ToSpan, Unit};
use upower_dbus::{BatteryState, BatteryType, DeviceProxy};
use zbus::{Connection, zvariant::ObjectPath};
use zbus::Connection;
use zbus::zvariant::ObjectPath;
mod ppdaemon;
mod s76powerdaemon;

View file

@ -3,12 +3,11 @@ mod backend;
use self::backend::{GetCurrentPowerProfile, SetPowerProfile};
use backend::{Battery, ConnectedDevice, PowerProfile};
use cosmic::Task;
use cosmic::iced::core::text::{Ellipsize, EllipsizeHeightLimit};
use cosmic::iced::widget::{column, row};
use cosmic::iced::{self, Alignment, Length, stream};
use cosmic::widget::{self, settings, space, text};
use cosmic::{Apply, surface};
use cosmic::{Apply, Task, surface};
use cosmic_config::{Config, CosmicConfigEntry};
use cosmic_idle_config::CosmicIdleConfig;
use cosmic_settings_page::{self as page, Section, section};

View file

@ -0,0 +1,536 @@
// Copyright 2026 Lionel DARNIS <lionel@wopr.io>
// SPDX-License-Identifier: GPL-3.0-only
//! CUPS backend for the printers page.
//!
//! Read-only operations (destinations, jobs) use the `cups_rs` bindings to
//! libcups. Mutating operations (add, remove, default, pause) go through the
//! CUPS command line tools, which authenticate transparently against the
//! local scheduler for users in the CUPS system group.
use tokio::process::Command;
pub use cups_rs::{JobStatus, PrinterState};
const TEST_PAGE: &str = "/usr/share/cups/data/testprint";
/// `printer-type` bit marking destinations that CUPS merely discovered on the
/// network (dnssd browsing). Those are not real queues: they cannot be removed
/// or configured and vanish on their own, so the page must not list them.
const CUPS_PRINTER_DISCOVERED: u32 = 0x1000000;
/// libcups keeps process-wide global state; serialize every FFI call.
static CUPS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn cups_lock() -> std::sync::MutexGuard<'static, ()> {
CUPS_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// A printer queue known to the CUPS scheduler.
#[derive(Clone, Debug)]
pub struct Printer {
pub name: String,
pub info: String,
pub make_and_model: Option<String>,
pub location: Option<String>,
pub device_uri: Option<String>,
pub state: PrinterState,
pub state_reasons: Vec<String>,
pub accepting_jobs: bool,
pub is_default: bool,
}
/// An active print job.
#[derive(Clone, Debug)]
pub struct Job {
pub id: i32,
pub title: String,
pub dest: String,
pub status: JobStatus,
}
/// A device that a printer queue can be created for.
#[derive(Clone, Debug)]
pub struct Device {
pub uri: String,
pub info: String,
}
/// A printer driver (PPD or driverless profile).
#[derive(Clone, Debug)]
pub struct Driver {
pub id: String,
pub description: String,
}
/// Read operations (blocking FFI; call from a blocking task).
pub fn printers_sync() -> Result<Vec<Printer>, String> {
let _guard = cups_lock();
let destinations = cups_rs::get_all_destinations().map_err(|err| err.to_string())?;
Ok(destinations
.iter()
.filter(|dest| {
!dest
.get_option("printer-type")
.and_then(|value| value.parse::<u32>().ok())
.is_some_and(|printer_type| printer_type & CUPS_PRINTER_DISCOVERED != 0)
})
.map(|dest| Printer {
name: dest.name.clone(),
info: dest
.info()
.filter(|info| !info.is_empty())
.cloned()
.unwrap_or_else(|| dest.name.clone()),
make_and_model: dest.make_and_model().cloned(),
location: dest.location().cloned(),
device_uri: dest.device_uri().cloned(),
state: dest.state(),
state_reasons: dest
.state_reasons()
.into_iter()
.filter(|reason| reason != "none")
.collect(),
accepting_jobs: dest.is_accepting_jobs(),
is_default: dest.is_default,
})
.collect())
}
pub fn jobs_sync() -> Result<Vec<Job>, String> {
let _guard = cups_lock();
let jobs = cups_rs::get_active_jobs(None).map_err(|err| err.to_string())?;
Ok(jobs
.into_iter()
.map(|job| Job {
id: job.id,
title: job.title,
dest: job.dest,
status: job.status,
})
.collect())
}
pub fn cancel_job_sync(job_id: i32) -> Result<(), String> {
let _guard = cups_lock();
cups_rs::job::cancel_job(job_id).map_err(|err| err.to_string())
}
pub async fn print_test_page(printer: &str) -> Result<(), String> {
// Submit through `lp` so CUPS sniffs the format itself: the test page is
// a cups-filters PDF banner template (#PDF-BANNER), not PostScript, and
// cups_rs only allows a hardcoded list of document formats.
run_cli("lp", &["-d", printer, TEST_PAGE]).await
}
/// Discovery and administration (CUPS command line tools).
pub async fn devices() -> Result<Vec<Device>, String> {
let output = run_cli_capture("lpinfo", &["-v"]).await?;
let mut devices: Vec<Device> = Vec::new();
for line in output.lines() {
let Some((_, uri)) = line.split_once(char::is_whitespace) else {
continue;
};
let uri = uri.trim();
if !uri.contains("://") {
continue;
}
let info = device_info(uri);
// One entry per physical device: the same printer is advertised over
// several schemes (dnssd, ipp, ipps…). Prefer the dnssd URI, which
// keeps working across IP changes.
if let Some(existing) = devices.iter_mut().find(|device| device.info == info) {
if uri.starts_with("dnssd://") && !existing.uri.starts_with("dnssd://") {
existing.uri = uri.to_string();
}
continue;
}
devices.push(Device {
info,
uri: uri.to_string(),
});
}
Ok(devices)
}
pub async fn drivers() -> Result<Vec<Driver>, String> {
let output = run_cli_capture("lpinfo", &["-m"]).await?;
let mut drivers: Vec<Driver> = output
.lines()
.filter_map(|line| {
let (id, description) = line.split_once(char::is_whitespace)?;
Some(Driver {
id: id.trim().to_string(),
description: description.trim().to_string(),
})
})
.collect();
// Offer the driverless IPP Everywhere profile first.
drivers.sort_by_key(|driver| driver.id != "everywhere");
Ok(drivers)
}
pub async fn add_printer(name: &str, uri: &str, driver: &str) -> Result<(), String> {
run_cli("lpadmin", &["-p", name, "-E", "-v", uri, "-m", driver]).await
}
pub async fn remove_printer(name: &str) -> Result<(), String> {
match run_cli("lpadmin", &["-x", name]).await {
Ok(()) => Ok(()),
// A queue that vanished between listing and removal is already gone;
// treat it as a successful removal instead of an error.
Err(err)
if err.contains("n'existe pas") || err.to_lowercase().contains("not exist") =>
{
Ok(())
}
Err(err) => Err(err),
}
}
/// Default job options supported by a queue, with their current values.
///
/// Values are taken from the queue's PPD view (`lpoptions -l`): `PageSize`,
/// `MediaType`, `ColorModel`, `cupsPrintQuality` and `Duplex`, which
/// `lpadmin -o` can persist.
#[derive(Clone, Debug, Default)]
pub struct PrinterOptions {
pub media: Vec<String>,
pub media_default: Option<String>,
pub media_types: Vec<String>,
pub media_type_default: Option<String>,
pub color_models: Vec<String>,
pub color_model_default: Option<String>,
pub qualities: Vec<String>,
pub quality_default: Option<String>,
pub sides: Vec<String>,
pub sides_default: Option<String>,
}
pub async fn printer_options(name: &str) -> Result<PrinterOptions, String> {
let output = run_cli_capture("lpoptions", &["-p", name, "-l"]).await?;
let mut options = PrinterOptions::default();
for line in output.lines() {
let Some((key, values)) = line.split_once(':') else {
continue;
};
let key = key.split('/').next().unwrap_or(key).trim();
let parsed: Vec<(String, bool)> = values
.split_whitespace()
.map(|value| {
(
value.trim_start_matches('*').to_string(),
value.starts_with('*'),
)
})
.collect();
let (list, default) = match key {
"PageSize" => (&mut options.media, &mut options.media_default),
"MediaType" => (&mut options.media_types, &mut options.media_type_default),
"ColorModel" => (&mut options.color_models, &mut options.color_model_default),
"cupsPrintQuality" => (&mut options.qualities, &mut options.quality_default),
"Duplex" => (&mut options.sides, &mut options.sides_default),
_ => continue,
};
for (value, selected) in parsed {
// Custom.WIDTHxHEIGHT placeholders are not selectable values.
if value.starts_with("Custom.") {
continue;
}
if selected {
*default = Some(value.clone());
}
list.push(value);
}
}
Ok(options)
}
/// Set a queue-wide default job option (PageSize, cupsPrintQuality, Duplex…).
pub async fn set_default_option(printer: &str, option: &str, value: &str) -> Result<(), String> {
run_cli(
"lpadmin",
&["-p", printer, "-o", &format!("{option}={value}")],
)
.await
}
/// A consumable reported by the printer through the IPP marker attributes.
#[derive(Clone, Debug)]
pub struct Marker {
pub name: String,
/// 0-100; negative values mean unknown.
pub level: i32,
/// "#RRGGBB" when the printer advertises a color.
pub color: Option<String>,
}
/// Query ink levels directly from the printer.
///
/// The scheduler does not relay marker levels for driverless queues, so the
/// printer's IPP endpoint is resolved (dnssd -> ippfind) and queried with
/// ipptool. Returns an empty list when the printer does not report markers.
pub async fn markers(device_uri: &str) -> Vec<Marker> {
let Some(endpoint) = resolve_endpoint(device_uri).await else {
return Vec::new();
};
let names = query_marker_attribute(&endpoint, "marker-names").await;
let raw_levels = query_marker_attribute(&endpoint, "marker-levels").await;
let colors = query_marker_attribute(&endpoint, "marker-colors").await;
let levels: Vec<i32> = raw_levels
.iter()
.filter_map(|value| value.trim().parse::<i32>().ok())
.collect();
if levels.is_empty() {
return Vec::new();
}
(0..levels.len())
.map(|index| Marker {
name: names
.get(index)
.cloned()
.unwrap_or_else(|| format!("marker {}", index + 1)),
level: levels[index],
color: colors.get(index).cloned().filter(|value| value.starts_with('#')),
})
.collect()
}
/// Resolve a device URI into a queryable IPP endpoint.
async fn resolve_endpoint(device_uri: &str) -> Option<String> {
if device_uri.starts_with("ipp://") || device_uri.starts_with("ipps://") {
return Some(device_uri.to_string());
}
let rest = device_uri.strip_prefix("dnssd://")?;
let name = rest.split(['/', '?']).next()?;
let name = percent_decode(name.split("._").next()?);
let output = run_cli_capture(
"timeout",
&["8", "ippfind", "_ipp._tcp", "_ipps._tcp", "-N", &name, "-p"],
)
.await
.ok()?;
let mut fallback = None;
for line in output.lines() {
let line = line.trim();
if line.starts_with("ipps://") {
return Some(line.to_string());
}
if fallback.is_none() && line.starts_with("ipp://") {
fallback = Some(line.to_string());
}
}
fallback
}
/// Run one ipptool Get-Printer-Attributes query and return the displayed
/// values of a single marker attribute.
async fn query_marker_attribute(endpoint: &str, attribute: &str) -> Vec<String> {
let test_file = std::env::temp_dir().join(format!("cosmic-settings-printers-{attribute}.test"));
let content = format!(
"{{\n\
\x20 NAME \"{attribute}\"\n\
\x20 OPERATION Get-Printer-Attributes\n\
\x20 GROUP operation-attributes-tag\n\
\x20 ATTR charset attributes-charset utf-8\n\
\x20 ATTR naturalLanguage attributes-natural-language en\n\
\x20 ATTR uri printer-uri $uri\n\
\x20 ATTR keyword requested-attributes {attribute}\n\
\x20 STATUS successful-ok\n\
\x20 DISPLAY {attribute}\n\
}}\n"
);
if tokio::fs::write(&test_file, content).await.is_err() {
return Vec::new();
}
let Some(file_path) = test_file.to_str() else {
return Vec::new();
};
let Ok(output) =
run_cli_capture("timeout", &["8", "ipptool", endpoint, file_path]).await
else {
return Vec::new();
};
parse_display_values(&output).unwrap_or_default()
}
/// Extract the value cell of a single-DISPLAY ipptool table: the line
/// following the dashes separator, split on commas.
fn parse_display_values(output: &str) -> Option<Vec<String>> {
let mut lines = output.lines();
while let Some(line) = lines.next() {
if !line.trim().is_empty() && line.trim().chars().all(|c| c == '-') {
let values = lines.next()?.trim();
return Some(values.split(',').map(|value| value.trim().to_string()).collect());
}
}
None
}
pub async fn set_default(name: &str) -> Result<(), String> {
run_cli("lpadmin", &["-d", name]).await
}
pub async fn set_enabled(name: &str, enabled: bool) -> Result<(), String> {
if enabled {
run_cli("cupsenable", &[name]).await
} else {
run_cli("cupsdisable", &[name]).await
}
}
/// Sanitize a display name into a valid CUPS queue name.
///
/// No deduplication: `lpadmin` modifies the queue when the name already
/// exists, which also promotes the temporary queues CUPS auto-creates for
/// discovered devices into permanent ones.
pub fn queue_name(raw: &str) -> String {
let mut sanitized = String::with_capacity(raw.len());
for c in raw.chars() {
if c.is_alphanumeric() {
sanitized.push(c);
} else if matches!(c, '[' | ']' | '(' | ')') {
// Brackets usually wrap a model identifier; drop them so
// "series [32C998]" becomes "series_32C998", not "series__32C998_".
continue;
} else {
sanitized.push('_');
}
}
let base = sanitized
.split('_')
.filter(|part| !part.is_empty())
.collect::<Vec<_>>()
.join("_");
if base.is_empty() {
"Printer".to_string()
} else {
base
}
}
async fn run_cli(program: &str, args: &[&str]) -> Result<(), String> {
let output = run_cli_capture(program, args).await?;
drop(output);
Ok(())
}
async fn run_cli_capture(program: &str, args: &[&str]) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.stdin(std::process::Stdio::null())
.output()
.await
.map_err(|err| format!("{program}: {err}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.success() {
return Ok(stdout);
}
// Ignore deprecation notices lpadmin prints on stderr for PPD drivers.
let message = stderr
.lines()
.filter(|line| !line.contains("deprecated"))
.collect::<Vec<_>>()
.join(" ");
Err(if message.trim().is_empty() {
format!("{program} {}", args.join(" "))
} else {
message
})
}
fn device_info(uri: &str) -> String {
if let Some(rest) = uri.strip_prefix("dnssd://") {
let name = rest.split(['/', '?']).next().unwrap_or(rest);
let name = name.split("._").next().unwrap_or(name);
let decoded = percent_decode(name);
if !decoded.is_empty() {
return decoded;
}
}
if let Some(rest) = uri.strip_prefix("usb://") {
let path = rest.split('?').next().unwrap_or(rest);
let decoded = percent_decode(path).replace('/', " ");
if !decoded.trim().is_empty() {
return decoded;
}
}
if uri.starts_with("cups-pdf") {
return "PDF".to_string();
}
// ipp/ipps/socket/lpd/http(s) URIs: keep the host label and drop the
// dnssd-style service suffix ("Name._ipp._tcp.local" -> "Name").
if let Some((_, rest)) = uri.split_once("://")
&& let Some(host) = rest.split(['/', '?']).next()
{
let decoded = percent_decode(host);
let label = decoded.split("._").next().unwrap_or(&decoded);
let label = label.trim_end_matches(".local");
if !label.is_empty() && !label.chars().all(|c| c.is_ascii_digit() || c == '.' || c == ':') {
return label.to_string();
}
}
percent_decode(uri)
}
fn percent_decode(input: &str) -> String {
let bytes = input.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' && index + 2 < bytes.len() {
let hex = &input[index + 1..index + 3];
if let Ok(value) = u8::from_str_radix(hex, 16) {
decoded.push(value);
index += 3;
continue;
}
}
decoded.push(bytes[index]);
index += 1;
}
String::from_utf8_lossy(&decoded).into_owned()
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,465 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use super::model;
use cosmic::desktop::{
DesktopEntryCache, DesktopLookupContext, DesktopResolveOptions, resolve_desktop_entry,
};
use cosmic::iced::futures;
use cosmic::iced::{self, Alignment, Length, window};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::{Apply, Element, surface, widget};
use cosmic_config::{Config, ConfigGet};
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use cosmic_settings_page::{self as page, Section, section};
use futures::executor::block_on;
use intmap::IntMap;
use slotmap::SlotMap;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
const AUDIO_CONFIG: &str = "com.system76.CosmicAudio";
const AMPLIFICATION_SINK: &str = "amplification_sink";
#[derive(Clone, Debug)]
pub enum Message {
Model(cosmic_settings_sound::Message),
SetBalance(u32, f32),
SetMute(u32, bool),
SetSink(u32, usize),
SetVolume(u32, u32),
Surface(surface::Action),
ToggleExpanded(u32),
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::SoundApplications(message)
}
}
impl From<Message> for crate::Message {
fn from(message: Message) -> Self {
crate::Message::PageMessage(message.into())
}
}
#[derive(Default)]
pub struct Page {
entity: page::Entity,
model: model::Model,
client: Option<Rc<RefCell<audio_client::Client>>>,
desktop_entries: DesktopEntryCache,
playback_icons: IntMap<u32, String>,
amplification_sink: bool,
expanded: Option<u32>,
}
impl page::AutoBind<crate::pages::Message> for Page {}
impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> cosmic::Task<crate::pages::Message> {
self.amplification_sink = Config::new(AUDIO_CONFIG, 1)
.ok()
.and_then(|config| config.get::<bool>(AMPLIFICATION_SINK).ok())
.unwrap_or(true);
cosmic::Task::none()
}
fn info(&self) -> page::Info {
page::Info::new("sound-applications", "preferences-sound-symbolic")
.title(fl!("sound-applications"))
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(view())])
}
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
*self = Page {
entity: self.entity,
..Page::default()
};
cosmic::Task::none()
}
fn subscription(&self, _core: &cosmic::Core) -> iced::Subscription<crate::pages::Message> {
iced::Subscription::run(|| {
iced::stream::channel(
1,
move |emitter: futures::channel::mpsc::Sender<crate::pages::Message>| async move {
cosmic_settings_sound::subscribe(emitter, |m| Message::Model(m).into()).await
},
)
})
}
}
impl Page {
fn resolve_playback_icon(&mut self, node_id: u32, info: &audio_client::PlaybackInfo) {
let Some(candidate) = [
info.application_id.as_deref(),
info.icon_name.as_deref(),
info.application_name.as_deref(),
]
.into_iter()
.flatten()
.find(|value| !value.is_empty()) else {
return;
};
let mut context = DesktopLookupContext::new(candidate);
if let Some(icon_name) = info.icon_name.as_deref().filter(|value| !value.is_empty()) {
context = context.with_identifier(icon_name);
}
if let Some(application_name) = info
.application_name
.as_deref()
.filter(|value| !value.is_empty())
{
context = context.with_title(application_name);
}
let entry = resolve_desktop_entry(
&mut self.desktop_entries,
&context,
&DesktopResolveOptions::default(),
);
let entry_icon = entry.icon();
let icon_name = entry_icon
.filter(|value| !value.is_empty())
.or(info.icon_name.as_deref())
.or(info.application_id.as_deref())
.unwrap_or("application-default");
self.playback_icons.insert(node_id, icon_name.to_owned());
}
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::app::Message> {
match message {
Message::Model(cosmic_settings_sound::Message::Subscription(message)) => {
match &message {
audio_client::Event::Playback(node_id, info) => {
self.resolve_playback_icon(*node_id, info);
}
audio_client::Event::RemoveNode(node_id) => {
self.playback_icons.remove(*node_id);
}
_ => {}
}
self.model.update(message);
}
Message::Model(cosmic_settings_sound::Message::Client(client)) => {
if let Some(client) = Arc::into_inner(client) {
self.client = Some(Rc::new(RefCell::new(client)));
self.model = model::Model::default();
self.playback_icons = IntMap::new();
}
}
Message::SetMute(node_id, mute) => {
self.model
.update(audio_client::Event::NodeMute(node_id, mute));
if let Some(client) = &self.client {
match block_on(client.borrow_mut().conn.set_node_mute(node_id, mute)) {
Ok(Ok(_)) => {}
Ok(Err(why)) => {
tracing::error!(?why, node_id, mute, "daemon rejected playback mute");
}
Err(why) => {
tracing::error!(?why, node_id, mute, "failed to set playback mute");
}
}
} else {
tracing::warn!(node_id, mute, "audio client is not connected");
}
}
Message::SetBalance(playback_id, balance) => {
if let Some(playback) = self
.model
.playback
.iter_mut()
.find(|playback| playback.id == playback_id)
{
playback.balance = Some(balance);
}
if let Some(client) = &self.client {
match block_on(
client
.borrow_mut()
.conn
.set_node_volume_balance(playback_id, Some(balance)),
) {
Ok(Ok(volume)) => {
self.model.update(audio_client::Event::NodeVolume(
playback_id,
volume.volume,
volume.balance,
));
}
Ok(Err(why)) => {
tracing::error!(
?why,
playback_id,
balance,
"daemon rejected playback balance"
);
}
Err(why) => {
tracing::error!(
?why,
playback_id,
balance,
"failed to set playback balance"
);
}
}
} else {
tracing::warn!(playback_id, balance, "audio client is not connected");
}
}
Message::SetSink(playback_id, sorted_pos) => {
let Some(&sink_pos) = self.model.sinks.sorted_index.get(sorted_pos) else {
return cosmic::Task::none();
};
let Some(&sink_id) = self.model.sinks.id.get(sink_pos as usize) else {
return cosmic::Task::none();
};
if let Some(client) = &self.client {
match block_on(
client
.borrow_mut()
.conn
.set_playback_sink(playback_id, sink_id),
) {
Ok(Ok(())) => {
if let Some(playback) = self
.model
.playback
.iter_mut()
.find(|playback| playback.id == playback_id)
{
playback.sink_id = Some(sink_id);
}
}
Ok(Err(why)) => {
tracing::error!(
?why,
playback_id,
sink_id,
"daemon rejected playback output"
);
}
Err(why) => {
tracing::error!(
?why,
playback_id,
sink_id,
"failed to set playback output"
);
}
}
} else {
tracing::warn!(playback_id, sink_id, "audio client is not connected");
}
}
Message::SetVolume(node_id, volume) => {
if let Some(playback) = self
.model
.playback
.iter_mut()
.find(|playback| playback.id == node_id)
{
playback.volume = volume;
}
if let Some(client) = &self.client {
match block_on(client.borrow_mut().conn.set_node_volume(node_id, volume)) {
Ok(Ok(_)) => {}
Ok(Err(why)) => {
tracing::error!(
?why,
node_id,
volume,
"daemon rejected playback volume"
);
}
Err(why) => {
tracing::error!(?why, node_id, volume, "failed to set playback volume");
}
}
} else {
tracing::warn!(node_id, volume, "audio client is not connected");
}
}
Message::Surface(action) => {
return cosmic::task::message(crate::app::Message::Surface(action));
}
Message::ToggleExpanded(node_id) => {
self.expanded = if self.expanded == Some(node_id) {
None
} else {
Some(node_id)
};
}
}
cosmic::Task::none()
}
}
pub fn view() -> Section<crate::pages::Message> {
Section::default().view::<Page>(move |_, page, _section| {
if page.model.playback.is_empty() {
return widget::settings::section()
.add(
widget::text::body(fl!("sound-applications", "none"))
.width(Length::Fill)
.align_x(Alignment::Center),
)
.into();
}
let mut applications = widget::settings::section();
for playback in page.model.playback.iter().cloned() {
let node_id = playback.id;
let mute = playback.mute;
let volume = playback.volume;
let expanded = page.expanded == Some(node_id);
let icon_name = page
.playback_icons
.get(node_id)
.map(String::as_str)
.unwrap_or("application-default");
let slider = if page.amplification_sink {
widget::slider(0..=150, volume, move |volume| {
Message::SetVolume(node_id, volume)
})
.breakpoints(&[100])
} else {
widget::slider(0..=100, volume, move |volume| {
Message::SetVolume(node_id, volume)
})
}
.width(Length::Fill)
.apply(widget::container)
.max_width(250.);
let volume_controls = widget::row::with_capacity(5)
.align_y(Alignment::Center)
.push(
widget::button::icon(widget::icon::from_name(if mute {
"audio-volume-muted-symbolic"
} else {
"audio-volume-high-symbolic"
}))
.on_press(Message::SetMute(node_id, !mute)),
)
.push(
widget::text::body(volume.to_string())
.width(Length::Fixed(22.))
.align_x(Alignment::Center),
)
.push(horizontal_space().width(8.))
.push(slider)
.push(
widget::button::icon(widget::icon::from_name(if expanded {
"go-up-symbolic"
} else {
"go-down-symbolic"
}))
.on_press(Message::ToggleExpanded(node_id)),
)
.apply(Element::from)
.map(crate::pages::Message::from);
applications = applications.add(
widget::settings::item::builder(playback.name().to_owned())
.icon(widget::icon::from_name(icon_name).size(16))
.flex_control(volume_controls)
.align_items(Alignment::Center),
);
if !expanded {
continue;
}
let selected_sink =
playback
.selected_sink(page.model.default_sink)
.and_then(|sink_id| {
let sink_pos = page.model.sinks.id.iter().position(|id| *id == sink_id)?;
page.model
.sinks
.sorted_index
.iter()
.position(|pos| usize::from(*pos) == sink_pos)
});
let output = widget::dropdown::popup_dropdown(
&page.model.sinks.sorted_display,
selected_sink,
move |pos| Message::SetSink(node_id, pos),
window::Id::RESERVED,
Message::Surface,
crate::Message::from,
)
.apply(Element::from)
.map(crate::pages::Message::from);
let balance = widget::row::with_capacity(5)
.align_y(Alignment::Center)
.push(
widget::column::with_capacity(2)
.align_x(Alignment::Center)
.push(widget::text::body(fl!("sound-output", "left")))
.push(horizontal_space().width(22.)),
)
.push(horizontal_space().width(8.))
.push(
widget::slider(0.0..=2.0, playback.balance.unwrap_or(1.0), move |balance| {
Message::SetBalance(node_id, balance)
})
.step(0.01)
.breakpoints(&[1.0]),
)
.push(horizontal_space().width(8.))
.push(
widget::column::with_capacity(2)
.align_x(Alignment::Center)
.push(widget::text::body(fl!("sound-output", "right")))
.push(horizontal_space().width(22.)),
)
.apply(Element::from)
.map(crate::pages::Message::from);
applications = applications
.add(widget::settings::item(
fl!("sound-output", "device"),
output,
))
.add(widget::settings::item(
fl!("sound-output", "balance"),
balance,
));
}
applications.into()
})
}

View file

@ -1,13 +1,26 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{Apply, widget};
use super::model;
use cosmic::iced::futures;
use cosmic::{Apply, iced, widget};
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use cosmic_settings_page::{self as page, Section, section};
use cosmic_settings_sound_subscription::{self as subscription};
use futures::executor::block_on;
use slotmap::SlotMap;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub enum Message {}
pub enum Message {
/// Update for the model.
Model(cosmic_settings_sound::Message),
/// Set the profile of a sound device.
SetProfile(u32, u32),
/// Surface Action
Surface(cosmic::surface::Action),
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
@ -24,6 +37,8 @@ impl From<Message> for crate::Message {
#[derive(Default)]
pub struct Page {
entity: page::Entity,
model: model::Model,
client: Option<Rc<RefCell<audio_client::Client>>>,
}
impl page::AutoBind<crate::pages::Message> for Page {}
@ -41,55 +56,83 @@ impl page::Page<crate::pages::Message> for Page {
Some(vec![sections.insert(view())])
}
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
cosmic::Task::done(crate::pages::Message::Sound(super::Message::Reload))
}
fn set_id(&mut self, entity: cosmic_settings_page::Entity) {
self.entity = entity;
}
fn subscription(
&self,
_core: &cosmic::Core,
) -> cosmic::iced::Subscription<crate::pages::Message> {
cosmic::iced::Subscription::run(subscription::watch)
.map(|message| super::Message::Subscription(message).into())
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
*self = Page {
entity: self.entity,
..Page::default()
};
cosmic::Task::none()
}
fn subscription(&self, _core: &cosmic::Core) -> iced::Subscription<crate::pages::Message> {
iced::Subscription::run(|| {
iced::stream::channel(
1,
move |emitter: futures::channel::mpsc::Sender<crate::pages::Message>| async move {
cosmic_settings_sound::subscribe(emitter, |m| Message::Model(m).into()).await
},
)
})
}
}
impl Page {
pub fn update(&mut self, _message: Message) -> cosmic::Task<crate::app::Message> {
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::app::Message> {
match message {
Message::Model(cosmic_settings_sound::Message::Subscription(message)) => {
self.model.update(message);
}
Message::Model(cosmic_settings_sound::Message::Client(client)) => {
if let Some(client) = Arc::into_inner(client) {
self.client = Some(Rc::new(RefCell::new(client)));
self.model = model::Model {
text: model::Text {
hd_audio: fl!("sound-hd-audio"),
usb_audio: fl!("sound-usb-audio"),
},
..model::Model::default()
};
}
}
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
Message::SetProfile(id, index) => {
if let Some(client) = self.client.clone() {
block_on(async move {
_ = client.borrow_mut().conn.set_profile(id, index, true).await;
});
}
}
}
cosmic::Task::none()
}
}
pub fn view() -> Section<crate::pages::Message> {
Section::default().view::<Page>(move |binder, _page, _section| {
let sound_page_id = binder.find_page_by_id("sound").unwrap().0;
let sound_page = binder.page[sound_page_id]
.downcast_ref::<super::Page>()
.unwrap();
let devices = sound_page
.model
.device_profile_dropdowns
.iter()
.cloned()
.map(|(device_id, name, active_profile, indexes, descriptions)| {
Section::default().view::<Page>(move |_, page, _section| {
let devices = page.model.device_profile_dropdowns.iter().cloned().map(
|(device_id, name, active_profile, indexes, descriptions)| {
let dropdown = widget::dropdown::popup_dropdown(
descriptions,
active_profile,
move |id| super::Message::SetProfile(device_id, indexes[id]),
move |id| Message::SetProfile(device_id, indexes[id]),
cosmic::iced::window::Id::RESERVED,
super::Message::Surface,
Message::Surface,
crate::Message::from,
)
.apply(cosmic::Element::from)
.map(crate::pages::Message::from);
widget::settings::item::builder(name).control(dropdown)
});
},
);
widget::settings::section().extend(devices).into()
})

View file

@ -1,18 +1,22 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
pub mod applications;
pub mod device_profiles;
use cosmic::{
Apply, Element, Task,
iced::{Alignment, Length, window},
surface,
widget::{self, settings, space::horizontal as horizontal_space},
};
use cosmic::iced::{self, Alignment, Length, window};
use cosmic::widget::space::horizontal as horizontal_space;
use cosmic::widget::{self, settings};
use cosmic::{Apply, Element, Task, surface};
use cosmic_config::{Config, ConfigGet, ConfigSet};
use cosmic_settings_audio_client::{self as audio_client, Client, CosmicAudioProxy};
use cosmic_settings_page::{self as page, Section, section};
use cosmic_settings_sound_subscription as subscription;
use cosmic_settings_sound::model;
use futures::executor::block_on;
use slotmap::SlotMap;
use std::sync::atomic::AtomicU32;
use std::sync::{Arc, atomic};
use std::time::Duration;
const AUDIO_CONFIG: &str = "com.system76.CosmicAudio";
const AMPLIFICATION_SINK: &str = "amplification_sink";
@ -20,22 +24,20 @@ const AMPLIFICATION_SOURCE: &str = "amplification_source";
#[derive(Clone, Debug)]
pub enum Message {
/// Reload the model
Reload,
/// Updates for the model.
Model(cosmic_settings_sound::Message),
/// Reattach the client
ReattachClient(Arc<Client>),
/// Change the default output.
SetDefaultSink(usize),
/// Change the default input output.
SetDefaultSource(usize),
/// Set the profile of a sound device.
SetProfile(u32, u32),
/// Change the balance of the active sink.
SetSinkBalance(u32),
SetSinkBalance(f32),
/// Request to change the default output volume.
SetSinkVolume(u32),
/// Request to change the input volume.
SetSourceVolume(u32),
/// Messages handled by the sound module in cosmic-settings-subscriptions
Subscription(subscription::Message),
/// Surface Action
Surface(surface::Action),
/// Toggle the mute status of the output.
@ -60,32 +62,38 @@ impl From<Message> for crate::Message {
}
}
impl From<subscription::Message> for Message {
fn from(val: subscription::Message) -> Self {
Message::Subscription(val)
}
}
pub struct Page {
entity: page::Entity,
applications: page::Entity,
device_profiles: page::Entity,
pub(self) model: subscription::Model,
client: Option<audio_client::Client>,
model: model::Model,
sound_config: Option<Config>,
applied_sink_volume: Arc<AtomicU32>,
applied_sink_volume_balance: Arc<AtomicU32>,
applied_source_volume: Arc<AtomicU32>,
amplification_sink: bool,
amplification_source: bool,
}
impl Default for Page {
fn default() -> Self {
let mut model = subscription::Model::default();
model.unplugged_text = fl!("sound-device-port-unplugged");
model.hd_audio_text = fl!("sound-hd-audio");
model.usb_audio_text = fl!("sound-usb-audio");
Self {
entity: page::Entity::default(),
applications: page::Entity::default(),
device_profiles: page::Entity::default(),
model,
client: None,
model: model::Model {
text: model::Text {
hd_audio: fl!("sound-hd-audio"),
usb_audio: fl!("sound-usb-audio"),
},
..model::Model::default()
},
sound_config: None,
applied_sink_volume: Arc::new(AtomicU32::new(0)),
applied_sink_volume_balance: Arc::new(AtomicU32::new(0)),
applied_source_volume: Arc::new(AtomicU32::new(0)),
amplification_sink: false,
amplification_source: false,
}
@ -117,6 +125,7 @@ impl page::Page<crate::pages::Message> for Page {
Some(vec![
sections.insert(output()),
sections.insert(input()),
sections.insert(applications()),
sections.insert(device_profiles()),
])
}
@ -131,17 +140,21 @@ impl page::Page<crate::pages::Message> for Page {
self.entity = entity;
}
fn subscription(
&self,
_core: &cosmic::Core,
) -> cosmic::iced::Subscription<crate::pages::Message> {
cosmic::iced::Subscription::run(subscription::watch)
.map(|message| Message::Subscription(message).into())
fn subscription(&self, _core: &cosmic::Core) -> iced::Subscription<crate::pages::Message> {
iced::Subscription::run(|| {
iced::stream::channel(
1,
move |emitter: futures::channel::mpsc::Sender<crate::pages::Message>| async move {
cosmic_settings_sound::subscribe(emitter, |m| Message::Model(m).into()).await
},
)
})
}
fn on_leave(&mut self) -> Task<crate::pages::Message> {
*self = Page {
entity: self.entity,
applications: self.applications,
device_profiles: self.device_profiles,
..Page::default()
};
@ -155,61 +168,136 @@ impl page::AutoBind<crate::pages::Message> for Page {
mut page: page::Insert<crate::pages::Message>,
) -> page::Insert<crate::pages::Message> {
let id = page.sub_page_with_id::<device_profiles::Page>();
let applications_id = page.sub_page_with_id::<applications::Page>();
let model = page.model.page_mut::<Page>().unwrap();
model.device_profiles = id;
model.applications = applications_id;
page
}
}
impl Page {
pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
tracing::debug!(target: "sound", ?message, "update");
match message {
Message::Surface(a) => return cosmic::task::message(crate::app::Message::Surface(a)),
Message::Subscription(message) => {
return self
.model
.update(message)
.map(|message| Message::Subscription(message).into());
Message::Model(cosmic_settings_sound::Message::Subscription(message)) => {
self.model.update(message);
}
Message::SetSinkBalance(balance) => {
return self
.model
.set_sink_balance(balance)
.map(|message| Message::Subscription(message).into());
Message::Model(cosmic_settings_sound::Message::Client(client)) => {
if let Some(client) = Arc::into_inner(client) {
self.client = Some(client);
self.model = model::Model {
text: model::Text {
hd_audio: fl!("sound-hd-audio"),
usb_audio: fl!("sound-usb-audio"),
},
..model::Model::default()
};
}
}
Message::ReattachClient(client) => {
if let Some(client) = Arc::into_inner(client) {
self.client = Some(client);
}
}
Message::SetDefaultSink(pos) => {
return self
.model
.set_default_sink(pos)
.map(|message| Message::Subscription(message).into());
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.client.as_mut()
{
block_on(async {
_ = client.conn.set_default(node_id, true).await;
});
}
}
Message::SetDefaultSource(pos) => {
return self
.model
.set_default_source(pos)
.map(|message| Message::Subscription(message).into());
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.client.as_mut()
{
block_on(async {
_ = client.conn.set_default(node_id, true).await;
});
}
}
Message::ToggleSinkMute => self.model.toggle_sink_mute(),
Message::ToggleSinkMute => {
if let Some(ref mut client) = self.client {
block_on(async {
_ = client.conn.sink_mute_toggle().await;
});
}
}
Message::ToggleSourceMute => self.model.toggle_source_mute(),
Message::ToggleSourceMute => {
if let Some(ref mut client) = self.client {
block_on(async {
_ = client.conn.source_mute_toggle().await;
});
}
}
Message::SetSinkVolume(volume) => {
return self
.model
.set_sink_volume(volume)
.map(|message| Message::Subscription(message).into());
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.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()
});
}
}
Message::SetSourceVolume(volume) => {
return self
.model
.set_source_volume(volume)
.map(|message| Message::Subscription(message).into());
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.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_source_volume(volume.load(atomic::Ordering::Relaxed))
.await;
Message::ReattachClient(Arc::new(client)).into()
});
}
}
Message::SetSinkBalance(balance) => {
self.applied_sink_volume_balance
.store((balance * 1000.0) as u32, atomic::Ordering::Relaxed);
self.model.active_sink.balance = Some(balance);
if let Some((mut client, sink_id)) = self.client.take().zip(self.model.default_sink)
{
let balance = Arc::clone(&self.applied_sink_volume_balance);
return cosmic::Task::future(async move {
tokio::time::sleep(Duration::from_millis(128)).await;
_ = client
.conn
.set_node_volume_balance(
sink_id,
Some(balance.load(atomic::Ordering::Relaxed) as f32 / 1000.0),
)
.await;
Message::ReattachClient(Arc::new(client)).into()
});
}
}
Message::ToggleOverAmplificationSink(enabled) => {
@ -231,18 +319,6 @@ impl Page {
tracing::error!(?why, "Failed to save over amplification setting");
}
}
Message::SetProfile(object_id, index) => {
self.model.set_profile(object_id, index, true);
}
Message::Reload => {
let mut model = subscription::Model::default();
model.hd_audio_text = std::mem::take(&mut self.model.hd_audio_text);
model.unplugged_text = std::mem::take(&mut self.model.unplugged_text);
model.usb_audio_text = std::mem::take(&mut self.model.usb_audio_text);
self.model = model;
}
}
Task::none()
@ -262,17 +338,17 @@ fn input() -> Section<crate::pages::Message> {
.title(fl!("sound-input"))
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
if page.model.sources().is_empty() {
if page.model.sources.id.is_empty() {
return widget::space().into();
}
let slider = if page.amplification_source {
widget::slider(0..=150, page.model.source_volume, |change| {
widget::slider(0..=150, page.model.active_source.volume, |change| {
Message::SetSourceVolume(change).into()
})
.breakpoints(&[100])
} else {
widget::slider(0..=100, page.model.source_volume, |change| {
widget::slider(0..=100, page.model.active_source.volume, |change| {
Message::SetSourceVolume(change).into()
})
}
@ -283,23 +359,25 @@ fn input() -> Section<crate::pages::Message> {
let volume_control = widget::row::with_capacity(4)
.align_y(Alignment::Center)
.push(
widget::button::icon(widget::icon::from_name(if page.model.source_mute {
"microphone-sensitivity-muted-symbolic"
} else {
"audio-input-microphone-symbolic"
}))
widget::button::icon(widget::icon::from_name(
if page.model.active_source.mute {
"microphone-sensitivity-muted-symbolic"
} else {
"audio-input-microphone-symbolic"
},
))
.on_press(Message::ToggleSourceMute.into()),
)
.push(
widget::text::body(&page.model.source_volume_text)
widget::text::body(&page.model.active_source.volume_text)
.width(Length::Fixed(22.0))
.align_x(Alignment::Center),
)
.push(horizontal_space().width(8.))
.push(slider);
let devices = widget::dropdown::popup_dropdown(
page.model.sources(),
Some(page.model.active_source().unwrap_or(0)),
&page.model.sources.sorted_display,
page.model.sources.active(),
Message::SetDefaultSource,
window::Id::RESERVED,
Message::Surface,
@ -346,12 +424,12 @@ fn output() -> Section<crate::pages::Message> {
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let slider = if page.amplification_sink {
widget::slider(0..=150, page.model.sink_volume, |change| {
widget::slider(0..=150, page.model.active_sink.volume, |change| {
Message::SetSinkVolume(change).into()
})
.breakpoints(&[100])
} else {
widget::slider(0..=100, page.model.sink_volume, |change| {
widget::slider(0..=100, page.model.active_sink.volume, |change| {
Message::SetSinkVolume(change).into()
})
}
@ -362,7 +440,7 @@ fn output() -> Section<crate::pages::Message> {
let volume_control = widget::row::with_capacity(4)
.align_y(Alignment::Center)
.push(
widget::button::icon(if page.model.sink_mute {
widget::button::icon(if page.model.active_sink.mute {
widget::icon::from_name("audio-volume-muted-symbolic")
} else {
widget::icon::from_name("audio-volume-high-symbolic")
@ -370,7 +448,7 @@ fn output() -> Section<crate::pages::Message> {
.on_press(Message::ToggleSinkMute.into()),
)
.push(
widget::text::body(&page.model.sink_volume_text)
widget::text::body(&page.model.active_sink.volume_text)
.width(Length::Fixed(22.0))
.align_x(Alignment::Center),
)
@ -378,8 +456,8 @@ fn output() -> Section<crate::pages::Message> {
.push(slider);
let devices = widget::dropdown::popup_dropdown(
page.model.sinks(),
Some(page.model.active_sink().unwrap_or(0)),
&page.model.sinks.sorted_display,
page.model.sinks.active(),
Message::SetDefaultSink,
window::Id::RESERVED,
Message::Surface,
@ -412,12 +490,12 @@ fn output() -> Section<crate::pages::Message> {
.push(horizontal_space().width(8.))
.push(
widget::slider(
0..=200,
(page.model.sink_balance.unwrap_or(1.0).max(0.) * 100.).round()
as u32,
0.0..=2.0,
page.model.active_sink.balance.unwrap_or(1.0),
|change| Message::SetSinkBalance(change).into(),
)
.breakpoints(&[100]),
.step(0.01)
.breakpoints(&[1.0]),
)
.push(horizontal_space().width(8.))
.push(
@ -453,24 +531,31 @@ fn device_profiles() -> Section<crate::pages::Message> {
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
let button = widget::row::with_children(vec![
horizontal_space().into(),
widget::icon::from_name("go-next-symbolic").size(16).into(),
]);
let device_profiles = settings::item::builder(&*descriptions[button_txt])
.control(button)
.spacing(16)
.apply(widget::container)
.width(Length::Fill)
.class(cosmic::theme::Container::List)
.apply(widget::button::custom)
.width(Length::Fill)
.class(cosmic::theme::Button::Transparent)
.on_press(crate::pages::Message::Page(page.device_profiles))
.width(Length::Fill);
settings::section()
.add(crate::widget::go_next_item(
&descriptions[button_txt],
crate::pages::Message::Page(page.device_profiles),
))
.into()
})
}
settings::section().add(device_profiles).into()
/// A section for opening the application volume controls sub-page.
fn applications() -> Section<crate::pages::Message> {
crate::slab!(descriptions {
button_txt = fl!("sound-applications");
});
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
settings::section()
.add(crate::widget::go_next_item(
&section.descriptions[button_txt],
crate::pages::Message::Page(page.applications),
))
.into()
})
}
@ -489,21 +574,3 @@ fn device_profiles() -> Section<crate::pages::Message> {
// .into()
// })
// }
// fn applications() -> Section<crate::pages::Message> {
// let mut descriptions = Slab::new();
// let applications = descriptions.insert(fl!("sound-applications", "desc"));
// Section::default()
// .title(fl!("sound-applications"))
// .descriptions(descriptions)
// .view::<Page>(move |_binder, _page, section| {
// settings::section().title(&section.title)
// .add(settings::item(
// &*section.descriptions[applications],
// text::body("TODO"),
// ))
// .into()
// })
// }

View file

@ -66,7 +66,8 @@ impl page::Page<crate::pages::Message> for Page {
fn on_enter(&mut self) -> Task<crate::pages::Message> {
let (task, handle) = Task::future(async move {
crate::pages::Message::About(Message::Info(Box::new(Info::load())))
let info = Info::load().await;
crate::pages::Message::About(Message::Info(Box::new(info)))
})
.abortable();

View file

@ -3,7 +3,9 @@
#[cfg(feature = "wgpu")]
use cosmic::iced::wgpu;
use std::{collections::HashMap, collections::HashSet, ffi::OsStr, process::Command};
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::process::Command;
#[must_use]
#[derive(Clone, Debug, Default)]
@ -29,7 +31,7 @@ impl Info {
}
#[cfg(feature = "wgpu")]
fn wgpu_graphics() -> Vec<String> {
async fn wgpu_graphics() -> Vec<String> {
let mut graphics = Vec::new();
// Use wgpu to enumerate GPUs. Works cross-platform and doesn't require external tools
@ -43,7 +45,7 @@ impl Info {
let mut seen_devices = HashSet::new();
let mut seen_names = HashSet::new();
for adapter in adapters {
for adapter in adapters.await {
let adapter_info = adapter.get_info();
if adapter_info.device_type == wgpu::DeviceType::Cpu {
@ -140,7 +142,7 @@ impl Info {
graphics
}
pub fn load() -> Info {
pub async fn load() -> Info {
let mut info = Info {
os_architecture: architecture(),
kernel_version: kernel_version(),
@ -192,7 +194,7 @@ impl Info {
#[cfg(feature = "wgpu")]
{
info.graphics = Self::wgpu_graphics();
info.graphics = Self::wgpu_graphics().await;
}
info

View file

@ -4,25 +4,22 @@
mod getent;
use crate::pages;
use cosmic::{
Apply, Element,
dialog::file_chooser,
iced::{Alignment, Length},
widget::{self, column, icon, list, row, settings, space::horizontal, text},
};
use cosmic::dialog::file_chooser;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::space::horizontal;
use cosmic::widget::{self, column, icon, list, row, settings, text};
use cosmic::{Apply, Element};
use cosmic_settings_page::{self as page, Section, section};
use image::GenericImageView;
use pwhash::{bcrypt, md5_crypt, sha256_crypt, sha512_crypt};
use regex::Regex;
use slotmap::SlotMap;
use std::{
collections::HashMap,
fs::File,
future::Future,
io::{BufRead, BufReader},
path::{Path, PathBuf},
sync::Arc,
};
use std::collections::HashMap;
use std::fs::File;
use std::future::Future;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use url::Url;
use zbus_polkit::policykit1::CheckAuthorizationFlags;

View file

@ -2,24 +2,19 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::widget::selection_context_item;
use cosmic::{
Apply, Element, Task,
app::ContextDrawer,
cosmic_config::{self, ConfigGet, ConfigSet},
iced::core::text::Wrapping,
surface,
widget::{self, dropdown, list, settings},
};
use cosmic::app::ContextDrawer;
use cosmic::cosmic_config::{self, ConfigGet, ConfigSet};
use cosmic::iced::core::text::Wrapping;
use cosmic::widget::{self, dropdown, settings};
use cosmic::{Apply, Element, Task, surface};
use cosmic_settings_page::{self as page, Section, section};
use icu::{
calendar::{Gregorian, types::Weekday, week},
datetime::{
DateTimeFormatter, DateTimeFormatterPreferences, fieldsets,
input::{Date, DateTime, Time},
options::TimePrecision,
},
locale::{Locale, preferences::extensions::unicode::keywords::HourCycle},
};
use icu::calendar::types::Weekday;
use icu::calendar::{Gregorian, week};
use icu::datetime::input::{Date, DateTime, Time};
use icu::datetime::options::TimePrecision;
use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences, fieldsets};
use icu::locale::Locale;
use icu::locale::preferences::extensions::unicode::keywords::HourCycle;
use slotmap::{Key, SlotMap};
pub use timedate_zbus::TimeDateProxy;
use tracing::error;

View file

@ -7,22 +7,20 @@ use std::sync::Arc;
use crate::widget::selection_context_item;
use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, button, list};
use cosmic::widget::{self, button};
use cosmic::{Apply, Element};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use cosmic_settings_page::{self as page, Section, section};
use eyre::Context;
use icu::{
calendar::{types::Weekday, week},
datetime::{
DateTimeFormatter, DateTimeFormatterPreferences, fieldsets,
input::{Date, DateTime, Time},
},
decimal::{DecimalFormatter, input::Decimal},
locale::Locale,
};
use icu::calendar::types::Weekday;
use icu::calendar::week;
use icu::datetime::input::{Date, DateTime, Time};
use icu::datetime::{DateTimeFormatter, DateTimeFormatterPreferences, fieldsets};
use icu::decimal::DecimalFormatter;
use icu::decimal::input::Decimal;
use icu::locale::Locale;
use locales_rs as locale;
use regex::Regex;
use slotmap::{DefaultKey, SlotMap};
static GNOME_LANGUAGE_SELECTOR: &str = "gnome-language-selector";
@ -242,9 +240,7 @@ impl Page {
let region = region.lang_code.clone();
return cosmic::task::future(async move {
if let Ok(exit_status) = set_locale(lang, region.clone()).await
&& exit_status.success()
{
if set_locale(lang, region.clone()).await.is_ok() {
update_time_settings_after_region_change(region);
}
@ -498,11 +494,8 @@ mod preferred_languages {
use crate::pages::time::region::localized_iso_codes;
use super::Message;
use cosmic::{
Apply,
iced::{Alignment, Length},
widget,
};
use cosmic::iced::{Alignment, Length};
use cosmic::{Apply, widget};
use cosmic_settings_page::Section;
pub fn section() -> Section<crate::pages::Message> {
@ -710,20 +703,26 @@ pub async fn page_reload() -> eyre::Result<PageRefresh> {
let mut available_languages_set = BTreeSet::new();
let output = tokio::process::Command::new("localectl")
.arg("list-locales")
// Use 'locale -a' instead of 'localectl list-locales' for OpenRC compatibility
let output_result = tokio::process::Command::new("locale")
.arg("-a")
.output()
.await
.expect("Failed to run localectl");
.await;
let output = String::from_utf8(output.stdout).unwrap_or_default();
for line in output.lines() {
if line == "C.UTF-8" {
continue;
let locale_list = match output_result {
Ok(output) => {
let output_str = String::from_utf8(output.stdout).unwrap_or_default();
parse_locale_output(&output_str)
}
Err(why) => {
tracing::error!(?why, "failed to list available locales using 'locale -a'");
Vec::new()
}
};
if let Some(locale) = registry.locale(line) {
available_languages_set.insert(localized_locale(&locale, line.to_owned()));
for line in locale_list {
if let Some(locale) = registry.locale(&line) {
available_languages_set.insert(localized_locale(&locale, line));
}
}
@ -841,27 +840,28 @@ fn popover_menu_row(
.apply(Element::from)
}
pub async fn set_locale(
lang: String,
region: String,
) -> Result<std::process::ExitStatus, std::io::Error> {
eprintln!("setting locale lang={lang}, region={region}");
tokio::process::Command::new("localectl")
.arg("set-locale")
.args(&[
["LANG=", &lang].concat(),
["LC_ADDRESS=", &region].concat(),
["LC_IDENTIFICATION=", &region].concat(),
["LC_MEASUREMENT=", &region].concat(),
["LC_MONETARY=", &region].concat(),
["LC_NAME=", &region].concat(),
["LC_NUMERIC=", &region].concat(),
["LC_PAPER=", &region].concat(),
["LC_TELEPHONE=", &region].concat(),
["LC_TIME=", &region].concat(),
])
.status()
/// Sets the system locale using D-Bus instead of localectl for OpenRC compatibility.
pub async fn set_locale(lang: String, region: String) -> eyre::Result<()> {
tracing::debug!("setting locale lang={lang}, region={region}");
let conn = zbus::Connection::system()
.await
.wrap_err("failed to connect to system D-Bus")?;
let proxy = locale1::locale1Proxy::new(&conn)
.await
.wrap_err("failed to create locale1 D-Bus proxy")?;
let locale_settings = build_locale_settings(&lang, &region);
let locale_strs: Vec<&str> = locale_settings.iter().map(|s| s.as_str()).collect();
proxy
.set_locale(&locale_strs, true)
.await
.wrap_err("failed to set locale via D-Bus")?;
tracing::debug!("successfully set locale via D-Bus");
Ok(())
}
/// Sets the user's preferred language list via AccountsService D-Bus.
@ -883,7 +883,7 @@ pub async fn set_user_language(language_list: String) -> eyre::Result<()> {
.await
.wrap_err("failed to set language via AccountsService")?;
eprintln!("set user language via AccountsService: {language_list}");
tracing::debug!("set user language via AccountsService: {language_list}");
Ok(())
}
@ -1016,3 +1016,206 @@ fn strip_locale_suffix(locale: &str) -> String {
.unwrap_or(without_codeset)
.to_string()
}
/// Parses the output from `locale -a` command and returns a vector of locale strings.
/// Filters out pseudo-locales (C, POSIX) and accepts only allowed character encodings.
fn parse_locale_output(output: &str) -> Vec<String> {
// Regex to match pseudo-locales: C or POSIX, optionally followed by .anything
let pseudo_locale_re = Regex::new(r"^(C|POSIX)(\.|$)").unwrap();
// Regex to match UTF-8 encoded locales (case-insensitive)
// Supports optional modifiers after encoding (e.g., @euro, @valencia)
let utf8_encoding_re = Regex::new(r"(?i)\.(utf-?8)(@.*)?$").unwrap();
output
.lines()
.map(|line| line.trim())
.filter(|line| !pseudo_locale_re.is_match(line))
.filter(|line| utf8_encoding_re.is_match(line))
.map(|line| line.to_string())
.collect()
}
/// Builds the locale settings array for D-Bus SetLocale call.
/// Sets LANG to the language parameter and all LC_* variables to the region parameter.
fn build_locale_settings(lang: &str, region: &str) -> Vec<String> {
vec![
format!("LANG={}", lang),
format!("LC_ADDRESS={}", region),
format!("LC_IDENTIFICATION={}", region),
format!("LC_MEASUREMENT={}", region),
format!("LC_MONETARY={}", region),
format!("LC_NAME={}", region),
format!("LC_NUMERIC={}", region),
format!("LC_PAPER={}", region),
format!("LC_TELEPHONE={}", region),
format!("LC_TIME={}", region),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_locale_output_handles_empty_input() {
let output = "";
let result = parse_locale_output(output);
assert_eq!(result.len(), 0);
}
#[test]
fn test_parse_locale_output_preserves_locale_strings() {
let output = "en_US.utf8\nde_DE.utf8\nfr_FR.utf8\n";
let result = parse_locale_output(output);
assert_eq!(result.len(), 3);
assert!(result.contains(&"en_US.utf8".to_string()));
}
#[test]
fn test_build_locale_settings_includes_all_lc_variables() {
let lang = "en_US.UTF-8";
let region = "de_DE.UTF-8";
let settings = build_locale_settings(lang, region);
assert_eq!(settings.len(), 10);
assert!(settings.contains(&format!("LANG={}", lang)));
assert!(settings.contains(&format!("LC_ADDRESS={}", region)));
assert!(settings.contains(&format!("LC_IDENTIFICATION={}", region)));
assert!(settings.contains(&format!("LC_MEASUREMENT={}", region)));
assert!(settings.contains(&format!("LC_MONETARY={}", region)));
assert!(settings.contains(&format!("LC_NAME={}", region)));
assert!(settings.contains(&format!("LC_NUMERIC={}", region)));
assert!(settings.contains(&format!("LC_PAPER={}", region)));
assert!(settings.contains(&format!("LC_TELEPHONE={}", region)));
assert!(settings.contains(&format!("LC_TIME={}", region)));
}
#[test]
fn test_build_locale_settings_uses_correct_values() {
let lang = "fr_FR.UTF-8";
let region = "en_GB.UTF-8";
let settings = build_locale_settings(lang, region);
// LANG should use the lang parameter
assert!(settings.iter().any(|s| s == "LANG=fr_FR.UTF-8"));
// LC_* variables should use the region parameter
assert!(settings.iter().any(|s| s == "LC_TIME=en_GB.UTF-8"));
}
#[test]
fn test_parse_locale_output_filters_pseudo_locales() {
let output = "C\nC.utf8\nC.UTF-8\nPOSIX\nen_US.utf8\nde_DE.UTF-8\n";
let result = parse_locale_output(output);
// Should filter out all C and POSIX variants
assert!(!result.contains(&"C".to_string()));
assert!(!result.contains(&"C.utf8".to_string()));
assert!(!result.contains(&"C.UTF-8".to_string()));
assert!(!result.contains(&"POSIX".to_string()));
// Should keep actual locales
assert!(result.contains(&"en_US.utf8".to_string()));
assert!(result.contains(&"de_DE.UTF-8".to_string()));
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_locale_output_accepts_only_utf8_locales() {
let output =
"en_US\nen_US.utf8\nen_US.UTF-8\nar_IN\nar_IN.utf8\nde_DE.iso88591\nfr_FR.UTF-8\n";
let result = parse_locale_output(output);
// Should accept UTF-8 variants
assert!(result.contains(&"en_US.utf8".to_string()));
assert!(result.contains(&"en_US.UTF-8".to_string()));
assert!(result.contains(&"ar_IN.utf8".to_string()));
assert!(result.contains(&"fr_FR.UTF-8".to_string()));
// Should filter out non-UTF-8 encoded locales
assert!(!result.contains(&"en_US".to_string()));
assert!(!result.contains(&"ar_IN".to_string()));
assert!(!result.contains(&"de_DE.iso88591".to_string()));
assert_eq!(result.len(), 4);
}
#[test]
fn test_parse_locale_output_filters_any_c_posix_variant() {
let output = "C\nC.iso88591\nC.anything\nPOSIX\nPOSIX.utf8\nen_US.utf8\n";
let result = parse_locale_output(output);
// Should filter out any C or POSIX variant regardless of encoding
assert!(!result.contains(&"C".to_string()));
assert!(!result.contains(&"C.iso88591".to_string()));
assert!(!result.contains(&"C.anything".to_string()));
assert!(!result.contains(&"POSIX".to_string()));
assert!(!result.contains(&"POSIX.utf8".to_string()));
// Should keep actual locales
assert!(result.contains(&"en_US.utf8".to_string()));
assert_eq!(result.len(), 1);
}
#[test]
fn test_parse_locale_output_handles_whitespace() {
let output = " en_US.utf8 \n\t de_DE.UTF-8\t\n fr_FR.utf8 \n";
let result = parse_locale_output(output);
// Should handle leading/trailing whitespace
assert!(result.contains(&"en_US.utf8".to_string()));
assert!(result.contains(&"de_DE.UTF-8".to_string()));
assert!(result.contains(&"fr_FR.utf8".to_string()));
assert_eq!(result.len(), 3);
}
#[test]
fn test_parse_locale_output_handles_empty_lines() {
let output = "en_US.utf8\n\n\nde_DE.UTF-8\n\n";
let result = parse_locale_output(output);
// Should skip empty lines
assert!(result.contains(&"en_US.utf8".to_string()));
assert!(result.contains(&"de_DE.UTF-8".to_string()));
assert_eq!(result.len(), 2);
}
#[test]
fn test_parse_locale_output_catalan_not_filtered_as_pseudo() {
let output = "C\nca_ES.UTF-8\nca_ES.utf8\ncs_CZ.UTF-8\nen_US.utf8\n";
let result = parse_locale_output(output);
// Should filter out C but not Catalan (ca_*) or Czech (cs_*)
assert!(!result.contains(&"C".to_string()));
assert!(result.contains(&"ca_ES.UTF-8".to_string()));
assert!(result.contains(&"ca_ES.utf8".to_string()));
assert!(result.contains(&"cs_CZ.UTF-8".to_string()));
assert_eq!(result.len(), 4);
}
#[test]
fn test_parse_locale_output_handles_locale_modifiers() {
let output = "en_US.UTF-8@euro\nca_ES.UTF-8@valencia\nde_DE.utf8\n";
let result = parse_locale_output(output);
// Locales with modifiers should be accepted
assert!(result.contains(&"en_US.UTF-8@euro".to_string()));
assert!(result.contains(&"ca_ES.UTF-8@valencia".to_string()));
assert!(result.contains(&"de_DE.utf8".to_string()));
assert_eq!(result.len(), 3);
}
#[test]
fn test_parse_locale_output_case_variations() {
let output = "en_US.UTF-8\nen_US.utf-8\nen_US.utf8\nen_US.UTF8\nde_DE.Utf8\n";
let result = parse_locale_output(output);
// All case variations should be accepted (case-insensitive regex)
assert!(result.contains(&"en_US.UTF-8".to_string()));
assert!(result.contains(&"en_US.utf-8".to_string()));
assert!(result.contains(&"en_US.utf8".to_string()));
assert!(result.contains(&"en_US.UTF8".to_string()));
assert!(result.contains(&"de_DE.Utf8".to_string()));
assert_eq!(result.len(), 5);
}
}

View file

@ -0,0 +1,631 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
//! Service manager abstraction for managing system services.
//!
//! This module provides a trait-based interface for interacting with system
//! service managers (like systemd, OpenRC, etc.), allowing for testability
//! and flexibility across different init systems.
use std::future::Future;
use std::pin::Pin;
/// Trait for managing system services.
///
/// Implementations are bound to a specific service name and provide
/// methods to query service state and perform lifecycle operations.
pub trait ServiceManager {
/// Whether the service is configured to start on boot.
fn is_enabled(&self) -> bool;
/// Whether the service is currently running.
fn is_active(&self) -> bool;
/// Start the service.
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>>;
/// Enable the service to start on boot and start it now.
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>>;
/// Whether the service is installed on this system.
fn is_installed(&self) -> bool;
}
/// A newtype around `Box<dyn ServiceManager>` that enables `#[derive(Default)]`
/// on containing structs. Methods on `ServiceManager` are accessible via `Deref`.
///
/// The `Default` impl creates a manager for the `"bluetooth"` service, which is
/// currently the only consumer. If other services need this in the future the
/// default can be made configurable.
pub struct ServiceManagerHandle(Box<dyn ServiceManager>);
impl ServiceManagerHandle {
pub fn new(manager: Box<dyn ServiceManager>) -> Self {
Self(manager)
}
}
impl std::ops::Deref for ServiceManagerHandle {
type Target = dyn ServiceManager;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl Default for ServiceManagerHandle {
fn default() -> Self {
Self(create_default_service_manager("bluetooth"))
}
}
/// Log a warning or error after a privileged command completes.
fn log_command_result(
result: &Result<std::process::ExitStatus, std::io::Error>,
description: &str,
service: &str,
) {
match result {
Ok(status) if status.success() => {}
Ok(status) => tracing::warn!(
"{} for '{}' failed with exit code: {:?}",
description,
service,
status.code(),
),
Err(e) => tracing::error!("Failed to execute {} for '{}': {}", description, service, e,),
}
}
/// Run a privileged command via `pkexec`.
///
/// This is the common async entry point for service management actions
/// that require elevated privileges.
async fn run_pkexec_command(args: &[&str], description: &str, service: &str) {
let result = tokio::process::Command::new("pkexec")
.args(args)
.status()
.await;
log_command_result(&result, description, service);
}
/// Mock ServiceManager that returns configurable boolean values.
#[cfg(test)]
pub struct MockServiceManager {
enabled: bool,
active: bool,
installed: bool,
}
#[cfg(test)]
impl MockServiceManager {
pub fn new(enabled: bool, active: bool) -> Self {
Self {
enabled,
active,
installed: true,
}
}
pub fn with_installed(mut self, installed: bool) -> Self {
self.installed = installed;
self
}
}
#[cfg(test)]
impl ServiceManager for MockServiceManager {
fn is_enabled(&self) -> bool {
self.enabled
}
fn is_active(&self) -> bool {
self.active
}
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn is_installed(&self) -> bool {
self.installed
}
}
/// SystemD implementation of ServiceManager.
#[cfg(feature = "systemd")]
pub struct SystemDServiceManager {
service_name: String,
}
#[cfg(feature = "systemd")]
impl SystemDServiceManager {
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
}
}
}
#[cfg(feature = "systemd")]
impl ServiceManager for SystemDServiceManager {
fn is_enabled(&self) -> bool {
std::process::Command::new("systemctl")
.args(["is-enabled", &self.service_name])
.status()
.map(|status| status.success())
.unwrap_or(true)
}
fn is_active(&self) -> bool {
std::process::Command::new("systemctl")
.args(["is-active", &self.service_name])
.status()
.map(|status| status.success())
.unwrap_or(true)
}
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
let service = self.service_name.clone();
Box::pin(async move {
run_pkexec_command(
&["systemctl", "start", &service],
"systemctl start",
&service,
)
.await;
})
}
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
let service = self.service_name.clone();
Box::pin(async move {
run_pkexec_command(
&["systemctl", "enable", "--now", &service],
"systemctl enable --now",
&service,
)
.await;
})
}
fn is_installed(&self) -> bool {
std::process::Command::new("systemctl")
.args(["cat", &self.service_name])
.status()
.map(|status| status.success())
.unwrap_or(false)
}
}
/// OpenRC implementation of ServiceManager.
#[cfg(feature = "openrc")]
pub struct OpenRcServiceManager {
service_name: String,
}
#[cfg(feature = "openrc")]
impl OpenRcServiceManager {
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
}
}
}
/// Parses `rc-update show` output to check if a service is in any runlevel.
///
/// Uses exact service name matching to avoid false positives from prefix matches
/// (e.g., "bluetoothd" should not match a lookup for "bluetooth").
#[cfg(any(feature = "openrc", test))]
fn is_service_in_runlevel_output(output: &str, service_name: &str) -> bool {
output.lines().any(|line| {
line.split('|')
.next()
.map(|name| name.trim() == service_name)
.unwrap_or(false)
})
}
#[cfg(feature = "openrc")]
impl ServiceManager for OpenRcServiceManager {
fn is_enabled(&self) -> bool {
std::process::Command::new("rc-update")
.args(["show"])
.output()
.map(|output| {
is_service_in_runlevel_output(
&String::from_utf8_lossy(&output.stdout),
&self.service_name,
)
})
.unwrap_or(true)
}
fn is_active(&self) -> bool {
std::process::Command::new("rc-service")
.args([&self.service_name, "status"])
.status()
.map(|status| status.success())
.unwrap_or(true)
}
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
let service = self.service_name.clone();
Box::pin(async move {
run_pkexec_command(
&["rc-service", &service, "start"],
"rc-service start",
&service,
)
.await;
})
}
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
let service = self.service_name.clone();
Box::pin(async move {
run_pkexec_command(
&["rc-update", "add", &service, "default"],
"rc-update add",
&service,
)
.await;
run_pkexec_command(
&["rc-service", &service, "start"],
"rc-service start",
&service,
)
.await;
})
}
fn is_installed(&self) -> bool {
std::path::Path::new("/etc/init.d")
.join(&self.service_name)
.exists()
}
}
/// Detect the running service manager and create a manager for the named service.
///
/// Checks runtime directories (`/run/systemd/system`, `/run/openrc`) to determine
/// which service manager is actively running, rather than relying on which binaries
/// are installed. This avoids false detections when multiple service managers are
/// installed but only one is actively managing services.
pub fn detect_service_manager(
service_name: impl Into<String>,
) -> Result<Box<dyn ServiceManager>, String> {
let service_name = service_name.into();
#[cfg(feature = "systemd")]
{
if std::path::Path::new("/run/systemd/system").exists() {
tracing::debug!("Detected systemd service manager via /run/systemd/system");
return Ok(Box::new(SystemDServiceManager::new(service_name)));
}
}
#[cfg(feature = "openrc")]
{
if std::path::Path::new("/run/openrc").exists() {
tracing::debug!("Detected OpenRC service manager via /run/openrc");
return Ok(Box::new(OpenRcServiceManager::new(service_name)));
}
}
{
let mut checked = Vec::new();
#[cfg(feature = "systemd")]
checked.push("systemd (/run/systemd/system)");
#[cfg(feature = "openrc")]
checked.push("openrc (/run/openrc)");
Err(format!(
"Could not detect a running service manager. Checked for: {}. \
None of these service managers appear to be running on this system.",
checked.join(", ")
))
}
}
/// Factory: returns `MockServiceManager` in tests, detects the running service manager in production.
#[cfg(test)]
pub fn create_default_service_manager(_service_name: impl Into<String>) -> Box<dyn ServiceManager> {
Box::new(MockServiceManager::new(false, false))
}
#[cfg(not(test))]
pub fn create_default_service_manager(service_name: impl Into<String>) -> Box<dyn ServiceManager> {
let service_name = service_name.into();
detect_service_manager(&service_name).unwrap_or_else(|e| {
tracing::warn!(
"Failed to detect service manager: {}. Service management features will not be available.",
e
);
tracing::warn!(
"Using no-op service manager for '{}'. Service status will always report as enabled/active, but operations will not actually execute.",
service_name
);
// Graceful degradation — app continues but service management won't work
Box::new(NoOpServiceManager)
})
}
/// Fallback when no service manager is detected — reports everything as active but does nothing.
struct NoOpServiceManager;
impl ServiceManager for NoOpServiceManager {
fn is_enabled(&self) -> bool {
true
}
fn is_active(&self) -> bool {
true
}
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn is_installed(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing_subscriber::util::SubscriberInitExt;
#[test]
fn test_service_manager_trait_is_enabled() {
struct TestServiceManager {
enabled: bool,
installed: bool,
}
impl ServiceManager for TestServiceManager {
fn is_enabled(&self) -> bool {
self.enabled
}
fn is_active(&self) -> bool {
true
}
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn is_installed(&self) -> bool {
self.installed
}
}
let test_service = TestServiceManager {
enabled: true,
installed: true,
};
assert!(test_service.is_enabled());
let disabled_service = TestServiceManager {
enabled: false,
installed: true,
};
assert!(!disabled_service.is_enabled());
}
#[test]
fn test_service_manager_trait_is_active() {
struct TestServiceManager {
active: bool,
installed: bool,
}
impl ServiceManager for TestServiceManager {
fn is_enabled(&self) -> bool {
true
}
fn is_active(&self) -> bool {
self.active
}
fn activate(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn enable(&self) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async {})
}
fn is_installed(&self) -> bool {
self.installed
}
}
let test_service = TestServiceManager {
active: true,
installed: true,
};
assert!(test_service.is_active());
let inactive_service = TestServiceManager {
active: false,
installed: true,
};
assert!(!inactive_service.is_active());
}
#[test]
fn test_mock_service_manager_returns_configured_values() {
let bluetooth = MockServiceManager::new(true, true);
assert!(bluetooth.is_enabled());
assert!(bluetooth.is_active());
let disabled_bluetooth = MockServiceManager::new(false, false);
assert!(!disabled_bluetooth.is_enabled());
assert!(!disabled_bluetooth.is_active());
}
#[test]
#[cfg(feature = "systemd")]
fn test_systemd_service_manager_implements_trait() {
let bluetooth = SystemDServiceManager::new("bluetooth");
// Can't test actual systemd calls in unit tests, but verify the struct exists
let _enabled: bool = bluetooth.is_enabled();
let _active: bool = bluetooth.is_active();
}
#[tokio::test]
async fn test_service_manager_trait_has_activate_method() {
let bluetooth = MockServiceManager::new(false, false);
bluetooth.activate().await;
}
#[tokio::test]
async fn test_service_manager_trait_has_enable_method() {
let bluetooth = MockServiceManager::new(false, false);
bluetooth.enable().await;
}
#[tokio::test]
async fn test_service_manager_activate_returns_future() {
let bluetooth = MockServiceManager::new(false, false);
let future = bluetooth.activate();
future.await;
}
#[tokio::test]
async fn test_service_manager_enable_returns_future() {
let bluetooth = MockServiceManager::new(false, false);
let future = bluetooth.enable();
future.await;
}
#[test]
fn test_default_service_manager_uses_mock_in_tests() {
let bluetooth = create_default_service_manager("bluetooth");
assert!(!bluetooth.is_enabled());
assert!(!bluetooth.is_active());
}
#[test]
#[cfg(feature = "openrc")]
fn test_openrc_service_manager_implements_trait() {
let bluetooth = OpenRcServiceManager::new("bluetooth");
// Can't test actual OpenRC calls in unit tests, but verify the struct exists
let _enabled: bool = bluetooth.is_enabled();
let _active: bool = bluetooth.is_active();
}
#[test]
fn test_detect_service_manager_returns_result() {
let result = detect_service_manager("test-service");
match result {
Ok(_) => {}
Err(e) => {
assert!(!e.is_empty());
assert!(
e.contains("service manager"),
"Error message should mention service manager: {}",
e
);
}
}
}
#[test]
fn test_mock_service_manager_accepts_service_name() {
let bluetooth = MockServiceManager::new(true, true);
assert!(bluetooth.is_enabled());
}
#[test]
fn test_mock_service_manager_is_installed_defaults_to_true() {
let bluetooth = MockServiceManager::new(true, true);
assert!(bluetooth.is_installed());
}
#[test]
fn test_mock_service_manager_with_installed_configures_state() {
let not_installed = MockServiceManager::new(true, true).with_installed(false);
assert!(!not_installed.is_installed());
let installed = MockServiceManager::new(true, true).with_installed(true);
assert!(installed.is_installed());
}
#[test]
fn test_no_op_service_manager_reports_not_installed() {
let no_op = NoOpServiceManager;
// When no service manager is detected at runtime, the fallback must report
// not-installed so DBusServiceUnknown falls through to the unknown-service path
// instead of showing a non-functional toggle.
assert!(!no_op.is_installed());
}
#[test]
fn test_parse_rc_update_output_exact_service_name() {
// Must use exact matching, not prefix matching — "bluetoothd" must not match "bluetooth".
let output = " bluetoothd | default\n";
let found = is_service_in_runlevel_output(output, "bluetooth");
assert!(!found);
}
#[test]
fn test_parse_rc_update_output_matches_exact_name() {
let output = " bluetooth | default\n";
let found = is_service_in_runlevel_output(output, "bluetooth");
assert!(found);
}
#[test]
fn test_parse_rc_update_output_not_found() {
let output = " dbus | default\n network | sysinit\n";
let found = is_service_in_runlevel_output(output, "bluetooth");
assert!(!found);
}
#[test]
fn test_log_command_result_logs_all_branches() {
let _guard = tracing_subscriber::fmt().with_test_writer().set_default();
let err_result: Result<std::process::ExitStatus, std::io::Error> = Err(
std::io::Error::new(std::io::ErrorKind::NotFound, "command not found"),
);
log_command_result(&err_result, "test command", "test-service");
let fail_result = std::process::Command::new("false").status();
log_command_result(&fail_result, "test false", "test-service");
let ok_result = std::process::Command::new("true").status();
log_command_result(&ok_result, "test true", "test-service");
}
#[test]
fn test_parse_rc_update_output_multi_line() {
let output = " dbus | default\n bluetooth | default\n network | sysinit\n";
let found = is_service_in_runlevel_output(output, "bluetooth");
assert!(found);
}
}

View file

@ -2,11 +2,9 @@ use std::any::TypeId;
use ashpd::desktop::location::{Location, LocationProxy};
use chrono::NaiveDate;
use cosmic::iced::{
Subscription,
futures::{SinkExt, StreamExt, channel::mpsc::Sender, future},
stream,
};
use cosmic::iced::futures::channel::mpsc::Sender;
use cosmic::iced::futures::{SinkExt, StreamExt, future};
use cosmic::iced::{Subscription, stream};
use sunrise::{Coordinates, SolarDay, SolarEvent};
use tokio::select;

View file

@ -1,8 +1,5 @@
use cosmic::iced::{
Subscription,
futures::{self, SinkExt},
stream,
};
use cosmic::iced::futures::{self, SinkExt};
use cosmic::iced::{Subscription, stream};
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::fmt::Debug;
use std::hash::Hash;

View file

@ -1,10 +1,8 @@
use std::path::PathBuf;
use cosmic::iced::{
Subscription,
futures::{SinkExt, StreamExt, channel::mpsc::Sender, future},
stream,
};
use cosmic::iced::futures::channel::mpsc::Sender;
use cosmic::iced::futures::{SinkExt, StreamExt, future};
use cosmic::iced::{Subscription, stream};
use image::{ImageBuffer, Rgba};
#[derive(Clone, Debug)]

View file

@ -1,7 +1,8 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::{iced::Border, theme};
use cosmic::iced::Border;
use cosmic::theme;
#[must_use]
pub fn display_container_frame() -> cosmic::theme::Container<'static> {

View file

@ -1,4 +1,5 @@
use std::{future::Future, io, process};
use std::future::Future;
use std::{io, process};
use futures::future::select;

View file

@ -8,10 +8,9 @@ use cosmic::cosmic_theme::Spacing;
use cosmic::iced::core::text::Wrapping;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::color_picker::ColorPickerUpdate;
use cosmic::widget::space::{horizontal, vertical};
use cosmic::widget::{
self, ColorPickerModel, button, column, container, divider, icon, list, row, settings,
space::{horizontal, vertical},
text,
self, ColorPickerModel, button, column, container, divider, icon, list, row, settings, text,
};
use cosmic::{Apply, Element, theme};
use cosmic_settings_page as page;
@ -206,8 +205,10 @@ pub fn go_next_with_item<'a, Msg: 'static>(
msg_opt: impl Into<Option<Msg>>,
) -> list::ListButton<'a, Msg> {
settings::item_row(vec![
text::body(description).wrapping(Wrapping::Word).into(),
horizontal().into(),
text::body(description)
.width(Length::Fill)
.wrapping(Wrapping::Word)
.into(),
row::with_capacity(2)
.push(item)
.push(icon::from_name("go-next-symbolic").size(16).icon())

View file

@ -1,21 +0,0 @@
[package]
name = "cosmic-pipewire"
version = "1.0.7"
edition = "2024"
repository = "https://github.com/pop-os/cosmic-settings"
rust-version.workspace = true
license = "MPL-2.0"
publish = true
[dependencies]
intmap = "3.1.3"
libspa = "0.9.2"
libspa-sys = "0.9.2"
pipewire = "0.9"
serde = { version = "1.0.228", features = ["derive"]}
serde_json = "1.0.149"
tracing = "0.1.44"
[features]
# Cache route port types
route-port-type = []

View file

@ -1,358 +0,0 @@
Mozilla Public License Version 2.0
==================================
## 1. Definitions
### 1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
### 1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
### 1.3. "Contribution"
means Covered Software of a particular Contributor.
### 1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
### 1.5. "Incompatible With Secondary Licenses"
means
+ (a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
+ (b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
### 1.6. "Executable Form"
means any form of the work other than Source Code Form.
### 1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
### 1.8. "License"
means this document.
### 1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
### 1.10. "Modifications"
means any of the following:
+ (a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
+ (b) any new file in Source Code Form that contains any Covered
Software.
### 1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
### 1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
### 1.13. "Source Code Form"
means the form of the work preferred for making modifications.
### 1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
## 2. License Grants and Conditions
### 2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
+ (a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
+ (b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
### 2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
### 2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
+ (a) for any code that a Contributor has removed from Covered Software;
or
+ (b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
+ (c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
### 2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
### 2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
### 2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
### 2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
## 3. Responsibilities
### 3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
### 3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
+ (a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
+ (b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
### 3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
### 3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
### 3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
## 4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
## 5. Termination
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
## 6. Disclaimer of Warranty
**Covered Software is provided under this License on an "as is"
basis, without warranty of any kind, either expressed, implied, or
statutory, including, without limitation, warranties that the
Covered Software is free of defects, merchantable, fit for a
particular purpose or non-infringing. The entire risk as to the
quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You
(not any Contributor) assume the cost of any necessary servicing,
repair, or correction. This disclaimer of warranty constitutes an
essential part of this License. No use of any Covered Software is
authorized under this License except under this disclaimer.**
#7. Limitation of Liability
**Under no circumstances and under no legal theory, whether tort
(including negligence), contract, or otherwise, shall any
Contributor, or anyone who distributes Covered Software as
permitted above, be liable to You for any direct, indirect,
special, incidental, or consequential damages of any character
including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any
and all other commercial damages or losses, even if such party
shall have been informed of the possibility of such damages. This
limitation of liability shall not apply to liability for death or
personal injury resulting from such party's negligence to the
extent applicable law prohibits such limitation. Some
jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and
limitation may not apply to You.**
## 8. Litigation
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
## 9. Miscellaneous
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
## 10. Versions of the License
### 10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
### 10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
### 10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
### 10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
## Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
## Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -1,27 +0,0 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use pipewire::device::DeviceInfoRef;
/// Device information
#[must_use]
#[derive(Clone, Debug)]
pub struct Device {
pub id: u32,
pub name: String,
}
impl Device {
/// Attains process info from a pipewire info node.
#[must_use]
pub fn from_device(info: &DeviceInfoRef) -> Option<Self> {
let props = info.props()?;
let device = Device {
id: props.get("object.id")?.parse::<u32>().ok()?,
name: props.get("device.description")?.to_owned(),
};
Some(device)
}
}

View file

@ -21,20 +21,19 @@ pub use route::{Route, RouteProps};
mod spa_utils;
pub use spa_utils::Channel;
use libspa::{
param::{ParamType, format::FormatProperties},
pod::{self, Pod, serialize::PodSerializer},
utils::SpaTypes,
};
use pipewire::{
device::{DeviceChangeMask, DeviceListener},
main_loop::MainLoopWeak,
metadata::MetadataListener,
node::NodeListener,
proxy::{ProxyListener, ProxyT},
types::ObjectType,
};
use std::{cell::RefCell, rc::Rc};
use libspa::param::ParamType;
use libspa::param::format::FormatProperties;
use libspa::pod::serialize::PodSerializer;
use libspa::pod::{self, Pod};
use libspa::utils::SpaTypes;
use pipewire::device::{DeviceChangeMask, DeviceListener};
use pipewire::main_loop::MainLoopWeak;
use pipewire::metadata::MetadataListener;
use pipewire::node::NodeListener;
use pipewire::proxy::{ProxyListener, ProxyT};
use pipewire::types::ObjectType;
use std::cell::RefCell;
use std::rc::Rc;
pub type NodeId = u32;
pub type RouteId = u32;

View file

@ -1,8 +1,10 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{Channel, spa_utils::array_from_pod};
use libspa::{pod::Pod, utils::Id};
use crate::Channel;
use crate::spa_utils::array_from_pod;
use libspa::pod::Pod;
use libspa::utils::Id;
use pipewire::node::{NodeInfoRef, NodeState};
use std::ffi::c_float;

View file

@ -1,98 +0,0 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Currently unusued
use crate::pipewire::Direction;
use pipewire::port::PortInfoRef;
#[must_use]
#[derive(Clone, Debug)]
pub struct Port {
pub node_id: u32,
pub object_id: u32,
pub port_id: u32,
pub audio_channel: String,
pub format_dsp: String,
pub object_path: String,
pub port_direction: Direction,
pub port_group: String,
pub port_name: String,
pub port_alias: String,
pub port_physical: bool,
pub port_terminal: bool,
pub port_monitor: bool,
}
impl Port {
/// Attains process info from a pipewire info port.
#[must_use]
pub fn from_port(info: &PortInfoRef) -> Option<Self> {
let props = info.props()?;
let object_id = info.id();
let port_direction = match info.direction() {
libspa::utils::Direction::Input => Direction::Input,
libspa::utils::Direction::Output => Direction::Output,
_ => return None,
};
let mut node_id = 0;
let mut port_id = 0;
let mut port_monitor = false;
let mut port_physical = false;
let mut port_terminal = false;
let mut audio_channel = String::new();
let mut format_dsp = String::new();
let mut object_path = String::new();
let mut port_alias = String::new();
let mut port_group = String::new();
let mut port_name = String::new();
for (entry, value) in props.iter() {
match entry {
// 32 bit float mono audio
"format.dsp" => format_dsp = value.to_owned(),
// FR
"audio.channel" => audio_channel = value.to_owned(),
// playback
"port.group" => port_group = value.to_owned(),
// 1
"port.id" => port_id = value.parse::<u32>().ok()?,
// false
"port.monitor" => port_monitor = value == "true",
// true
"port.physical" => port_physical = value == "true",
// true
"port.terminal" => port_terminal = value == "true",
// alsa:acp:Device:3:playback:playback_1
"object.path" => object_path = value.to_owned(),
// playback_FR
"port.name" => port_name = value.to_owned(),
// MosArt USB Audio Device:playback_FR
"port.alias" => port_alias = value.to_owned(),
// 59
"node.id" => node_id = value.parse::<u32>().ok()?,
_ => (),
}
}
let port = Port {
format_dsp,
audio_channel,
port_id,
port_direction,
object_path,
port_name,
port_alias,
port_group,
port_monitor,
port_physical,
port_terminal,
node_id,
object_id,
};
Some(port)
}
}

View file

@ -1,7 +1,8 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{Availability, spa_utils::string_from_pod};
use crate::Availability;
use crate::spa_utils::string_from_pod;
use libspa::pod::Pod;
#[derive(Clone, Debug, Default)]

View file

@ -3,11 +3,10 @@
use std::ffi::{c_float, c_int};
use crate::{
Availability, Channel, Direction,
spa_utils::{array_from_pod, string_from_pod},
};
use libspa::{pod::Pod, utils::Id};
use crate::spa_utils::{array_from_pod, string_from_pod};
use crate::{Availability, Channel, Direction};
use libspa::pod::Pod;
use libspa::utils::Id;
#[derive(Clone, Debug, Default)]
pub struct Route {

View file

@ -1,150 +0,0 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use libspa::pod::Pod;
use std::ffi::CStr;
/// Read a `Pod`'s string if it contains a string.
pub fn string_from_pod(pod: &Pod) -> Option<String> {
if !pod.is_string() {
return None;
}
let mut cstr = std::ptr::null();
unsafe {
// SAFETY: Pod is checked to be a string beforehand
if libspa_sys::spa_pod_get_string(pod.as_raw_ptr(), &mut cstr) == 0 && !cstr.is_null() {
return Some(String::from_utf8_lossy(CStr::from_ptr(cstr).to_bytes()).into_owned());
}
}
None
}
/// SAFETY: Must be absolutely certain that the array is a compatible array.
pub unsafe fn array_from_pod<CType: Copy>(pod: &Pod) -> Option<Vec<CType>> {
if !pod.is_array() {
return None;
}
let mut len = 0;
unsafe {
let array: *mut CType = libspa_sys::spa_pod_get_array(pod.as_raw_ptr(), &mut len).cast();
if array.is_null() {
return None;
}
Some(std::slice::from_raw_parts(array, len as usize).to_vec())
}
}
#[repr(u32)]
#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
pub enum Channel {
#[default]
UNKNOWN = 0, // unspecified
NA, // N/A, silent
MONO, // mono stream
FL, // front left
FR, // front right
FC, // front center
LFE, // LFE
SL, // side left
SR, // side right
FLC, // front left center
FRC, // front right center
RC, // rear center
RL, // rear left
RR, // rear right
TC, // top center
TFL, // top front left
TFC, // top front center
TFR, // top front right
TRL, // top rear left
TRC, // top rear center
TRR, // top rear right
RLC, // rear left center
RRC, // rear right center
FLW, // front left wide
FRW, // front right wide
LFE2, // LFE 2
FLH, // front left high
FCH, // front center high
FRH, // front right high
TFLC, // top front left center
TFRC, // top front right center
TSL, // top side left
TSR, // top side right
LLFE, // left LFE
RLFE, // right LFE
BC, // bottom center
BLC, // bottom left center
BRC = 37, // bottom right center
AUX0 = 4096, // aux channels
AUX1,
AUX2,
AUX3,
AUX4,
AUX5,
AUX6,
AUX7,
AUX8,
AUX9,
AUX10,
AUX11,
AUX12,
AUX13,
AUX14,
AUX15,
AUX16,
AUX17,
AUX18,
AUX19,
AUX20,
AUX21,
AUX22,
AUX23,
AUX24,
AUX25,
AUX26,
AUX27,
AUX28,
AUX29,
AUX30,
AUX31,
AUX32,
AUX33,
AUX34,
AUX35,
AUX36,
AUX37,
AUX38,
AUX39,
AUX40,
AUX41,
AUX42,
AUX43,
AUX44,
AUX45,
AUX46,
AUX47,
AUX48,
AUX49,
AUX50,
AUX51,
AUX52,
AUX53,
AUX54,
AUX55,
AUX56,
AUX57,
AUX58,
AUX59,
AUX60,
AUX61,
AUX62,
AUX63 = 4159,
}

12
debian/changelog vendored
View file

@ -1,3 +1,15 @@
cosmic-settings (1.7.0) noble; urgency=medium
* Epoch 1.7.0 version update
-- Michael Murphy <michael@mmurphy.dev> Fri, 21 Aug 2026 20:44:57 +0200
cosmic-settings (1.0.12) noble; urgency=medium
* release: 1.0.12
-- Michael Murphy <mmstick@pop-os> Mon, 27 Apr 2026 15:24:27 +0200
cosmic-settings (1.0.7) noble; urgency=medium
* release: 1.0.7

4
debian/control vendored
View file

@ -8,15 +8,14 @@ Build-Depends:
fonts-open-sans,
just,
libclang-dev,
libdav1d-dev,
libexpat1-dev,
libfontconfig-dev,
libfreetype-dev,
libinput-dev,
libpipewire-0.3-dev,
libudev-dev,
libwayland-dev,
libxkbcommon-dev,
mold,
pkg-config,
rust-all,
Standards-Version: 4.6.2
@ -29,6 +28,7 @@ Depends:
${shlibs:Depends},
accountsservice,
cosmic-randr,
cosmic-settings-daemon,
gettext,
iso-codes,
network-manager-gnome,

7
debian/rules vendored
View file

@ -1,6 +1,7 @@
#!/usr/bin/make -f
export DESTDIR = debian/tmp
OS_VERSION = $(shell rg VERSION_ID /etc/os-release | awk -F\" '{print $2}')
%:
dh $@
@ -9,7 +10,11 @@ override_dh_auto_clean:
ischroot || just vendor
override_dh_auto_build:
test -e vendor.tar && just build-vendored || just
ifeq ($(OS_VERSION),22.04)
test -e vendor.tar && just build-vendored || just build-release --no-default-features
else
test -e vendor.tar && just build-vendored || just build-release
endif
override_dh_auto_install:
just rootdir=$(DESTDIR) install

View file

@ -1,4 +1,6 @@
tar-ignore=.github
tar-ignore=.vscode
tar-ignore=target/.rustc_info.json
tar-ignore=target/CACHEDIR.TAG
tar-ignore=target/debug
tar-ignore=target/release
tar-ignore=vendor
tar-ignore=target

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1760533177,
"narHash": "sha256-OwM1sFustLHx+xmTymhucZuNhtq98fHIbfO8Swm5L8A=",
"lastModified": 1785747939,
"narHash": "sha256-D740uKsMbgsfK2oaDenJLLPIZfq7W0/g4KN/Fls8eKs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "35f590344ff791e6b1d6d6b8f3523467c9217caf",
"rev": "104240a772428cc2e20d8fd86c9ddbb886bbaff2",
"type": "github"
},
"original": {

View file

@ -41,6 +41,7 @@
libinput
libpulseaudio.dev
pipewire.dev
dav1d
];
RUST_SRC_PATH = rustPlatform.rustLibSrc;

View file

@ -16,7 +16,7 @@ xdg-entry-date-time-comment = المنطقة الزمنية وإعدادات ا
xdg-entry-default-apps = التطبيقات المبدئية
xdg-entry-default-apps-comment = متصفح الويب المبدئي، عميل البريد، متصفح الملفات، والتطبيقات الأخرى
xdg-entry-desktop = سطح المكتب
xdg-entry-desktop-comment = إعدادات خلفية الشاشة والمظهر واللوحة والمنصة وإدارة النوافذ ومساحة العمل
xdg-entry-desktop-comment = إعدادات خلفية الشاشة والمظهر واللوحة والإرساء وإدارة النوافذ ومساحة العمل
xdg-entry-displays = شاشات العرض
xdg-entry-displays-comment = أدِر إعدادات تضبيط العرض
xdg-entry-dock = الإرساء
@ -107,6 +107,8 @@ vpn-error = خطأ في VPN
.connect = فشل الاتصال بـ VPN
.connection-editor = فشل محرر الاتصال
.connection-settings = فشل الحصول على إعدادات الاتصالات النشطة
.openvpn-config-path = مسار ملف غير صالح لإعدادات OpenVPN
.openvpn-config-path-desc = يجب أن يكون الملف المختار موجودًا على نظام الملفات المحلي.
.updating-state = فشل تحديث حالة مدير الشبكة
.wireguard-config-path = مسار ملف غير صالح لتكوين WireGuard
.wireguard-config-path-desc = يجب أن يكون الملف المختار موجودًا على نظام ملفات محلي.
@ -273,13 +275,26 @@ frosted = تأثير الزجاج البلوري على واجهة النظام
enable-export = قم بتطبيق هذا المظهر على تطبيقات GNOME.
.desc = لا تدعم جميع مجموعات الأدوات التبديل التلقائي. قد تحتاج التطبيقات غير التابعة لـ COSMIC إلى إعادة التشغيل بعد تغيير المظهر.
icon-theme = نسق الأيقونات
.desc = يطبق مجموعة مختلفة من الأيقونات على التطبيقات
.desc = يطبِّق مجموعة مختلفة من الأيقونات على التطبيقات
text-tint = لون نص الواجهة
.desc = اللون المستخدم لاشتقاق ألوان نص الواجهة التي تتمتع بتباين كافٍ على الأسطح المختلفة
style = المظهر
style = الأسلوب
.round = دائري
.slightly-round = دائري قليلاً
.square = مربع
.frosted-glass = زجاج بلوري
.frosted-system-interface = واجهة النظام
.frosted-system-interface-desc = يُطبِّق مظهر الزجاج البلوري على المشغّل، ومكتبة التطبيقات، ونظرة عامة على مساحة العمل، وعناصر التحكم التي تظهر على الشاشة
.frosted-windows = النوافذ
.frosted-windows-desc = يطبِّق مظهر الزجاج البلوري على النوافذ ومربعات الحوار
.frosted-panels = اللوحات
.frosted-panels-desc = يطبِّق الزجاج البلوري على اللوحة وقاعدة الإرساء
.frosted-applets = البريمجات
.frosted-applets-desc = يطبِّق الزجاج البلوري على قوائم البريمجات الموجودة على اللوحة والإرساء
.frosted-thickness = السماكة البلورية
.less = أقل
.more = أكثر
.glass-opacity = عتامة الزجاج
interface-density = كثافة الواجهة
.comfortable = مريحة
.compact = صغير ة
@ -352,7 +367,7 @@ super-key = إجراءات مفتاح Super
.launcher = فتح مشغل التطبيقات
.workspaces = فتح مساحات العمل
.applications = فتح التطبيقات
.disable = تعطيل
.none = لا شيء
edge-gravity = تنجذب النوافذ العائمة إلى الحواف القريبة
window-controls = تحكمات النوافذ
.maximize = أظهر زر التكبير
@ -533,11 +548,27 @@ keyboard-sources = مصادر الإدخال
.view-layout = اعرض تخطيط لوحة المفاتيح
.remove = أزِل
.add = أضِف مصدر إدخال
keyboard-special-char = إدخال أحرف خاصة
keyboard-special-char = إدخال الأحرف الخاصة
.alternate = مفتاح الأحرف البديلة
.compose = مفتاح التركيب
.compose-desc = مفتاح التركيب يسمح بإدخال مجموعة واسعة من الأحرف. لاستخدامه، اضغط على مفتاح التركيب ثم اكتب تسلسلًا من الأحرف. على سبيل المثال، الضغط على مفتاح التركيب متبوعًا بـ C و o سيدخل الرمز ©، بينما الضغط على مفتاح التركيب متبوعًا بـ a و ‘ سيدخل الحرف á.
.caps = مفتاح Caps Lock
.compose = مفتاح التكوين
.compose-desc = يتيح مفتاح التكوين إدخال مجموعة متنوعة من الأحرف. لاستخدامه، اضغط على مفتاح التكوين ثم سلسلة من الأحرف. على سبيل المثال، سيؤدي الضغط على مفتاح التكوين متبوعًا بالحرفين C و o إلى إدخال الرمز ©، بينما سيؤدي الضغط على مفتاح «التكوين» متبوعًا بالحرفين a و ‘ إلى إدخال الحرف á.
.caps = مفتاح قفل الحروف
.ctrl = مفتاح التحكم
.ctrl-right = مفتاح Ctrl الأيمن
.swap-with-ctrl = تبديل مع مفتاح التحكم
.alt = مفتاح Alt
.alt-left = مفتاح Alt الأيسر
.alt-right = Alt الأيمن
.super = Super
.super-left = Super الأيسر
.super-right = Super الأيمن
.menu = مفتاح القائمة
.backspace = مسافة للخلف
.escape = هروب (ESC)
.swap-with-escape = التبديل مع مفتاح الهروب
.print-screen = اطبع الشاشة
.scroll-lock = قفل التمرير
.none = لا شيء
keyboard-typing-assist = الكتابة
.repeat-rate = معدل التكرار
.repeat-delay = تأخير التكرار
@ -911,3 +942,9 @@ xdg-entry-displays-keywords = COSMIC;عرض;
xdg-entry-workspaces = مساحات العمل
xdg-entry-wired-comment = الاتصالات السلكية وملفات تعريف الاتصال
no-search-results = لا توجد شبكات تطابق بحثك.
workspaces-navigation = التنقل
.wraparound = تنقل بين مساحة العمل الأولى والأخيرة باستخدام اختصارات لوحة المفاتيح والإيماءات
bluetooth-display-passkey = إقران البلوتوث
.description = يُرجى إدخال مفتاح المرور التالي على { $device }، ثم الضغط على مفتاح Enter
bluetooth-display-pin = إقتران البلوتوث
.description = يُرجى إدخال رمز PIN التالي على { $device }، ثم الضغط على Enter

View file

@ -0,0 +1,11 @@
cancel = Cancel·lar
confirm = Confirmar
password = Contrasenya
identity = Identitat
password-confirm = Confirmeu la contrasenya
connected = Connectat
settings = Configuració
save = Desa
forget-dialog = Oblidar aquesta xarxa Wi-Fi?
.description = Caldrà tornar a introduir la contrasenya per utilitzar aquesta xarxa en el futur.
close = Tancar

View file

@ -32,7 +32,7 @@ xdg-entry-network = Síť a Wi-Fi
xdg-entry-network-comment = Správa síťových připojení
xdg-entry-notifications = Oznámení
xdg-entry-notifications-comment = Režim Nerušit, oznámení na zamykací obrazovce a nastavení jednotlivých aplikací
xdg-entry-panel-comment = Hlavní systémová lišta pro menu a applety
xdg-entry-panel-comment = Hlavní systémová lišta pro nabídky a applety
xdg-entry-power = Napájení a baterie
xdg-entry-power-comment = Režimy napájení a možnosti úspory energie
xdg-entry-region-language = Region a jazyk
@ -86,10 +86,10 @@ dock = Dok
## Desktop: Panel
panel = Panel
panel-behavior-and-position = Chování a pozice
panel-behavior-and-position = Chování a umístění
.autohide = Automaticky skrýt panel
.dock-autohide = Automaticky skrýt dok
.position = Pozice na obrazovce
.position = Umístění na obrazovce
.display = Zobrazit na obrazovce
panel-top = Nahoře
panel-bottom = Dole
@ -116,9 +116,9 @@ panel-missing = Nastavení panelu chybí
.desc = Soubor nastavení panelu chybí kvůli poškození nebo použití vlastního nastavení.
.fix = Obnovit výchozí
applets = Applety
start-segment = Začínající segment
start-segment = Počáteční segment
center-segment = Prostřední segment
end-segment = Konečný segment
end-segment = Koncový segment
add = Přidat
add-applet = Přidat applet
search-applets = Vyhledat applety...
@ -215,7 +215,8 @@ sound-alerts = Upozornění
.volume = Hlasitost upozornění
.sound = Zvuk pro upozornění
sound-applications = Aplikace
.desc = Hlasitosti aplikací a nastavení
.desc = Hlasitosti a nastavení aplikací
.none = Žádné aplikace nepoužívají zvuk
## System
@ -280,8 +281,24 @@ keyboard-sources = Vstupní zdroje
keyboard-special-char = Zadávání speciálních znaků
.alternate = Klávesa pro náhradní znaky
.compose = Klávesa Compose
.compose-desc = Klávesa Compose umožňuje zadávat širokou škálu znaků. Pro její použití stiskněte Compose a poté zadejte sekvenci znaků. Například [Compose, C, O] vloží znak „©“, zatímco [Compose, A, ‘] znak „á“.
.compose-desc = Klávesa Compose umožňuje zadávat širokou škálu znaků. Pro její použití stiskněte Compose a poté zadejte sekvenci znaků. Například Compose, C, O vloží znak „©“, zatímco Compose, A, ‘ vloží znak „á“.
.caps = Klávesa Caps Lock
.ctrl = Control
.ctrl-right = Pravý Ctrl
.swap-with-ctrl = Zaměnit s klávesou Control
.alt = Alt
.alt-left = Levý Alt
.alt-right = Pravý Alt
.super = Super
.super-left = Levý Super
.super-right = Pravý Super
.menu = Klávesa Menu
.backspace = Backspace
.escape = Escape
.swap-with-escape = Zaměnit s klávesou Escape
.print-screen = Print Screen
.scroll-lock = Scroll Lock
.none = Žádná
## Input: Keyboard: Shortcuts
@ -353,8 +370,10 @@ accessibility = Přístupnost
.unavailable = Nedostupné
.screen-reader = Čtečka obrazovky
.high-contrast = Vysoký kontrast
.invert-colors = Převrátit barvy
.invert-colors = Inverze barev
.color-filters = Barevné filtry
.shake-cursor = Nalezení ukazatele zatřesením
.shake-cursor-desc = Rychlým zatřesením myší se ukazatel dočasně zvětší
hearing = Sluch
.mono = Přehrávat stereo zvuk jako mono
default = Výchozí
@ -418,6 +437,8 @@ vpn-error = Chyba VPN
.connect = Nepodařilo se připojit k VPN
.connection-editor = Editor připojení selhal
.connection-settings = Nepodařilo se získat nastavení aktivních připojení
.openvpn-config-path = Chybně zadaná cesta souboru nastavení OpenVPN
.openvpn-config-path-desc = Vybraný soubor musí být na lokálním souborovém systému
.updating-state = Nepodařilo se aktualizovat stav správce sítě
.wireguard-config-path = Chybně zadaná cesta souboru nastavení WireGuard
.wireguard-config-path-desc = Vybraný soubor musí být na lokálním souborovém systému
@ -443,14 +464,14 @@ container-background = Pozadí kontejneru
.desc = Používá se pro navigační postranní panel, výsuvný panel, dialogy a podobné prvky
control-tint = Odstín ovládacích prvků
.desc = Používá se pro pozadí standardních tlačítek, vyhledávacích polí, textových vstupů a podobných komponent
frosted = Efekt zmrazeného okna na systémovém rozhraní
.desc = Použije rozmazané pozadí pro panel, dok, applety, spouštěč a knihovnu aplikací
frosted = Efekt ledového skla na systémovém rozhraní
.desc = Použít rozmazané pozadí pro panel, dok, applety, spouštěč a knihovnu aplikací
enable-export = Použít aktuální motiv v aplikacích GNOME
.desc = Ne všechny toolkity podporují automatické přepínání. Aplikace, které nejsou postaveny na COSMIC toolkitu mohou vyžadovat restart po změně motivu.
password-mismatch = Heslo a jeho potvrzení se musí shodovat
invalid-username = Neplatné uživatelské jméno
icon-theme = Motiv ikon
.desc = Použije jinou sadu ikon pro aplikace
.desc = Použít jinou sadu ikon pro aplikace
monospace-font = Písmo s pevnou šířkou
window-management = Správa oken
window-tiling = Dlaždicový režim oken
@ -467,7 +488,7 @@ super-key = Akce klávesy Super
.launcher = Otevřít spouštěč
.workspaces = Otevřít pracovní plochy
.applications = Otevřít aplikace
.disable = Zakázat
.none = Žádná
edge-gravity = Plovoucí okna se přichytávají k blízkým okrajům
focus-navigation = Navigace zaměření
.focus-follows-cursor = Zaměření následuje kurzor
@ -503,7 +524,7 @@ magnifier = Lupa
.show_overlay = Zobrazit překrytí lupy
.increment = Krok přiblížení
.signin = Spustit lupu při přihlášení
.applet = Lupu lze přepnout v appletu na panelu
.applet = Lupu lze přepnout (zap/vyp) v nabídce zobrazené po kliknutí na applet přímo v panelu
.movement = Pohyb přiblíženého zobrazení
.continuous = Neustále s ukazatelem
.onedge = Když ukazatel dosáhne okraje
@ -511,9 +532,9 @@ magnifier = Lupa
color-filter = Typ barevného filtru
.unknown = Neznámý filtr aktivní
.greyscale = Stupně šedi
.deuteranopia = Zeleno/Červená (oslabení zelené, Deuteranopie)
.protanopia = Červeno/Zelená (oslabení červené, Protanopie)
.tritanopia = Modro/Žlutá (oslabení modré, Tritanopie)
.deuteranopia = Zelená/červená (oslabené vnímání zelené, deuteranopie)
.protanopia = Červená/zelená (oslabené vnímání červené, protanopie)
.tritanopia = Modrá/žlutá (oslabené vnímání modré, tritanopie)
text-tint = Odstín textu rozhraní
.desc = Používá se k odvození barev textu rozhraní, které mají dostatečný kontrast na různých površích
experimental-settings = Experimentální nastavení
@ -530,8 +551,21 @@ display = Obrazovky
.additional-scale-options = Další možnosti měřítka
style = Styl
.round = Kulatý
.slightly-round = Lehce kulatý
.slightly-round = Mírně zaoblený
.square = Hranatý
.frosted-glass = Efekt ledového skla
.frosted-system-interface = Rozhraní systému
.frosted-system-interface-desc = Použít efekt ledového skla pro spouštěč, knihovnu aplikací, přehled pracovních ploch a ovládací prvky na obrazovce
.frosted-windows = Okna
.frosted-windows-desc = Použít efekt ledového skla pro okna a dialogy
.frosted-panels = Panely
.frosted-panels-desc = Použít efekt ledového skla pro dok a panel
.frosted-applets = Applety
.frosted-applets-desc = Použít efekt ledového skla pro nabídky appletů na panelu a doku
.frosted-thickness = Tloušťka ledu
.less = menší
.more = větší
.glass-opacity = Krytí skla
interface-density = Hustota rozhraní
.comfortable = Pohodlné
.compact = Kompaktní
@ -627,13 +661,13 @@ power-saving = Možnosti úspory energie
disable-while-typing = Zakázat při psaní
input-devices = Vstupní zařízení
modified = { $count } změněných
slow = Pomalá
fast = Rychlá
short = Krátké
long = Dlouhé
slow = Pomalu
fast = Rychle
short = Krátká
long = Dlouhá
keyboard-typing-assist = Psaní
.repeat-rate = Frekvence opakování
.repeat-delay = Zpoždění opakování
.repeat-rate = Rychlost opakování
.repeat-delay = Prodleva před opakováním
keyboard-numlock-boot = Num Lock
.boot-state = Stav klávesy při spuštění systému
.last-boot = Poslední spuštění systému
@ -893,3 +927,9 @@ xdg-entry-displays-keywords = COSMIC;Obrazovka;Obrazovky;Displeje;Monitory;Zobra
xdg-entry-workspaces-keywords = COSMIC;Pracovní;Plocha;Plochy;Orientace;Přehled;Sledování;
xdg-entry-x11-applications-keywords = COSMIC;X11;Aplikace;Hra;Hry;Kompatibilita;
no-search-results = Žádné sítě neodpovídají vašemu vyhledávání.
workspaces-navigation = Navigace
.wraparound = Přecházet mezi první a poslední pracovní plochou pomocí klávesových zkratek a gest
bluetooth-display-passkey = Párování Bluetooth
.description = Zadejte prosím následující párovací kód na zařízení { $device } a poté stiskněte Enter
bluetooth-display-pin = Párování Bluetooth
.description = Zadejte prosím následující PIN na zařízení { $device } a poté stiskněte Enter

View file

@ -11,8 +11,8 @@ add-network = Tilføj Netværk
add-vpn = Tilføj VPN
airplane-on = Airplane mode is on. Flytilstand er slået til
cable-unplugged = Kabel frakoblet
connect = Tilslut
connected = Tilsluttet
connect = Opret forbindelse
connected = Forbundet
connecting = Tilslutter…
disconnect = Afbryd
forget = Glem
@ -21,14 +21,14 @@ network-and-wireless = Netværk & trådløst
no-networks = Der blev ikke fundet nogen netværk.
no-vpn = Ingen VPN-forbindelser tilgængelige.
password = Adgangskode
password-confirm = Godkend Adgangskode
password-confirm = Bekræft adgangskode
remove = Fjern
settings = Indstillinger
username = Brugernavn
visible-networks = Synlige Netværk
identity = Identitet
forget-dialog = Glem dette Wi-Fi-netværk?
.description = Du skal indtaste en adgangskode igen for at bruge dette Wi-Fi-netværk i fremtiden.
.description = Du skal indtaste adgangskoden igen for at bruge dette Wi-Fi-netværk i fremtiden.
network-device-state =
.activated = Tilsluttet
.config = Opretter forbindelse
@ -192,7 +192,6 @@ never = Aldrig
## Desktop: Udseende
appearance = Udseende
.desc = Accentfarver og tema.
accent-color = Accentfarve
app-background = Applikation eller vinduesbaggrund
auto = Auto
@ -200,11 +199,11 @@ close = Luk
color-picker = Farvevælger
copied-to-clipboard = Kopieret til udklipsholder
copy-to-clipboard = Kopiér til udklipsholder
dark = Mørk
dark = Mørkt
export = Eksporter
hex = Hex
import = Importer
light = Lys
light = Lyst
mode-and-colors = Tilstand og farver
recent-colors = Seneste farver
reset-to-default = Nulstil til standard
@ -515,7 +514,7 @@ show-extended-input-sources = Vis udvidede inputkilder
keyboard-shortcuts = Tastaturgenveje
.desc = Se og tilpas genveje
cancel = Annuller
cancel = Afbryd
command = Kommando
custom = Brugerdefineret
debug = Debug
@ -714,7 +713,7 @@ region = Region
## Applications
applications = Applikationer
applications = Programmer
## Applications: Default Applications

View file

@ -9,7 +9,7 @@ number = { $number }
add-network = Netzwerk hinzufügen
.profile = Profil hinzufügen
add-vpn = VPN hinzufügen
airplane-on = Flugzeugmodus ist eingeschaltet.
airplane-on = Flugmodus ist eingeschaltet.
cable-unplugged = Kabel ausgesteckt
connect = Verbinden
connected = Verbunden
@ -60,6 +60,8 @@ vpn-error = VPN-Fehler
.connect = Verbinden mit dem VPN ist fehlgeschlagen
.connection-editor = Verbindungseditor ist fehlgeschlagen
.connection-settings = Abrufen der Einstellungen für aktive Verbindungen ist fehlgeschlagen
.openvpn-config-path = Ungültiger Dateipfad für OpenVPN-Konfiguration
.openvpn-config-path-desc = Die ausgewählte Datei muss sich im lokalen Dateisystem befinden.
.updating-state = Aktualisieren des Netzwerkmanager-Status ist fehlgeschlagen
.wireguard-config-path = Ungültiger Dateipfad für WireGuard-Konfiguration
.wireguard-config-path-desc = Die ausgewählte Datei muss sich im lokalen Dateisystem befinden.
@ -122,6 +124,8 @@ accessibility = Barrierefreiheit
.high-contrast = Modus für hohen Kontrast
.invert-colors = Farben invertieren
.color-filters = Farbfilter
.shake-cursor = Mauszeiger schütteln, um ihn zu finden
.shake-cursor-desc = Schüttle die Maus kurz, um den Mauszeiger vorübergehend zu vergrößern
hearing = Hören
.mono = Stereo-Audio als Mono abspielen
default = Standard
@ -234,6 +238,19 @@ style = Stil
.round = Rund
.slightly-round = Leicht rund
.square = Quadratisch
.frosted-glass = Milchglas
.frosted-system-interface = Systemschnittstelle
.frosted-system-interface-desc = Wendet einen Milchglaseffekt auf den Starter, die Anwendungsbibliothek, die Arbeitsflächenübersicht und die Bildschirmsteuerelemente an
.frosted-windows = Fenster
.frosted-windows-desc = Wendet einen Milchglaseffekt auf Fenster und Dialoge an
.frosted-panels = Panels
.frosted-panels-desc = Wendet einen Milchglaseffekt auf das Panel und das Dock an
.frosted-applets = Applets
.frosted-applets-desc = Wendet einen Milchglaseffekt auf die Applet-Menüs im Panel und im Dock an
.frosted-thickness = Dicke der Mattierung
.less = weniger
.more = mehr
.glass-opacity = Glastrübung
interface-density = Dichte der Benutzeroberfläche
.comfortable = Komfortabel
.compact = Kompakt
@ -263,12 +280,12 @@ applets = Applets
center-segment = Mittelsegment
end-segment = Endsegment
large = Groß
no-applets-found = Keine Applets gefunden...
no-applets-found = Keine Applets gefunden ...
panel-bottom = Unten
panel-left = Links
panel-right = Rechts
panel-top = Oben
search-applets = Applets suchen...
search-applets = Applets suchen ...
small = Klein
start-segment = Anfangssegment
panel-appearance = Aussehen
@ -306,7 +323,7 @@ super-key = Super-Tasten-Aktion
.launcher = Starter öffnen
.workspaces = Arbeitsflächen öffnen
.applications = Anwendungen öffnen
.disable = Deaktivieren
.none = Keine
edge-gravity = Schwebende Fenster werden von nahe gelegenen Kanten angezogen
window-controls = Fenstersteuerung
.minimize = Minimieren-Schaltfläche anzeigen
@ -416,6 +433,7 @@ sound-alerts = Alarmsignale
.sound = Alarmsignalklang
sound-applications = Anwendungen
.desc = Lautstärken und Einstellungen von Anwendungen
.none = Keine Anwendungen verwenden einen Klang
## Energie
@ -495,6 +513,22 @@ keyboard-special-char = Eingabe von Sonderzeichen
.compose = Compose-Taste
.compose-desc = Mit der Compose-Taste kannst du viele verschiedene Zeichen eingeben. Drück einfach die Compose-Taste und dann die Zeichen, die du brauchst. Wenn du zum Beispiel die Compose-Taste und dann C und o drückst, kommt ©, und wenn du die Compose-Taste und dann a und ' drückst, kommt á.
.caps = Feststelltaste
.ctrl = Steuerungstaste
.ctrl-right = Rechte Strg-Taste
.swap-with-ctrl = Mit der Steuerungstaste tauschen
.alt = Alt-Taste
.alt-left = Linke Alt-Taste
.alt-right = Rechte Alt-Taste
.super = Super-Taste
.super-left = Linke Super-Taste
.super-right = Rechte Super-Taste
.menu = Menü-Taste
.backspace = Rücktaste
.escape = Escape-Taste
.swap-with-escape = Mit der Escape-Taste tauschen
.print-screen = Druck-Taste
.scroll-lock = Rollen-Taste
.none = Keine
keyboard-typing-assist = Tippen
.repeat-rate = Wiederholungsrate
.repeat-delay = Wiederholungsverzögerung
@ -505,7 +539,7 @@ keyboard-numlock-boot = Numlock
.off = Deaktiviert
.set = Numlock-Boot-Status setzen
added = Hinzugefügt
type-to-search = Zum Suchen tippen...
type-to-search = Zum Suchen tippen ...
show-extended-input-sources = Erweiterte Eingabequellen anzeigen
## Input: Keyboard: Shortcuts
@ -912,3 +946,9 @@ xdg-entry-desktop-keywords = COSMIC;Desktop;
xdg-entry-desktop = Desktop
xdg-entry-a11y-keywords = COSMIC;Barrierefreiheit;A11y;Bildschirm;Leser;Vergrößerungsglas;Kontrast;Farbe;
xdg-entry-a11y-comment = Bildschirmleser, Vergrößerungsglas, hoher Kontrast und Farbfilter
workspaces-navigation = Navigation
.wraparound = Mit Tastenkombinationen und Gesten zwischen der ersten und letzten Arbeitsfläche wechseln
bluetooth-display-passkey = Bluetooth-Kopplung
.description = Bitte gib den folgenden Passkey auf { $device } ein und drücke dann die Eingabetaste
bluetooth-display-pin = Bluetooth-Kopplung
.description = Bitte gib die folgende PIN auf { $device } ein und drücke dann die Eingabetaste

View file

@ -1 +1,858 @@
share = Κοινοποίηση δικτύου
connect = Σύνδεση
close = Κλείσιμο
save = Αποθήκευση
password = Κωδικός πρόσβασης
remove = Αφαίρεση
import = Εισαγωγή
username = Όνομα χρήστη
light = Ανοιχτόχρωμο
ok = OK
applets = Μικροεφαρμογές
dbus-connection-error = Αποτυχία σύνδεσης στο DBus
dark = Σκουρόχρωμο
cancel = Ακύρωση
settings = Ρυθμίσεις
replace = Αντικατάσταση
add = Προσθήκη
appearance = Εμφάνιση
panel-left = Αριστερά
panel-top = Πάνω
region = Περιοχή
forget-dialog = Διαγραφή αυτού του δικτύου Wi-Fi;
.description = Θα χρειαστεί να εισαγάγετε ξανά τον κωδικό πρόσβασης για να χρησιμοποιήσετε αυτό το δίκτυο Wi-Fi στο μέλλον.
default = Προεπιλογή
unknown = Άγνωστο
connecting = Σύνδεση…
export = Εξαγωγή
panel-right = Δεξιά
rgb = RGB
desktop = Επιφάνεια εργασίας
identity = Ταυτότητα
disconnect = Αποσύνδεση
app = Ρυθμίσεις COSMIC
debug = Εντοπισμός σφαλμάτων
number = { $number }
small = Μικρό
terminate = Τερματισμός
password-confirm = Επιβεβαίωση κωδικού πρόσβασης
applications = Εφαρμογές
connected = Συνδέθηκε
large = Μεγάλο
confirm = Επιβεβαίωση
enable = Ενεργοποίηση
never = Ποτέ
activate = Ενεργοποίηση
dialog-add = Προσθήκη
command = Εντολή
panel-bottom = Κάτω
navigate = Πλοήγηση
colors = Χρώματα
xdg-entry-touchpad = Επιφάνεια αφής
xdg-entry-bluetooth-comment = Διαχείριση συσκευών Bluetooth
xdg-entry-appearance-keywords = Έμφαση;Χρώμα;Εικονίδιο;Γραμματοσειρά;Θέμα;COSMIC;Accent;Color;Icon;Font;Theme
xdg-entry-mouse = Ποντίκι
firmware = Υλικολογισμικό
xdg-entry-a11y = Προσβασιμότητα
xdg-entry-desktop-keywords = Επιφάνεια;Εργασίας;COSMIC;Desktop;
xdg-entry-users = Χρήστες
xdg-entry-default-apps = Προεπιλεγμένες εφαρμογές
xdg-entry-applications-comment = Προεπιλεγμένες εφαρμογές, εφαρμογές εκκίνησης και ρυθμίσεις συμβατότητας εφαρμογών με το X11
xdg-entry-about-keywords = Πληροφορίες;Σχετικά;Περί;COSMIC;About
xdg-entry-notifications = Ειδοποιήσεις
xdg-entry-a11y-keywords = Προσβασιμότητα;Προσιτότητα;Οθόνη;Ανάγνωση;Αναγνώστης;Μεγεθυντικός;Φακός;Αντίθεση;Χρώμα;COSMIC;Accessibility;A11y;Screen;Reader;Magnifier;Contrast;Color;
xdg-entry-desktop = Επιφάνεια εργασίας
xdg-entry-date-time-keywords = Ζώνη;Ώρα;COSMIC;Time;Zone;
xdg-entry-wallpaper = Ταπετσαρία
xdg-entry-keyboard = Πληκτρολόγιο
xdg-entry-a11y-comment = Ανάγνωση οθόνης, μεγεθυντικός φακός, υψηλή αντίθεση και χρωματικά φίλτρα
xdg-entry-displays = Οθόνες
sound = Ήχος
xdg-entry-date-time = Ημερομηνία και ώρα
notifications = Ειδοποιήσεις
xdg-entry-bluetooth-keywords = Σύνδεση;COSMIC;Bluetooth;
about = Πληροφορίες
workspaces = Χώροι εργασίας
xdg-entry-wireless = Wi-Fi
xdg-entry-about-comment = Όνομα συσκευής, πληροφορίες υλικού, προεπιλογές λειτουργικού συστήματος
xdg-entry-appearance-comment = Χρώματα έμφασης και θέματα
xdg-entry-comment = Εφαρμογή ρυθμίσεων για το περιβάλλον επιφάνειας εργασίας COSMIC
xdg-entry-applications = Εφαρμογές
xdg-entry-default-apps-keywords = Προεπιλογή;Προεπιλεγμένη;Εφαρμογή;COSMIC;Default;Application
xdg-entry-keywords = Προτιμήσεις;Ρυθμίσεις;COSMIC;Settings;
xdg-entry-about = Πληροφορίες
xdg-entry-input = Συσκευές εισόδου
keyboard = Πληκτρολόγιο
xdg-entry-appearance = Εμφάνιση
xdg-entry-sound = Ήχος
xdg-entry-displays-keywords = Οθόνη;COSMIC;Display;
xdg-entry-workspaces = Χώροι εργασίας
xdg-entry-date-time-comment = Ζώνη ώρας, αυτόματη ρύθμιση ρολογιού και μορφοποίηση ώρας
system-shortcut = Σύστημα
.app-library = Άνοιγμα βιβλιοθήκης εφαρμογών
.brightness-down = Μείωση φωτεινότητας οθόνης
.brightness-up = Αύξηση φωτεινότητας οθόνης
.display-toggle = Εναλλαγή εσωτερικής οθόνης
.home-folder = Άνοιγμα προσωπικού φακέλου
.keyboard-brightness-down = Μείωση φωτεινότητας πληκτρολογίου
.keyboard-brightness-up = Αύξηση φωτεινότητας πληκτρολογίου
.launcher = Άνοιγμα εκκινητή
.log-out = Αποσύνδεση
.lock-screen = Κλείδωμα οθόνης
.mute = Σίγαση εξόδου ήχου
.mute-mic = Σίγαση εισόδου μικροφώνου
.play-pause = Αναπαραγωγή/παύση
.play-next = Επόμενο κομμάτι
.play-prev = Προηγούμενο κομμάτι
.poweroff = Τερματισμός λειτουργίας
.screenshot = Λήψη στιγμιότυπου οθόνης
.suspend = Αναστολή λειτουργίας
.terminal = Άνοιγμα τερματικού
.touchpad-toggle = Εναλλαγή λειτουργίας επιφάνειας αφής
.volume-lower = Μείωση έντασης εξόδου ήχου
.volume-raise = Αύξηση έντασης εξόδου ήχου
.web-browser = Άνοιγμα προγράμματος περιήγησης ιστού
.window-switcher = Εναλλαγή μεταξύ των ανοικτών παραθύρων
.window-switcher-previous = Εναλλαγή μεταξύ των ανοικτών παραθύρων (αντίστροφα)
.workspace-overview = Άνοιγμα επισκόπησης χώρων εργασίας
super-key = Ενέργεια πλήκτρου Super
.launcher = Άνοιγμα μενού εκκίνησης
.workspaces = Άνοιγμα χώρων εργασίας
.applications = Άνοιγμα εφαρμογών
.none = Καμία
x-minutes =
{ $number } { $number ->
[one] λεπτό
*[other] λεπτά
}
vrr = Μεταβλητός ρυθμός ανανέωσης
.enabled = Ενεργός
.force = Πάντα
.auto = Αυτόματα
.disabled = Ανενεργός
window-hint-accent = Χρώμα ένδειξης ενεργού παραθύρου
fit-to-screen = Προσαρμογή στην οθόνη
control-tint = Απόχρωση στοιχείων ελέγχου
.desc = Χρησιμοποιείται για το φόντο των τυπικών κουμπιών, εισόδων αναζήτησης, εισόδων κειμένου και παρόμοιων στοιχείων
vpn-error = Σφάλμα VPN
.config = Αποτυχία προσθήκης διαμόρφωσης VPN
.connect = Αποτυχία σύνδεσης στο VPN
.connection-editor = Αποτυχία επεξεργαστή συνδέσεων
.connection-settings = Αποτυχία λήψης ρυθμίσεων για ενεργές συνδέσεις
.openvpn-config-path = Μη έγκυρη διαδρομή αρχείου για τη διαμόρφωση του OpenVPN
.openvpn-config-path-desc = Το επιλεγμένο αρχείο πρέπει να βρίσκεται σε τοπικό σύστημα αρχείων.
.updating-state = Αποτυχία ενημέρωσης κατάστασης διαχειριστή δικτύου
.wireguard-config-path = Μη έγκυρη διαδρομή αρχείων για τη διαμόρφωση του WireGuard
.wireguard-config-path-desc = Το επιλεγμένο αρχείο πρέπει να βρίσκεται στο τοπικό σύστημα αρχείων.
.wireguard-device = Αποτυχία δημιουργίας συσκευής WireGuard
.with-password =
Αποτυχία ορισμού { $field ->
*[username] ονόματος χρήστη
[password] κωδικού πρόσβασης
[password-flags] σημαιών κωδικού πρόσβασης
} VPN με το nmcli
toggle-stacking = Εναλλαγή στοίβαξης παραθύρων
no-applets-found = Δεν βρέθηκαν μικροεφαρμογές...
zoom-in = Μεγέθυνση
place-here = Τοποθετήστε τις μικροεφαρμογές εδώ
-requires-restart = Απαιτεί επανεκκίνηση
workspaces-orientation = Προσανατολισμός χώρων εργασίας
.vertical = Κάθετος
.horizontal = Οριζόντιος
acceleration-desc = Προσαρμόζει αυτόματα την ευαισθησία ιχνηλάτησης με βάση την ταχύτητα
all-displays = Όλες οι οθόνες
scheduling = Προγραμματισμός
.manual = Χειροκίνητο πρόγραμμα
frosted = Εφέ ημιδιαφανούς γυαλιού στο περιβάλλον συστήματος
.desc = Εφαρμόζει θόλωση του φόντου της γραμμής συστήματος, της μπάρας εφαρμογών, του μενού εκκίνησης και της βιβλιοθήκης εφαρμογών
about-hardware = Υλικό
.model = Μοντέλο υλικού
.memory = Μνήμη
.processor = Επεξεργαστής
.graphics = Γραφικά
.disk-capacity = Χωρητικότητα δίσκου
short = Σύντομη
vpn = VPN
.connections = Συνδέσεις VPN
.error = Αποτυχία προσθήκης διαμόρφωσης VPN
.remove = Αφαίρεση προφίλ σύνδεσης
.select-file = Επιλογή αρχείου διαμόρφωσης VPN
power-mode = Λειτουργία ενέργειας
.battery = Εκτεταμένη διάρκεια μπαταρίας
.battery-desc = Μειωμένη χρήση ενέργειας και χαμηλές επιδόσεις
.balanced = Ισορροπημένη
.balanced-desc = Μέτριες επιδόσεις και ήπια χρήση ενέργειας
.performance = Υψηλές επιδόσεις
.performance-desc = Κορυφαίες επιδόσεις και υψηλή χρήση ενέργειας
.no-backend = Δεν βρέθηκε υπηρεσία παρασκηνίου. Εγκαταστήστε το system76-power ή το power-profiles-daemon.
wired = Ενσύρματη δικτύωση
.adapter = Προσαρμογέας ενσύρματου δικτύου { $id }
.connections = Ενσύρματες συνδέσεις
.devices = Ενσύρματες συσκευές
.remove = Αφαίρεση προφίλ σύνδεσης
bluetooth-available = Κοντινές συσκευές
cable-unplugged = Αποσυνδεδεμένο καλώδιο
color-picker = Εργαλείο επιλογής χρωμάτων
interface-font = Γραμματοσειρά συστήματος
zoom-out = Σμίκρυνση
hot-corner = Γωνία συντόμευσης
.top-left-corner = Ενεργοποίηση της πάνω αριστερής γωνίας συντόμευσης για τους χώρους εργασίας
keyboard-typing-assist = Πληκτρολόγηση
.repeat-rate = Ρυθμός επανάληψης
.repeat-delay = Καθυστέρηση επανάληψης
edge-gravity = Έλξη αιωρούμενων παραθύρων στις κοντινές άκρες
amplification = Ενίσχυση ήχου
.desc = Επιτρέπει την αύξηση της έντασης στο 150%
recent-colors = Πρόσφατα χρώματα
about-os = Λειτουργικό σύστημα
.os = Λειτουργικό σύστημα
.os-architecture = Αρχιτεκτονική λειτουργικού συστήματος
.kernel = Έκδοση πυρήνα
.desktop-environment = Περιβάλλον επιφάνειας εργασίας
.windowing-system = Σύστημα διαχείρισης παραθύρων
panel-behavior-and-position = Συμπεριφορά και θέσεις
.autohide = Αυτόματη απόκρυψη γραμμής συστήματος
.dock-autohide = Αυτόματη απόκρυψη μπάρας εφαρμογών
.position = Θέση στην οθόνη
.display = Εμφάνιση στην οθόνη
add-language = Προσθήκη γλώσσας
.context = Προσθήκη γλώσσας
password-mismatch = Ο κωδικός πρόσβασης και το πεδίο επιβεβαίωσης θα πρέπει να είναι ίδια
accessibility = Προσβασιμότητα
.vision = Όραση
.on = Ενεργό
.off = Ανενεργό
.unavailable = Μη διαθέσιμο
.screen-reader = Ανάγνωση οθόνης
.high-contrast = Λειτουργία υψηλής αντίθεσης
.invert-colors = Αναστροφή χρωμάτων
.color-filters = Φίλτρα χρωμάτων
container-background = Φόντο πλαισίων
.desc-detail = Το χρώμα φόντου πλαισίων χρησιμοποιείται για την πλαϊνή στήλη πλοήγησης, το πλευρικό συρτάρι, τα παράθυρα διαλόγων και παρόμοια γραφικά στοιχεία. Από προεπιλογή, το χρώμα φόντου πλαισίων προκύπτει αυτόματα από το φόντο των παραθύρων.
.reset = Επαναφορά στο αυτόματο
.desc = Χρησιμοποιείται για την πλαϊνή στήλη πλοήγησης, το πλευρικό συρτάρι, τα παράθυρα διαλόγων και παρόμοια γραφικά στοιχεία
primary-button = Κύριο κουμπί
.desc = Ορίζει τη σειρά των φυσικών κουμπιών
.left = Αριστερό
.right = Δεξί
sound-alerts = Ειδοποιήσεις
.volume = Ένταση ήχου ειδοποιήσεων
.sound = Ήχος ειδοποιήσεων
icon-theme = Θέμα εικονιδίων
.desc = Εφαρμόζει ένα διαφορετικό σύνολο εικονιδίων για εφαρμογές
keyboard-shortcuts = Συντομεύσεις πληκτρολογίου
.desc = Προβολή και προσαρμογή συντομεύσεων
switch-between-windows = Εναλλαγή μεταξύ των παραθύρων
text-tint = Απόχρωση κειμένου περιβάλλοντος χρήστη
.desc = Χρησιμοποιείται για τον προσδιορισμό των χρωμάτων του κειμένου του περιβάλλοντος χρήστη ώστε να έχουν επαρκή αντίθεση σε διάφορες επιφάνειες
online-accounts = Διαδικτυακοί λογαριασμοί
.desc = Προσθήκη λογαριασμών, IMAP και SMTP, εταιρικών συνδέσεων
open-workspaces-view = Άνοιγμα επισκόπησης χώρων εργασίας
all = Όλα
remove-user = Αφαίρεση χρήστη
sound-input = Είσοδος ήχου
.volume = Ένταση εισόδου
.device = Συσκευή εισόδου
.level = Επίπεδο εισόδου
add-another-keybinding = Προσθήκη άλλου συνδυασμού πλήκτρων
copy-to-clipboard = Αντιγραφή στο πρόχειρο
monospace-font = Γραμματοσειρά σταθερού πλάτους
experimental-settings = Πειραματικές ρυθμίσεις
recent-folders = Πρόσφατοι φάκελοι
color-filter = Τύπος φίλτρου χρωμάτων
.unknown = Άγνωστο φίλτρο ενεργό
.greyscale = Κλίμακα του γκρι
.deuteranopia = Πράσινο/κόκκινο (αδυναμία στο πράσινο, Δευτερανοπία)
.protanopia = Κόκκινο/πράσινο (αδυναμία στο κόκκινο, Πρωτανοπία)
.tritanopia = Μπλε/κίτρινο (αδυναμία στο μπλε, Τριτανοπία)
migrate-workspace =
Μεταφορά χώρου εργασίας { $direction ->
*[down] στην κάτω έξοδο
[left] στην αριστερή έξοδο
[right] στη δεξιά έξοδο
[up] στην πάνω έξοδο
}
battery = Μπαταρία
.minute =
{ $value } { $value ->
[one] λεπτό
*[other] λεπτά
}
.hour =
{ $value } { $value ->
[one] ώρα
*[other] ώρες
}
.day =
{ $value } { $value ->
[one] ημέρα
*[other] ημέρες
}
.less-than-minute = Λιγότερο από ένα λεπτό
.and = και
.remaining-time =
{ $time } μέχρι την { $action ->
[full] πλήρη φόρτιση
*[other] αποφόρτιση
}
search-applets = Αναζήτηση μικροεφαρμογών...
pinch-to-zoom = Τσίμπημα για ζουμ
.desc = Χρησιμοποιήστε δύο δάχτυλα για να κάνετε ζουμ στο περιεχόμενο, σε εφαρμογές που το υποστηρίζουν
wifi = Wi-Fi
.adapter = Προσαρμογέας Wi-Fi { $id }
.forget = Διαγραφή αυτού του δικτύου
x-hours =
{ $number } { $number ->
[one] ώρα
*[other] ώρες
}
added = Προστέθηκε
input-source-switch = Εναλλαγή πηγών εισόδου γλώσσας πληκτρολογίου
focus-navigation = Πλοήγηση εστίασης
.focus-follows-cursor = Η εστίαση ακολουθεί τον κέρσορα
.focus-follows-cursor-delay = Η εστίαση ακολουθεί την καθυστέρηση του κέρσορα σε ms
.cursor-follows-focus = Ο κέρσορας ακολουθεί την εστίαση
full-name = Ονοματεπώνυμο
app-background = Φόντο παραθύρων
keyboard-sources = Πηγές εισόδου
.desc = Μπορείτε να αλλάξετε τις πηγές εισόδου με τον συνδυασμό πλήκτρων Super+Space, τον οποίο μπορείτε να προσαρμόσετε στις ρυθμίσεις συντομεύσεων πληκτρολογίου.
.move-up = Μετακίνηση πάνω
.move-down = Μετακίνηση κάτω
.settings = Ρυθμίσεις
.view-layout = Προβολή διάταξης πληκτρολογίου
.remove = Αφαίρεση
.add = Προσθήκη πηγής εισόδου
nav-shortcuts = Πλοήγηση
.prev-output = Εστίαση στην προηγούμενη έξοδο
.next-output = Εστίαση στην επόμενη έξοδο
.last-workspace = Εστίαση στον τελευταίο χώρο εργασίας
.prev-workspace = Εστίαση στον προηγούμενο χώρο εργασίας
.next-workspace = Εστίαση στον επόμενο χώρο εργασίας
.focus =
Εστίαση κατά ένα παράθυρο { $direction ->
*[down] προς τα κάτω
[in] προς τα μέσα
[left] προς τα αριστερά
[out] προς τα έξω
[right] προς τα δεξιά
[up] προς τα πάνω
}
.output =
Εναλλαγή { $direction ->
*[down] στην κάτω έξοδο
[left] στην αριστερή έξοδο
[right] στη δεξιά έξοδο
[up] στην πάνω έξοδο
}
.workspace = Μετάβαση στον χώρο εργασίας { $num }
hearing = Ακοή
.mono = Αναπαραγωγή στερεοφωνικού ήχου ως μονοφωνικού
preferred-languages = Προτιμώμενες γλώσσες
.desc = Η σειρά των γλωσσών καθορίζει ποια γλώσσα θα χρησιμοποιείται για το περιβάλλον χρήστη. Οι αλλαγές θα εφαρμοστούν στην επόμενη σύνδεση.
panel-style = Στυλ
.anchor-gap = Κενό ανάμεσα στη γραμμή συστήματος και στις άκρες της οθόνης
.dock-anchor-gap = Κενό ανάμεσα στην μπάρα εφαρμογών και στις άκρες της οθόνης
.extend = Επέκταση της γραμμής συστήματος στις άκρες της οθόνης
.dock-extend = Επέκταση της μπάρας εφαρμογών στις άκρες της οθόνης
.appearance = Εμφάνιση
.size = Μέγεθος
.background-opacity = Αδιαφάνεια φόντου
panel-appearance = Εμφάνιση
.match = Συμφωνία με την επιφάνεια εργασίας
.light = Ανοιχτόχρωμη
.dark = Σκουρόχρωμη
workspaces-multi-behavior = Συμπεριφορά πολλαπλών οθονών
.span = Κοινοί χώροι εργασίας σε όλες τις οθόνες
.separate = Ξεχωριστοί χώροι εργασίας ανά οθόνη
replace-shortcut-dialog = Αντικατάσταση συντόμευσης;
.desc = Η συντόμευση { $shortcut } χρησιμοποιείται από το { $name }. Εάν την αντικαταστήσετε, το { $name } θα απενεργοποιηθεί.
about-device = Όνομα συσκευής
.desc = Αυτό το όνομα εμφανίζεται σε άλλα δίκτυα ή συσκευές Bluetooth
no-vpn = Δεν διατίθενται συνδέσεις VPN.
start-segment = Αρχικό τμήμα
reset-to-default = Επαναφορά προεπιλογών
enable-export = Εφαρμογή τρέχοντος θέματος σε εφαρμογές GNOME
.desc = Δεν υποστηρίζουν όλα τα σύνολα εργαλείων την αυτόματη εναλλαγή. Ενδέχεται να χρειαστεί επανεκκίνηση των μη-COSMIC εφαρμογών μετά από αλλαγή του θέματος.
shortcut-name = Όνομα συντόμευσης
style = Στυλ
.round = Στρογγυλό
.slightly-round = Ελαφρώς στρογγυλό
.square = Τετράγωνο
.frosted-glass = Ημιδιαφανές γυαλί
.frosted-system-interface = Περιβάλλον συστήματος
.frosted-system-interface-desc = Εφαρμόζει το εφέ ημιδιαφανούς γυαλιού στο μενού εκκίνησης, τη βιβλιοθήκη εφαρμογών, την επισκόπηση χώρων εργασίας και τα στοιχεία ελέγχου στην οθόνη
.frosted-windows = Παράθυρα
.frosted-windows-desc = Εφαρμόζει το εφέ ημιδιαφανούς γυαλιού στα παράθυρα και τους διαλόγους
.frosted-panels = Γραμμές συστήματος
.frosted-panels-desc = Εφαρμόζει το εφέ ημιδιαφανούς γυαλιού στη γραμμή συστήματος και την μπάρα εφαρμογών
.frosted-applets = Μικροεφαρμογές
.frosted-applets-desc = Εφαρμόζει το εφέ ημιδιαφανούς γυαλιού στα μενού των μικροεφαρμογών στη γραμμή συστήματος και την μπάρα εφαρμογών
.frosted-thickness = Βαθμός θολότητας
.less = λίγο
.more = πολύ
.glass-opacity = Διαφάνεια γυαλιού
panel-applets = Διαμόρφωση
.dock-desc = Διαμόρφωση των μικροεφαρμογών της μπάρας εφαρμογών
.desc = Διαμόρφωση των μικροεφαρμογών της γραμμής συστήματος
qr-code-unavailable = Δεν διατίθεται κωδικός QR
window-tiling = Παράθεση παραθύρων
.horizontal = Ορισμός οριζόντιου προσανατολισμού
.vertical = Ορισμός κάθετου προσανατολισμού
.swap-window = Εναλλαγή παραθύρου
.toggle-tiling = Εναλλαγή παράθεσης παραθύρων
.toggle-stacking = Εναλλαγή στοίβαξης παραθύρων
.toggle-floating = Εναλλαγή αιώρησης παραθύρων
.toggle-orientation = Εναλλαγή προσανατολισμού
change-password = Αλλαγή κωδικού πρόσβασης
disable-while-typing = Απενεργοποίηση κατά την πληκτρολόγηση
long = Μεγάλη
copied-to-clipboard = Αντιγράφηκε στο πρόχειρο
bluetooth-confirm-pin = Επιβεβαίωση PIN για σύνδεση Bluetooth
.description = Επιβεβαιώστε ότι το ακόλουθο PIN είναι ίδιο με αυτό που εμφανίζεται στο { $device }
legacy-app-scaling = Κλιμάκωση εφαρμογών στο σύστημα παραθύρων X11
.scaled-gaming = Βελτιστοποίηση για παιχνίδια και εφαρμογές πλήρους οθόνης
.gaming-description = Οι εφαρμογές σε X11 ενδέχεται να εμφανίζονται ελαφρώς μεγαλύτερες/μικρότερες σε σύγκριση με τις εφαρμογές σε Wayland
.scaled-applications = Βελτιστοποίηση για εφαρμογές
.applications-description = Τα παιχνίδια και οι εφαρμογές πλήρους οθόνης σε X11 ενδέχεται να μην αντιστοιχούν στην ανάλυση της οθόνης σας
.scaled-compatibility = Λειτουργία μέγιστης συμβατότητας
.compatibility-description = Οι εφαρμογές σε X11 ενδέχεται να εμφανίζονται θολές σε οθόνες HiDPI
.preferred-display = Προτιμώμενη οθόνη για παιχνίδια και εφαρμογές πλήρους οθόνης σε X11
.no-display = Καμία
night-light = Νυχτερινός φωτισμός
.auto = Αυτόματα (από τη δύση έως την ανατολή)
.desc = Μείωση του μπλε φωτός με θερμότερα χρώματα
custom-shortcuts = Προσαρμοσμένες συντομεύσεις
.add = Προσθήκη συντόμευσης
.context = Προσθήκη προσαρμοσμένης συντόμευσης
.none = Δεν υπάρχουν προσαρμοσμένες συντομεύσεις
visible-networks = Ορατά δίκτυα
window-hint-accent-toggle = Χρήση του χρώματος έμφασης για την ένδειξη ενεργού παραθύρου
type-to-search = Πληκτρολογήστε για αναζήτηση...
workspaces-behavior = Συμπεριφορά χώρων εργασίας
.dynamic = Δυναμικοί χώροι εργασίας
.dynamic-desc = Αυτόματη κατάργηση των κενών χώρων εργασίας.
.fixed = Σταθερός αριθμός χώρων εργασίας
.fixed-desc = Προσθήκη ή κατάργηση χώρων εργασίας στην επισκόπηση.
wireguard-dialog = Προσθήκη συσκευής WireGuard
.description = Επιλέξτε ένα όνομα συσκευής για τη διαμόρφωση του WireGuard.
remove-connection-dialog = Αφαίρεση προφίλ σύνδεσης;
.vpn-description = Θα χρειαστεί να εισαγάγετε ξανά τον κωδικό πρόσβασης για να χρησιμοποιήσετε αυτό το δίκτυο στο μέλλον.
.wired-description = Θα χρειαστεί να δημιουργήσετε εκ νέου αυτό το προφίλ για να το χρησιμοποιήσετε στο μέλλον.
click-behavior = Συμπεριφορά κλικ
.click-finger = Δευτερεύον κλικ με δύο δάχτυλα και μεσαίο κλικ με τρία δάχτυλα
.button-areas = Δευτερεύον κλικ στην κάτω δεξιά γωνία και μεσαίο κλικ στο κάτω κεντρικό μέρος
move-windows = Μετακίνηση παραθύρων
.direction =
Μετακίνηση παραθύρου { $direction ->
*[down] προς τα κάτω
[left] προς τα αριστερά
[right] προς τα δεξιά
[up] προς τα πάνω
}
.display =
Μετακίνηση παραθύρου κατά μία οθόνη { $direction ->
*[down] προς τα κάτω
[left] προς τα αριστερά
[right] προς τα δεξιά
[up] προς τα πάνω
}
.workspace =
Μετακίνηση παραθύρου κατά έναν χώρο εργασίας { $direction ->
*[below] προς τα κάτω
[left] προς τα αριστερά
[right] προς τα δεξιά
[above] προς τα πάνω
}
.workspace-num = Μετακίνηση παραθύρου στον χώρο εργασίας { $num }
.prev-workspace = Μετακίνηση παραθύρου στον προηγούμενο χώρο εργασίας
.next-workspace = Μετακίνηση παραθύρου στον επόμενο χώρο εργασίας
.last-workspace = Μετακίνηση παραθύρου στον τελευταίο χώρο εργασίας
.next-display = Μετακίνηση παραθύρου στην επόμενη οθόνη
.prev-display = Μετακίνηση παραθύρου στην προηγούμενη οθόνη
.send-to-prev-workspace = Μετακίνηση παραθύρου στον προηγούμενο χώρο εργασίας
.send-to-next-workspace = Μετακίνηση παραθύρου στον επόμενο χώρο εργασίας
switch-workspaces = Εναλλαγή χώρων εργασίας
.horizontal = Ολίσθηση με τέσσερα δάχτυλα προς τα αριστερά/δεξιά
.vertical = Ολίσθηση με τέσσερα δάχτυλα προς τα πάνω/κάτω
system-controls = Έλεγχος συστήματος
auto-switch = Αυτόματη εναλλαγή μεταξύ ανοιχτόχρωμης και σκουρόχρωμης λειτουργίας
.sunrise = Εναλλαγή στην ανοιχτόχρωμη λειτουργία την ανατολή
.sunset = Εναλλαγή στη σκουρόχρωμη λειτουργία τη δύση
.next-sunrise = Εναλλαγή στην ανοιχτόχρωμη λειτουργία στην επόμενη ανατολή
.next-sunset = Εναλλαγή στην ανοιχτόχρωμη λειτουργία στην επόμενη δύση
add-vpn = Προσθήκη VPN
keyboard-numlock-boot = Πλήκτρο Numlock
.boot-state = Κατάσταση κατά την εκκίνηση
.last-boot = Τελευταία εκκίνηση
.on = Ενεργό
.off = Ανενεργό
.set = Ορισμός κατάστασης Numlock κατά την εκκίνηση
about-related = Σχετικές ρυθμίσεις
.support = Λήψη υποστήριξης
tap-to-click = Πάτημα για κλικ
.desc = Ενεργοποιεί το πάτημα με ένα δάχτυλο για κύριο κλικ, με δύο δάχτυλα για δευτερεύον κλικ και με τρία δάχτυλα για μεσαίο κλικ
interface-density = Πυκνότητα περιβάλλοντος χρήστη
.comfortable = Άνετο
.compact = Πυκνό
.spacious = Ευρύχωρο
open-application-library = Άνοιγμα βιβλιοθήκης εφαρμογών
slow = Αργός
network-name = Όνομα δικτύου
scrolling = Κύλιση
.two-finger = Κύλιση με δύο δάχτυλα
.edge = Κύλιση κατά μήκος της άκρης με ένα δάχτυλο
.speed = Ταχύτητα κύλισης
.natural = Φυσική κατεύθυνση κύλισης
.natural-desc = Κύλιση του περιεχομένου, αντί της προβολής
auth-dialog = Απαιτείται πιστοποίηση
.vpn-description = Εισαγάγετε το όνομα χρήστη και τον κωδικό πρόσβασης που απαιτούνται από την υπηρεσία VPN.
.wifi-description = Εισαγάγετε τον κωδικό πρόσβασης ή το κλειδί κρυπτογράφησης. Μπορείτε επίσης να συνδεθείτε πατώντας το κουμπί «WPS» του δρομολογητή.
add-user = Προσθήκη χρήστη
add-image = Προσθήκη εικόνας
migrate-workspace-next = Μεταφορά χώρου εργασίας στην επόμενη έξοδο
airplane-on = Η λειτουργία πτήσης είναι ενεργή.
formatting = Μορφοποίηση
.dates = Ημερομηνίες
.time = Ώρα
.date-and-time = Ημερομηνία και ώρα
.numbers = Αριθμοί
.measurement = Μονάδα μέτρησης
.paper = Χαρτί
disabled = Ανενεργό
add-applet = Προσθήκη μικροεφαρμογής
show-extended-input-sources = Εμφάνιση εκτεταμένων πηγών εισόδου
mode-and-colors = Λειτουργία και χρώματα
forget = Διαγραφή
center-segment = Κεντρικό τμήμα
panel-missing = Απουσία διαμόρφωσης γραμμής συστήματος
.desc = Το αρχείο διαμόρφωσης της γραμμής συστήματος απουσιάζει λόγω της χρήσης μιας προσαρμοσμένης διαμόρφωσης ή είναι κατεστραμμένο.
.fix = Επαναφορά προεπιλογής
invalid-username = Μη έγκυρο όνομα χρήστη
mirroring = Κατοπτρισμός
.id = Κατοπτρισμός { $id }
.dont = Να μην γίνει κατοπτρισμός
.mirror = Κατοπτρισμός { $display }
.project =
Προβολή σε { $display ->
[all] όλες τις οθόνες
*[other] { $display }
}
.project-count =
Προβολή σε { $count } { $count ->
[1] άλλη οθόνη
*[other] άλλες οθόνες
}
orientation = Προσανατολισμός
.standard = Τυπικός
.rotate-90 = Περιστροφή 90°
.rotate-180 = Περιστροφή 180°
.rotate-270 = Περιστροφή 270°
auto = Αυτόματο
end-segment = Τελικό τμήμα
bluetooth-adapters = Προσαρμογείς Bluetooth
time-format = Μορφή ημερομηνίας και ώρας
.twenty-four = 24ωρη μορφή
.show-seconds = Εμφάνιση δευτερολέπτων
.first = Πρώτη ημέρα εβδομάδας
.show-date = Εμφάνιση ημερομηνίας στη μικροεφαρμογή ώρας
.friday = Παρασκευή
.saturday = Σάββατο
.sunday = Κυριακή
.monday = Δευτέρα
accent-color = Χρώμα έμφασης
type-key-combination = Εισαγάγετε τον συνδυασμό πλήκτρων
manage-windows = Διαχείριση παραθύρων
.close = Κλείσιμο παραθύρου
.maximize = Μεγιστοποίηση παραθύρου
.fullscreen = Παράθυρο σε πλήρη οθόνη
.minimize = Ελαχιστοποίηση παραθύρου
.resize-inwards = Αλλαγή μεγέθους παραθύρου προς τα μέσα
.resize-outwards = Αλλαγή μεγέθους παραθύρου προς τα έξω
.toggle-sticky = Εναλλαγή καρφιτσωμένου παραθύρου
install-additional-languages = Εγκατάσταση πρόσθετων γλωσσών
sound-output = Έξοδος ήχου
.volume = Ένταση εξόδου
.device = Συσκευή εξόδου
.level = Επίπεδο εξόδου
.config = Διαμόρφωση
.balance = Ισορροπία
.left = Αριστερό
.right = Δεξί
window-controls = Στοιχεία ελέγχου παραθύρων
.maximize = Εμφάνιση κουμπιού μεγιστοποίησης
.minimize = Εμφάνιση κουμπιού ελαχιστοποίησης
.active-window-hint = Εμφάνιση ένδειξης ενεργού παραθύρου
window-management-appearance = Διαχείριση παραθύρων
.active-hint = Μέγεθος ένδειξης ενεργού παραθύρου
.gaps = Κενά γύρω από τα παράθυρα σε παράθεση
migrate-workspace-prev = Μεταφορά χώρου εργασίας στην προηγούμενη έξοδο
legacy-app-global-shortcuts = Καθολικές συντομεύσεις σε εφαρμογές X11
.desc = Οι καθολικές συντομεύσεις επιτρέπουν σε πατήματα πλήκτρων και συμβάντα κουμπιών ποντικιού που εκτελούνται στις εφαρμογές να αναγνωρίζονται από άλλες εφαρμογές για λειτουργίες, όπως πάτημα για ομιλία ή πάτημα για σίγαση. Από προεπιλογή, οι καθολικές συντομεύσεις είναι απενεργοποιημένες στις εφαρμογές X11 για να διασφαλιστεί ότι οι άλλες εφαρμογές δεν μπορούν να παρακολουθούν τα συμβάντα πληκτρολογίου και ποντικιού που περιέχουν ευαίσθητες πληροφορίες.
.none = Κανένα πλήκτρο
.modifiers = Πλήκτρα τροποποίησης (Super, Shift, Control, Alt)
.combination = Όλα τα πλήκτρα, ενώ είναι πατημένα τα Super, Control ή Alt
.all = Όλα τα πλήκτρα
.mouse = Συμβάντα κουμπιών ποντικιού σε εφαρμογές X11
power-saving = Επιλογές εξοικονόμησης ενέργειας
.turn-off-screen-after = Απενεργοποίηση οθόνης μετά από
.auto-suspend = Αυτόματη αναστολή
.auto-suspend-ac = Αυτόματη αναστολή με τροφοδοσία ρεύματος
.auto-suspend-battery = Αυτόματη αναστολή με τροφοδοσία μπαταρίας
connected-devices = Συνδεδεμένες συσκευές
.unknown = Άγνωστη συσκευή
icons-and-toolkit = Εικονίδια και θέματα συνόλου εργαλείων
sound-applications = Εφαρμογές
.desc = Ένταση και ρυθμίσεις ήχου εφαρμογών
.none = Καμία εφαρμογή δεν χρησιμοποιεί ήχο
time-zone = Ζώνη ώρας
.auto = Αυτόματη ζώνη ώρας
.auto-info = Απαιτεί τη χρήση υπηρεσιών τοποθεσίας και πρόσβαση στο διαδίκτυο
fast = Γρήγορος
add-color = Προσθήκη χρώματος
add-network = Προσθήκη δικτύου
.profile = Προσθήκη προφίλ
network-device-state =
.activated = Συνδεδεμένο
.config = Σύνδεση
.deactivating = Αποσύνδεση
.disconnected = Αποσυνδεδεμένο
.failed = Αποτυχία σύνδεσης
.ip-check = Έλεγχος σύνδεσης
.ip-config = Αίτημα για διεύθυνση IP και πληροφορίες δρομολόγησης
.need-auth = Απαιτεί πιστοποίηση
.prepare = Προετοιμασία για σύνδεση
.secondaries = Αναμονή για δευτερεύουσα σύνδεση
.unavailable = Μη διαθέσιμο
.unknown = Άγνωστη κατάσταση
.unmanaged = Χωρίς διαχείριση
.unplugged = Αποσυνδεδεμένο καλώδιο
gestures = Χειρονομίες
.four-finger-down = Ολίσθηση με τέσσερα δάχτυλα προς τα κάτω
.four-finger-left = Ολίσθηση με τέσσερα δάχτυλα προς τα αριστερά
.four-finger-right = Ολίσθηση με τέσσερα δάχτυλα προς τα δεξιά
.four-finger-up = Ολίσθηση με τέσσερα δάχτυλα προς τα πάνω
.three-finger-any = Ολίσθηση με τρία δάχτυλα προς κάθε κατεύθυνση
system = Σύστημα και λογαριασμοί
no-networks = Δεν βρέθηκαν δίκτυα.
open-new-folder = Άνοιγμα νέου φακέλου
fill = Συμπλήρωση
custom = Προσαρμοσμένες
bluetooth-paired = Παλαιότερες συνδεδεμένες συσκευές
.connect = Σύνδεση
.battery = { $percentage }% μπαταρία
dialog = Παράθυρο διαλόγου
.title = Διατήρηση αυτών των ρυθμίσεων οθόνης;
.keep-changes = Διατήρηση αλλαγών
.change-prompt = Οι αλλαγές των ρυθμίσεων θα αναιρεθούν αυτόματα σε { $time } δευτερόλεπτα.
.revert-settings = Επαναφορά ρυθμίσεων
workspaces-overview-thumbnails = Μικρογραφίες επισκόπησης χώρων εργασίας
.show-number = Εμφάνιση αριθμού χώρων εργασίας
.show-name = Εμφάνιση ονόματος χώρων εργασίας
color = Χρώμα
.depth = Βάθος χρωμάτων
.profile = Προφίλ χρωμάτων
.sidebar = Προφίλ χρωμάτων
.temperature = Θερμοκρασία χρωμάτων
hex = Δεκαεξαδικό
magnifier = Μεγεθυντικός φακός
.controls =
Ή χρησιμοποιήστε αυτές τις συντομεύσεις: { $zoom_in ->
[zero] { "" }
*[other]
{ "" }
{ $zoom_in } για μεγέθυνση,
}{ $zoom_out ->
[zero] { "" }
*[other]
{ "" }
{ $zoom_out } για σμίκρυνση,
}
Super + κύλιση με το ποντίκι
.scroll_controls = Ενεργοποίηση ζουμ ποντικιού ή επιφάνειας αφής με Super + κύλιση
.show_overlay = Εμφάνιση της επικάλυψης μεγεθυντικού φακού
.increment = Αύξηση ζουμ
.signin = Έναρξη μεγεθυντικού φακού κατά τη σύνδεση
.applet = Εναλλαγή μεγεθυντικού φακού στη γραμμή συστήματος
.movement = Η ζουμαρισμένη προβολή μετακινείται
.continuous = Συνέχεια με τον κέρσορα
.onedge = Όταν ο κέρσορας φτάνει στην άκρη
.centered = Για να διατηρεί τον κέρσορα στο κέντρο
administrator = Διαχειριστής
.desc = Οι διαχειριστές μπορούν να αλλάζουν ρυθμίσεις για όλους τους χρήστες, να προσθέτουν και να καταργούν άλλους χρήστες
scan-to-connect-description = Σαρώστε τον κωδικό QR για να συνδεθείτε σε αυτό το δίκτυο.
modified = { $count } τροποποιήσεις
sound-usb-audio = Ήχος USB
sound-device-profiles = Προφίλ συσκευών
sound-hd-audio = Ήχος HD
shadow-and-corners = Σκίαση και γωνίες παραθύρων
shadows-tiling = Παράθεση παραθύρων
.clip = Συμφωνία με τις γωνίες συστήματος
.shadow = Εφαρμογή σκιών
shadows-floating = Αιωρούμενα παράθυρα
.clip = Συμφωνία με τις γωνίες συστήματος και εφαρμογή σκιών
sound-device-port-unplugged = Αποσυνδεδεμένο
workspaces-overview = Επισκόπηση χώρων εργασίας
.action-on-typing = Ενέργεια κατά την πληκτρολόγηση
.none = Καμία
.launcher = Άνοιγμα μενού εκκίνησης
.applications = Άνοιγμα εφαρμογών
time-region = Περιοχή και γλώσσα
power = Ενέργεια και μπαταρία
xdg-entry-wired = Ενσύρματη δικτύωση
xdg-entry-startup-apps-comment = Διαμόρφωση των εφαρμογών που εκτελούνται κατά τη σύνδεση
xdg-entry-region-language = Περιοχή και γλώσσα
time-date = Ημερομηνία και ώρα
.auto = Αυτόματη ρύθμιση
.auto-ntp = Η ημερομηνία και η ώρα θα ενημερώνονται αυτόματα όταν οριστεί ζώνη ώρας
xdg-entry-mouse-keywords = Ποντίκι;Επιτάχυνση;Κύλιση;COSMIC;Mouse;Acceleration;Scrolling;
xdg-entry-notifications-keywords = Ειδοποίηση;Κλείδωμα;COSMIC;Notification;Lock;
xdg-entry-startup-apps-keywords = Εφαρμογή;Εκκίνηση;COSMIC;Startup;Application;
xdg-entry-default-apps-comment = Προεπιλεγμένο πρόγραμμα περιήγησης ιστού, πρόγραμμα ηλεκτρονικού ταχυδρομείου, εφαρμογή περιήγησης αρχείων και άλλες εφαρμογές
xdg-entry-keyboard-comment = Πηγές εισόδου, εναλλαγή, εισαγωγή ειδικών χαρακτήρων, συντομεύσεις
xdg-entry-wired-keywords = Σύνδεση;Δίκτυο;Ενσύρματο;COSMIC;Wired;LAN;Network;Connection;
xdg-entry-power = Ενέργεια και μπαταρία
xdg-entry-dock-keywords = Μικροεφαρμογή;Μπάρα;Εφαρμογών;Γραμμή;Συστήματος;COSMIC;Dock;Panel;Applet
bluetooth = Bluetooth
.status = Αυτό το σύστημα είναι ορατό ως { $aliases } όσο είναι ανοικτές οι ρυθμίσεις του Bluetooth.
.connected = Συνδεδεμένο
.connecting = Σύνδεση
.disconnecting = Αποσύνδεση
.connect = Σύνδεση
.disconnect = Αποσύνδεση
.forget = Διαγραφή
.dbus-error = Προέκυψε σφάλμα κατά την αλληλεπίδραση με το DBus: { $why }
.disabled = Η υπηρεσία Bluetooth είναι απενεργοποιημένη
.inactive = Η υπηρεσία Bluetooth δεν είναι ενεργή
.unknown = Δεν ήταν δυνατή η ενεργοποίηση της υπηρεσίας Bluetooth. Είναι εγκατεστημένο το BlueZ;
xdg-entry-mouse-comment = Ταχύτητα ποντικιού, επιτάχυνση και φυσική κατεύθυνση κύλισης
xdg-entry-system = Σύστημα και λογαριασμοί
xdg-entry-panel-keywords = Μικροεφαρμογή;Γραμμή;Συστήματος;COSMIC;Panel;Applet
xdg-entry-window-management = Διαχείριση παραθύρων
xdg-entry-time-language-comment = Διαχείριση ημερομηνίας, ώρας, περιοχής και γλώσσας συστήματος
default-apps = Προεπιλεγμένες εφαρμογές
.web-browser = Περιήγηση ιστού
.file-manager = Διαχείριση αρχείων
.mail-client = Ηλεκτρονικό ταχυδρομείο
.music = Μουσική
.video = Βίντεο
.photos = Φωτογραφίες
.calendar = Ημερολόγιο
.terminal = Τερματικό
.other-associations = Άλλες συσχετίσεις
.text-editor = Επεξεργασία κειμένου
.not-installed = Δεν έχει εγκατασταθεί
xdg-entry-x11-applications-keywords = Εφαρμογή;Παιχνίδι;Συμβατότητα;COSMIC;X11;Application;Game;Compatibility;
xdg-entry-displays-comment = Διαχείριση ρυθμίσεων διαμόρφωσης οθονών
xdg-entry-touchpad-keywords = Επιφάνεια;Αφή;Χειρονομία;COSMIC;Touchpad;Gesture;
xdg-entry-time-language = Ώρα και γλώσσα
window-management = Διαχείριση παραθύρων
xdg-entry-system-keywords = Σύστημα;Πληροφορίες;Λογαριασμοί;Υλικολογισμικό;COSMIC;System;Info;Accounts;Firmware;
xdg-entry-wireless-keywords = Σύνδεση;Δίκτυο;COSMIC;WiFi;Wi-Fi;Network;Connection;
mouse = Ποντίκι
.speed = Ταχύτητα ποντικιού
.acceleration = Ενεργοποίηση επιτάχυνσης ποντικιού
dock = Μπάρα εφαρμογών
xdg-entry-panel = Γραμμή συστήματος
display = Οθόνες
.arrangement = Διάταξη οθονών
.arrangement-desc = Σύρετε τις οθόνες για να τις αναδιατάξετε
.enable = Ενεργοποίηση οθόνης
.external = Εξωτερική οθόνη { $size } { $output }
.laptop = Οθόνη φορητού υπολογιστή { $size }
.options = Επιλογές οθόνης
.refresh-rate = Ρυθμός ανανέωσης
.resolution = Ανάλυση
.scale = Κλίμακα
.additional-scale-options = Πρόσθετες επιλογές κλίμακας
xdg-entry-wireless-comment = Συνδέσεις Wi-Fi και προφίλ σύνδεσης
xdg-entry-desktop-comment = Ρυθμίσεις ταπετσαρίας, εμφάνισης, γραμμής συστήματος, μπάρας εφαρμογών, διαχείρισης παραθύρων και χώρων εργασίας
xdg-entry-network = Δίκτυα και ασύρματη σύνδεση
xdg-entry-users-comment = Πιστοποίηση και λογαριασμοί χρηστών
startup-apps = Εφαρμογές εκκίνησης
.add = Προσθήκη εφαρμογής
.user = Εφαρμογές που εκκινούνται όταν κάνετε σύνδεση
.none = Δεν έχουν προστεθεί εφαρμογές εκκίνησης
.remove-dialog-title = Αφαίρεση του «{ $name }»;
.remove-dialog-description = Αφαίρεση αυτής της εφαρμογής εκκίνησης;
.add-startup-app = Προσθήκη εφαρμογής εκκίνησης
xdg-entry-input-comment = Ρυθμίσεις πληκτρολογίου και ποντικιού
input-devices = Συσκευές εισόδου
xdg-entry-dock-comment = Μια προαιρετική μπάρα για εφαρμογές και μικροεφαρμογές
xdg-entry-network-keywords = Δίκτυο;Δικτύωση;Ασύρματο;COSMIC;Network;Wireless;WiFi;VPN;
xdg-entry-wallpaper-comment = Εικόνες ταπετσαρίας, χρώματα και επιλογές παρουσίασης διαφανειών
xdg-entry-dock = Μπάρα εφαρμογών
xdg-entry-startup-apps = Εφαρμογές εκκίνησης
xdg-entry-workspaces-keywords = Χώρος;Εργασίας;Προσανατολισμός;Επισκόπηση;Οθόνη;COSMIC;Workspace;Orientation;Overview;Monitor;
xdg-entry-system-comment = Πληροφορίες συστήματος, λογαριασμοί και ενημερώσεις υλικολογισμικού
xdg-entry-x11-applications-comment = Κλιμάκωση εφαρμογών συστήματος παραθύρων X11, κύρια οθόνη και καθολικές συντομεύσεις
xdg-entry-region-language-comment = Μορφοποίηση ημερομηνιών, ώρας και αριθμών με βάση την περιοχή σας
panel = Γραμμή συστήματος
xdg-entry-wallpaper-keywords = Ταπετσαρία;Φόντο;Παρουσίαση;Διαφανειών;COSMIC;Wallpaper;Background;Slideshow;
xdg-entry-users-keywords = Λογαριασμός;Χρήστης;COSMIC;User;Account;
xdg-entry-vpn-keywords = Δίκτυο;Σύνδεση;COSMIC;VPN;Network;Connection;OpenVPN;OpenConnect;
users = Χρήστες
.admin = Διαχειριστής
.standard = Τυπικός
.profile-add = Επιλογή εικόνας προφίλ
xdg-entry-time-language-keywords = Σύστημα;Ώρα;Ημερομηνία;Περιοχή;Γλώσσα;COSMIC;System;Time;Date;Region;Language;
xdg-entry-input-keywords = Εισαγωγή;Είσοδος;Πληκτρολόγιο;Ποντίκι;Ποντίκια;COSMIC;Input;Keyboard;Mouse;Mice;
xdg-entry-notifications-comment = Λειτουργία «Μην ενοχλείτε», ειδοποιήσεις οθόνης κλειδώματος και ρυθμίσεις ανά εφαρμογή
xdg-entry-sound-keywords = Ήχος;Ειδοποίηση;COSMIC;Sound;Audio;Alert;Pipewire;
xdg-entry-power-keywords = Μπαταρία;Ισχύς;Ενέργεια;COSMIC;Power;Battery
legacy-applications = Συμβατότητα εφαρμογών X11
xdg-entry-region-language-keywords = Περιοχή;Γλώσσα;Ημερομηνία;Μορφή;Ώρα;Τοπικές;Τοπικοποίηση;COSMIC;Region;Language;Date;Format;Time;Locale;Localization;
xdg-entry-applications-keywords = Προεπιλογή;Προεπιλεγμένες;Εφαρμογές;Εφαρμογή;Εκκίνηση;Συμβατότητα;COSMIC;Default;Application;Startup;X11;Compatibility
xdg-entry-sound-comment = Ρυθμίσεις ήχου για συσκευές, ειδοποιήσεις και εφαρμογές
wallpaper = Ταπετσαρία
.change = Αλλαγή εικόνας κάθε
.fit = Προσαρμογή ταπετσαρίας
.folder-dialog = Επιλογή φακέλου ταπετσαριών
.image-dialog = Επιλογή εικόνας ταπετσαρίας
.plural = Ταπετσαρίες
.same = Κοινή ταπετσαρία για όλες τις οθόνες
.slide = Παρουσίαση διαφανειών
xdg-entry-touchpad-comment = Ταχύτητα επιφάνειας αφής, επιλογές κλικ και χειρονομίες
xdg-entry-keyboard-keywords = Πληκτρολόγιο;Είσοδος;Πηγή;Συντομεύσεις;COSMIC;Keyboard;Input;Source;Shortcuts;
xdg-entry-vpn-comment = Συνδέσεις VPN και προφίλ σύνδεσης
xdg-entry-window-management-keywords = Παράθυρο;Διαχείριση;Παράθεση;Πλήκτρο;COSMIC;Window;Management;Tiling;Super;Key;
xdg-entry-workspaces-comment = Προσανατολισμός χώρων εργασίας, επισκόπηση και συμπεριφορά πολλαπλών οθονών
xdg-entry-network-comment = Διαχείριση συνδέσεων δικτύου
xdg-entry-panel-comment = Η κύρια γραμμή του συστήματος για μενού και μικροεφαρμογές
touchpad = Επιφάνεια αφής
.acceleration = Ενεργοποίηση επιτάχυνσης επιφάνειας αφής
.speed = Ταχύτητα επιφάνειας αφής
xdg-entry-power-comment = Λειτουργίες ενέργειας και επιλογές εξοικονόμησης ενέργειας
time = Ώρα και γλώσσα
xdg-entry-x11-applications = Συμβατότητα εφαρμογών X11
xdg-entry-window-management-comment = Ενέργεια πλήκτρου Super, επιλογές ελέγχου παραθύρων και πρόσθετες επιλογές παράθεσης παραθύρων
xdg-entry-wired-comment = Ενσύρματες συνδέσεις και προφίλ σύνδεσης
known-networks = Γνωστά δίκτυα
network-and-wireless = Δίκτυα και ασύρματη δικτύωση
keyboard-special-char = Εισαγωγή ειδικών χαρακτήρων
.alternate = Πλήκτρο εναλλακτικών χαρακτήρων
.compose = Πλήκτρο σύνθεσης
.compose-desc = Το πλήκτρο σύνθεσης επιτρέπει την εισαγωγή μιας ευρείας ποικιλίας χαρακτήρων. Για να το χρησιμοποιήσετε, πατήστε το πλήκτρο σύνθεσης και έπειτα, μια ακολουθία χαρακτήρων. Για παράδειγμα, μπορείτε να εισαγάγετε τον χαρακτήρα «©» πατώντας το πλήκτρο σύνθεσης, ακολουθούμενο από τα «C» και «o», ή τον χαρακτήρα «á» πατώντας το πλήκτρο σύνθεσης, ακολουθούμενο από τα «a» και «‘» .
.caps = Πλήκτρο Caps Lock
.ctrl = Control
.ctrl-right = Δεξί Ctrl
.swap-with-ctrl = Εναλλαγή με Control
.alt = Alt
.alt-left = Αριστερό Alt
.alt-right = Δεξί Alt
.super = Super
.super-left = Αριστερό Super
.super-right = Δεξί Super
.menu = Πλήκτρο μενού
.backspace = Backspace
.escape = Escape
.swap-with-escape = Εναλλαγή με Escape
.print-screen = Print Screen
.scroll-lock = Scroll Lock
.none = Κανένα
no-search-results = Δεν αντιστοιχεί κανένα δίκτυο στην αναζήτησή σας.
workspaces-navigation = Πλοήγηση
.wraparound = Μετακίνηση μεταξύ του πρώτου και του τελευταίου χώρου εργασίας με συντομεύσεις πληκτρολογίου και χειρονομίες
bluetooth-display-passkey = Σύζευξη Bluetooth
.description = Πληκτρολογήστε το ακόλουθο κλειδί πρόσβασης στο { $device } και πατήστε Enter
bluetooth-display-pin = Σύζευξη Bluetooth
.description = Πληκτρολογήστε το ακόλουθο PIN στο { $device } και πατήστε Enter

View file

@ -0,0 +1,9 @@
save = Save
cancel = Cancel
connected = Connected
confirm = Confirm
replace = Replace
settings = Settings
appearance = Appearance
dark = Dark
light = Light

View file

@ -198,6 +198,8 @@ vpn-error = VPN Error
.connect = Failed to connect to VPN
.connection-editor = Connection editor failed
.connection-settings = Failed to get settings for active connections
.openvpn-config-path = Invalid file path for OpenVPN config
.openvpn-config-path-desc = Chosen file must be on a local file system.
.updating-state = Failed to update network manager state
.wireguard-config-path = Invalid file path for WireGuard config
.wireguard-config-path-desc = Chosen file must be on a local file system.
@ -252,6 +254,12 @@ bluetooth-paired = Previously connected devices
bluetooth-confirm-pin = Confirm Bluetooth PIN
.description = Please confirm that the following PIN matches the one displayed on { $device }
bluetooth-display-passkey = Bluetooth Pairing
.description = Please type the following passkey on { $device }, then press Enter
bluetooth-display-pin = Bluetooth Pairing
.description = Please type the following PIN on { $device }, then press Enter
bluetooth-available = Nearby devices
bluetooth-adapters = Bluetooth adapters
@ -267,6 +275,8 @@ accessibility = Accessibility
.high-contrast = High contrast mode
.invert-colors = Invert colors
.color-filters = Color filters
.shake-cursor = Shake pointer to find it
.shake-cursor-desc = Quickly shake the mouse to temporarily enlarge the pointer
hearing = Hearing
.mono = Play stereo audio as mono
@ -393,7 +403,20 @@ style = Style
.round = Round
.slightly-round = Slightly round
.square = Square
.frosted-glass = Frosted glass
.frosted-system-interface = System interface
.frosted-system-interface-desc = Applies frosted glass to Launcher, application library, workspace overview, on-screen controls
.frosted-windows = Windows
.frosted-windows-desc = Applies frosted glass to windows and dialogs
.frosted-panels = Panels
.frosted-panels-desc = Applies frosted glass to panel and dock
.frosted-applets = Applets
.frosted-applets-desc = Applies frosted glass to applet menus on panel and dock
.frosted-thickness = Frost thickness
.less = less
.more = more
.glass-opacity = Glass opacity
interface-density = Interface density
.comfortable = Comfortable
.compact = Compact
@ -476,7 +499,7 @@ super-key = Super key action
.launcher = Open Launcher
.workspaces = Open Workspaces
.applications = Open Applications
.disable = Disable
.none = None
edge-gravity = Floating windows gravitate to nearby edges
@ -612,6 +635,7 @@ sound-alerts = Alerts
sound-applications = Applications
.desc = Application volumes and settings
.none = No applications are using sound
# No speaker, headphones, or microphone plugged into sound card port
sound-device-port-unplugged = Unplugged
@ -621,6 +645,92 @@ sound-usb-audio = USB Audio
# Profiles for sound card devices
sound-device-profiles = Device profiles
# Printers settings page
printers = Printers
xdg-entry-printers-comment = Manage printers and print jobs
printers-add = Add printer
printers-add-in-progress = Adding printer…
printers-added = Printer “{ $name }” added
printers-cancel-job = Cancel job
printers-color-black = Black
printers-color-cmy = CMY
printers-color-cmyk = CMYK
printers-color-gray = Grayscale
printers-color-rgb = Color
printers-color-rgb-16 = Color (16-bit)
printers-color-rgb-24 = Color (24-bit)
printers-color-rgb-48 = Color (48-bit)
printers-default = Default
printers-details = Printer details
printers-device = Device
printers-device-uri = Device URI
printers-driver = Driver
printers-error = Operation failed: { $error }
printers-ink-black = Black
printers-ink-cyan = Cyan
printers-ink-levels = Ink levels
printers-ink-magenta = Magenta
printers-ink-yellow = Yellow
printers-job-aborted = Aborted
printers-job-canceled = Canceled
printers-job-completed = Completed
printers-job-held = Held
printers-job-pending = Pending
printers-job-processing = Printing
printers-job-stopped = Stopped
printers-job-unknown = Unknown
printers-jobs = Print jobs
printers-loading = Searching for devices…
printers-location = Location
printers-make-default = Set as default
printers-media-type-cardstock = Cardstock
printers-media-type-envelope = Envelope
printers-media-type-labels = Labels
printers-media-type-photographic = Photo paper
printers-media-type-photographic-glossy = Glossy photo paper
printers-media-type-photographic-matte = Matte photo paper
printers-media-type-photographic-satin = Satin photo paper
printers-media-type-stationery = Plain paper
printers-media-type-stationery-coated = Coated paper
printers-media-type-stationery-fine = Fine paper
printers-media-type-stationery-inkjet = Inkjet paper
printers-media-type-stationery-letterhead = Letterhead paper
printers-media-type-stationery-preprint = Preprinted paper
printers-media-type-stationery-recycled = Recycled paper
printers-media-type-transparency = Transparency
printers-name = Name
printers-no-devices = No devices found
printers-no-matching-drivers = No matching drivers
printers-no-printers = No printers configured
printers-no-printers-desc = Printers connected to this computer or discovered on the network will appear here.
printers-not-accepting = Not accepting jobs
printers-option-color = Color mode
printers-option-media = Paper format
printers-option-media-type = Paper type
printers-option-quality = Print quality
printers-option-sides = Two-sided printing
printers-options = Print options
printers-pause = Pause
printers-quality-draft = Draft
printers-quality-high = High
printers-quality-normal = Normal
printers-remove = Remove printer
printers-remove-confirm = Remove this printer?
printers-remove-confirm-button = Remove
printers-remove-confirm-description = “{ $name }” will be removed from this system. Print jobs queued on it will be discarded.
printers-resume = Resume
printers-sides-long-edge = Long edge
printers-sides-one-sided = One-sided
printers-sides-short-edge = Short edge
printers-state = State
printers-state-idle = Idle
printers-state-processing = Printing
printers-state-reasons = State reasons
printers-state-stopped = Paused
printers-state-unknown = Unknown
printers-submit = Add
printers-test-page = Print test page
# Power & Battery settings page
power = Power & battery
@ -705,6 +815,22 @@ keyboard-special-char = Special Character Entry
.compose = Compose key
.compose-desc = The compose key allows a wide variety of characters to be entered. To use it, press compose and then a sequence of characters. For example, compose key followed by C and o will enter ©, while compose key followed by a and ‘ will enter á.
.caps = Caps Lock key
.ctrl = Control
.ctrl-right = Right Ctrl
.swap-with-ctrl = Swap with Control
.alt = Alt
.alt-left = Left Alt
.alt-right = Right Alt
.super = Super
.super-left = Left Super
.super-right = Right Super
.menu = Menu key
.backspace = Backspace
.escape = Escape
.swap-with-escape = Swap with Escape
.print-screen = Print Screen
.scroll-lock = Scroll Lock
.none = None
keyboard-typing-assist = Typing
.repeat-rate = Repeat rate

View file

@ -167,3 +167,5 @@ firmware = فریمور
users = کاربران
.desc = احراز هویت و ورود، قفل صفحه.
save = ذخیره
cancel = لغو

View file

@ -516,6 +516,19 @@ style = Tyyli
.round = Pyöristetty
.slightly-round = Hieman pyöristetty
.square = Nelikulmainen
.frosted-glass = Huurteinen lasi
.frosted-system-interface = Järjestelmän käyttöliittymä
.frosted-system-interface-desc = Toteuttaa huurteisen lasin käynnistimeen, sovelluskirjastoon, työtilan yleiskuvaan ja näytöllä näkyviin säätimiin
.frosted-windows = Ikkunat
.frosted-windows-desc = Toteuttaa huurteisen lasin ikkunoihin ja vahvistusikkunoihin
.frosted-panels = Paneelit
.frosted-panels-desc = Toteuttaa huurteisen lasin paneeliin ja telakkaan
.frosted-applets = Sovelmat
.frosted-applets-desc = Toteuttaa huurteisen lasin sovelmavalikkoihin paneelissa ja telakassa
.frosted-thickness = Huurteen tiheys
.less = vähemmän
.more = enemmän
.glass-opacity = Lasin peittävyys
panel-applets = Asetukset
.dock-desc = Telakkasovelmien asetukset
.desc = Paneelisovelmien asetukset
@ -778,6 +791,8 @@ accessibility = Esteettömyys
.high-contrast = Suurikontrastinen tila
.invert-colors = Käänteiset värit
.color-filters = Värisuodattimet
.shake-cursor = Ravista osoitinta paikantaaksesi sen
.shake-cursor-desc = Ravista nopeasti hiirtä suurentaaksesi osoittimen väliaikaisesti
debug = Vianjäljitys
system-shortcut = Järjestelmä
.app-library = Avaa sovelluskirjasto
@ -818,3 +833,9 @@ shadows-tiling = Asetellut ikkunat
.clip = Sovita järjestelmän kulmiin
.shadow = Käytä varjoja
no-search-results = Hakuasi vastaavia verkkoja ei löytynyt.
workspaces-navigation = Liikkuminen
.wraparound = Siirry ensimmäisen ja viimeisen työtilan välillä käyttäen pikanäppäimiä ja eleitä
bluetooth-display-passkey = Bluetooth-parinmuodostus
.description = Syötä seuraava turva-avain laitteelle { $device }, ja paina sitten Enter
bluetooth-display-pin = Bluetooth-parinmuodostus
.description = Syötä seuraava PIN-koodi laitteelle { $device }, ja paina sitten Enter

View file

@ -70,7 +70,7 @@ add-network = Ajouter un réseau
add-vpn = Ajouter un VPN
airplane-on = Mode avion activé.
cable-unplugged = Câble débranché
connect = Connecter
connect = Se connecter
connected = Connecté
connecting = Connexion…
disconnect = Se déconnecter
@ -117,6 +117,8 @@ vpn-error = Erreur VPN
.connect = Échec de la connexion au VPN
.connection-editor = Échec de l'éditeur de connexion
.connection-settings = Échec de l'obtention des paramètres des connexions actives
.openvpn-config-path = Chemin du fichier de configuration OpenVPN invalide
.openvpn-config-path-desc = Le fichier sélectionné doit se trouver sur un système de fichiers local.
.updating-state = Échec de la mise à jour de l'état du gestionnaire de réseau
.wireguard-config-path = Chemin de fichier invalide pour la configuration WireGuard
.wireguard-config-path-desc = Le fichier choisi doit se trouver sur un système de fichiers local.
@ -233,7 +235,7 @@ container-background = Arrière-plan
.desc = La couleur principale de l'arrière-plan est utilisée pour la barre latérale de navigation, le panneau latéral, les boîtes de dialogue et les widgets similaires
control-tint = Teinte des composants de contrôle
.desc = Utilisé pour les arrière-plans des boutons standards, les entrées de recherche, les entrées de texte et les composants similaires
frosted = Effet de verre sur l'interface système
frosted = Effet de verre dépoli dans l'interface système
.desc = Applique un effet de flou d'arrière-plan au panneau, au dock, aux applets, au lanceur et à la bibliothèque d'applications
enable-export = Appliquer ce thème aux applications GNOME
.desc = Certaines boîtes à outils ne supportent pas le changement automatique de thème. Les applications non-COSMIC peuvent nécessiter un redémarrage après un changement de thème.
@ -242,9 +244,22 @@ icon-theme = Thème d'icônes
text-tint = Teinte du texte de l'interface
.desc = Utilisée pour obtenir des couleurs de texte d'interface offrant un contraste suffisant sur différentes surfaces
style = Style
.round = Rond
.slightly-round = Légèrement rond
.round = Arrondi
.slightly-round = Légèrement arrondi
.square = Carré
.frosted-glass = Verre dépoli
.frosted-system-interface = Interface système
.frosted-system-interface-desc = Applique un effet de verre dépoli au lanceur, à la bibliothèque d’applications, à la vue d’ensemble de l’espace de travail et aux contrôles à l’écran
.frosted-windows = Fenêtres
.frosted-windows-desc = Applique un effet de verre dépoli aux fenêtres et aux boîtes de dialogue
.frosted-panels = Panneaux
.frosted-panels-desc = Applique un effet de verre dépoli au panneau et au dock
.frosted-applets = Applets
.frosted-applets-desc = Applique un effet de verre dépoli aux menus des applets du panneau et du dock
.frosted-thickness = Épaisseur du verre dépoli
.less = moins
.more = plus
.glass-opacity = Opacité du verre
interface-density = Densité de l'interface
.comfortable = Confortable
.compact = Compact
@ -317,7 +332,7 @@ super-key = Action de la touche Super
.launcher = Ouvrir le Lanceur
.workspaces = Ouvrir les Espaces de travail
.applications = Ouvrir les Applications
.disable = Désactiver
.none = Aucun
window-controls = Contrôles des fenêtres
.maximize = Afficher le bouton maximiser
.minimize = Afficher le bouton minimiser
@ -426,6 +441,94 @@ sound-alerts = Alertes
.sound = Alertes sonores
sound-applications = Applications
.desc = Volumes et paramètres d'application
.none = Aucune application n'utilise le son
## Printers
printers = Imprimantes
xdg-entry-printers-comment = Gérer les imprimantes et les travaux d'impression
printers-add = Ajouter une imprimante
printers-add-in-progress = Ajout de l'imprimante…
printers-added = Imprimante « { $name } » ajoutée
printers-cancel-job = Annuler le travail
printers-color-black = Noir
printers-color-cmy = CMJ
printers-color-cmyk = CMJN
printers-color-gray = Niveaux de gris
printers-color-rgb = Couleur
printers-color-rgb-16 = Couleur (16 bits)
printers-color-rgb-24 = Couleur (24 bits)
printers-color-rgb-48 = Couleur (48 bits)
printers-default = Par défaut
printers-details = Détails de l'imprimante
printers-device = Périphérique
printers-device-uri = URI du périphérique
printers-driver = Pilote
printers-error = Échec de l'opération : { $error }
printers-ink-black = Noir
printers-ink-cyan = Cyan
printers-ink-levels = Niveaux d'encre
printers-ink-magenta = Magenta
printers-ink-yellow = Jaune
printers-job-aborted = Interrompu
printers-job-canceled = Annulé
printers-job-completed = Terminé
printers-job-held = Retenu
printers-job-pending = En attente
printers-job-processing = Impression en cours
printers-job-stopped = Arrêté
printers-job-unknown = Inconnu
printers-jobs = Travaux d'impression
printers-loading = Recherche des périphériques…
printers-location = Emplacement
printers-make-default = Définir par défaut
printers-media-type-cardstock = Papier cartonné
printers-media-type-envelope = Enveloppe
printers-media-type-labels = Étiquettes
printers-media-type-photographic = Papier photo
printers-media-type-photographic-glossy = Papier photo brillant
printers-media-type-photographic-matte = Papier photo mat
printers-media-type-photographic-satin = Papier photo satiné
printers-media-type-stationery = Papier ordinaire
printers-media-type-stationery-coated = Papier couché
printers-media-type-stationery-fine = Papier fin
printers-media-type-stationery-inkjet = Papier jet d'encre
printers-media-type-stationery-letterhead = Papier à en-tête
printers-media-type-stationery-preprint = Papier pré-imprimé
printers-media-type-stationery-recycled = Papier recyclé
printers-media-type-transparency = Transparent
printers-name = Nom
printers-no-devices = Aucun périphérique trouvé
printers-no-matching-drivers = Aucun pilote ne correspond
printers-no-printers = Aucune imprimante configurée
printers-no-printers-desc = Les imprimantes connectées à cet ordinateur ou découvertes sur le réseau apparaîtront ici.
printers-not-accepting = N'accepte pas les travaux
printers-option-color = Mode de couleur
printers-option-media = Format du papier
printers-option-media-type = Type de papier
printers-option-quality = Qualité d'impression
printers-option-sides = Impression recto-verso
printers-options = Options d'impression
printers-pause = Suspendre
printers-quality-draft = Brouillon
printers-quality-high = Haute
printers-quality-normal = Normale
printers-remove = Supprimer l'imprimante
printers-remove-confirm = Supprimer cette imprimante ?
printers-remove-confirm-button = Supprimer
printers-remove-confirm-description = « { $name } » sera supprimée du système. Les travaux en attente sur cette imprimante seront abandonnés.
printers-resume = Reprendre
printers-sides-long-edge = Bords longs
printers-sides-one-sided = Recto seul
printers-sides-short-edge = Bords courts
printers-state = État
printers-state-idle = Disponible
printers-state-processing = Impression en cours
printers-state-reasons = Causes de l'état
printers-state-stopped = En pause
printers-state-unknown = Inconnu
printers-submit = Ajouter
printers-test-page = Imprimer une page de test
## Power
@ -504,7 +607,23 @@ keyboard-special-char = Saisie de caractères spéciaux
.alternate = Touche de caractères alternatifs
.compose = Touche de composition
.compose-desc = La touche de composition permet d'entrer une grande variété de caractères. Pour l'utiliser, appuyez sur composition puis une séquence de caractères. Par exemple, la touche composition suivie de C et o entrera ©, tandis que la touche composition suivie de a et ‘ entrera á.
.caps = Touche Verr Màj
.caps = Touche de verrouillage des majuscules
.ctrl = Ctrl
.ctrl-right = Ctrl Droite
.swap-with-ctrl = Echanger avec Ctrl
.alt = Alt
.alt-left = Alt Gauche
.alt-right = Alt Droite
.super = Super
.super-left = Super Gauche
.super-right = Super Droite
.menu = Touche de menu
.backspace = Retour arrière
.escape = Echap
.swap-with-escape = Echanger avec Echap
.print-screen = Impression Ecran
.scroll-lock = Arrêt Défilement
.none = Aucune
keyboard-typing-assist = Saisie
.repeat-rate = Taux de répétition
.repeat-delay = Délai de répétition
@ -777,6 +896,8 @@ accessibility = Accessibilité
.high-contrast = Mode contraste élevé
.invert-colors = Inverser les couleurs
.color-filters = Filtres de couleurs
.shake-cursor = Secouer le curseur pour le localiser
.shake-cursor-desc = Secouez rapidement la souris pour agrandir temporairement le curseur
color-filter = Type de filtre de couleurs
.unknown = Filtre inconnu actif
.greyscale = Niveaux de gris
@ -903,3 +1024,9 @@ xdg-entry-keyboard-keywords = COSMIC;Clavier;Saisie;Source;Raccourcis;
xdg-entry-window-management-keywords = COSMIC;Fenêtre;Gestion;Agencement;Super;Touche;
xdg-entry-displays-keywords = COSMIC;Écran;
no-search-results = Aucun réseau ne correspond à votre recherche.
workspaces-navigation = Navigation
.wraparound = Passez du premier au dernier espace de travail à l'aide des raccourcis clavier et des gestes
bluetooth-display-passkey = Appairage Bluetooth
.description = Veuillez saisir le code d’association sur { $device }, puis appuyer sur Entrée
bluetooth-display-pin = Appairage Bluetooth
.description = Veuillez saisir le code PIN suivant sur { $device }, puis appuyer sur Entrée

View file

@ -60,9 +60,11 @@ vpn-error = Earráid VPN
.connect = Theip ar nascadh le VPN
.connection-editor = Theip ar an eagarthóir ceangail
.connection-settings = Theip ar na socruithe a fháil le haghaidh naisc ghníomhacha
.openvpn-config-path = Cosán comhaid neamhbhailí do chumraíocht OpenVPN
.openvpn-config-path-desc = Ní mór an comhad roghnaithe a bheith ar chóras comhad áitiúil.
.updating-state = Theip ar staid an bhainisteora líonra a nuashonrú
.wireguard-config-path = Cosán comhaid neamhbhailí le haghaidh cumraíochta WireGuard
.wireguard-config-path-desc = Caithfidh an comhad roghnaithe a bheith ar chóras comhad áitiúil.
.wireguard-config-path-desc = Ní mór an comhad roghnaithe a bheith ar chóras comhad áitiúil.
.wireguard-device = Theip ar chruthú gléas WireGuard
.with-password =
Theip ar VPN a shocrú { $field ->
@ -234,6 +236,19 @@ style = Stíl
.round = Ciorclach
.slightly-round = Ciorclach beagán
.square = Cearnógach
.frosted-glass = Gloine sioctha
.frosted-system-interface = Comhéadan córais
.frosted-system-interface-desc = Cuireann sé gloine sioctha i bhfeidhm ar an Tosaitheoir, leabharlann feidhmchlár, forbhreathnú ar an spás oibre, rialuithe ar an scáileán
.frosted-windows = Fuinneoga
.frosted-windows-desc = Cuireann sé gloine sioctha i bhfeidhm ar fhuinneoga agus ar dhialóga
.frosted-panels = Painéil
.frosted-panels-desc = Cuireann sé gloine sioctha i bhfeidhm ar an bpainéal agus ar an duga
.frosted-applets = Feidhmchláiríní
.frosted-applets-desc = Cuireann sé gloine sioctha i bhfeidhm ar bhiachláir feidhmchláiríní ar an bpainéal agus ar an duga
.frosted-thickness = Tiús sioctha
.less = níos lú
.more = níos mó
.glass-opacity = Teimhneacht gloine
interface-density = Dlús comhéadan
.comfortable = Compordach
.compact = Dlúth
@ -306,7 +321,7 @@ super-key = Gníomh na heochrach Super
.launcher = Oscail lainseálaí
.workspaces = Oscail spásanna oibre
.applications = Oscail feidhmchláir
.disable = Díchumasaigh
.none = Dada
edge-gravity = Imtharraingítear fuinneoga snámhacha chuig imill in aice láimhe
window-controls = Rialuithe fuinneoige
.maximize = Taispeáin an cnaipe uasmhéadaithe
@ -490,6 +505,22 @@ keyboard-special-char = Iontráil Carachtar Speisialta
.compose = Eochair chumadóireachta
.compose-desc = Ligeann an eochair chumadóireachta raon leathan carachtar a iontráil. Chun í a úsáid, brúigh an eochair chumadóireachta agus ansin sraith carachtar. Mar shampla, má bhrúnn tú an eochair chumadóireachta agus C agus o ina dhiaidh sin, cuirfear © isteach; agus má bhrúnn tú an eochair chumadóireachta agus a agus ‘ ina dhiaidh sin, cuirfear á isteach.
.caps = Eochair Caps Lock
.ctrl = Ctrl
.ctrl-right = Ctrl ar Dheas
.swap-with-ctrl = Malartú le Ctrl
.alt = Alt
.alt-left = Alt ar Chlé
.alt-right = Alt ar Dheas
.super = Super
.super-left = Super ar Chlé
.super-right = Super ar Dheas
.menu = Eochair Roghchláir
.backspace = Spás Cúil
.escape = Éalú
.swap-with-escape = Malartú le hÉalú
.print-screen = Priontáil Scáileáin
.scroll-lock = Glas Scrollaithe
.none = Dada
keyboard-typing-assist = Ag clóscríobh
.repeat-rate = Ráta athuair
.repeat-delay = Moill athrá
@ -911,3 +942,9 @@ xdg-entry-x11-applications = Comhoiriúnacht Feidhmchláir X11
xdg-entry-window-management-comment = Gníomh na sár-eochrach, roghanna rialaithe fuinneoige, agus roghanna breise tíleála fuinneoige
xdg-entry-wired-comment = Naisc sreangaithe agus próifílí nasc
no-search-results = Níl aon líonra ag teacht leis an gcuardach seo.
workspaces-navigation = Nascleanúint
.wraparound = Bog idir an chéad spás oibre agus an spás oibre deireanach ag baint úsáide as aicearraí méarchláir agus gothaí
bluetooth-display-passkey = Péireáil Bluetooth
.description = Clóscríobh an eochair phas seo a leanas ar { $device }, ansin brúigh Iontráil
bluetooth-display-pin = Péireáil Bluetooth
.description = Clóscríobh an PIN seo a leanas ar { $device }, ansin brúigh Iontráil

858
i18n/gl/cosmic_settings.ftl vendored Normal file
View file

@ -0,0 +1,858 @@
save = Gardar
cancel = Cancelar
connected = Conectado
confirm = Confirmar
settings = Axustes
appearance = Apariencia
dark = Escuro
light = Claro
replace = Remplazar
remove = Eliminar
connect = Conectar
password = Contrasinal
username = Nome de usuario
identity = Identidade
password-confirm = Confirmar o contrasinal
forget-dialog = Esquecer esta rede Wi-Fi?
.description = Terás que introducir o contrasinal de novo para usar esta rede Wi-Fi no futuro.
applications = Aplicacións
dbus-connection-error = Non foi posible conectarse a DBus
ok = Aceptar
unknown = Descoñecido
number = { $number }
app = Configuración COSMIC
xdg-entry-comment = Aplicación de configuracións para o escritorio COSMIC
xdg-entry-keywords = COSMIC;Configuracións;
xdg-entry-about = Sobre
xdg-entry-about-comment = Nome do dispositivo, información do hardware, valores predeterminados do sistema operativo
xdg-entry-about-keywords = COSMIC;Sobre
xdg-entry-a11y = Accesibilidade
xdg-entry-a11y-comment = Lector de pantalla, ampliador, alto contraste e filtros de cor
xdg-entry-a11y-keywords = COSMIC;Accesibilidade;A11y;Pantalla;Lector;Ampliador;Contraste;Cor;
xdg-entry-appearance = Apariencia
xdg-entry-appearance-comment = Cores de acento e temas
xdg-entry-appearance-keywords = COSMIC;Acento;Cor;Icona;Tipo de letra;Tema
xdg-entry-applications = Aplicacións
xdg-entry-applications-comment = Aplicacións predeterminadas, aplicacións de inicio e configuracións de compatibilidade con aplicacións X11
xdg-entry-applications-keywords = COSMIC;Predeterminada;Aplicación;Inicio;X11;Compatibilidade
xdg-entry-bluetooth-comment = Xestionar dispositivos Bluetooth
xdg-entry-bluetooth-keywords = COSMIC;Bluetooth;
xdg-entry-date-time = Data e hora
xdg-entry-date-time-comment = Fuso horario, axustes automáticos do reloxo e formato da hora
xdg-entry-date-time-keywords = COSMIC;Fuso;Horario;
xdg-entry-default-apps = Aplicacións por defecto
xdg-entry-default-apps-comment = Navegador web predeterminado, cliente de correo, xestor de ficheiros e outras aplicacións
xdg-entry-default-apps-keywords = COSMIC;Aplicación;Predeterminada
xdg-entry-desktop = Escritorio
xdg-entry-desktop-comment = Fondo de pantalla, aparencia, panel, dock, xestión de xanelas e configuración dos espazos de traballo
xdg-entry-desktop-keywords = COSMIC;Escritorio;
xdg-entry-displays = Pantallas
xdg-entry-displays-comment = Xestionar a configuración da pantalla
xdg-entry-displays-keywords = COSMIC;Monitor;
xdg-entry-dock = Dock
xdg-entry-dock-comment = Unha barra opcional para aplicacións e miniaplicacións
xdg-entry-dock-keywords = COSMIC;Dock;Panel;Miniaplicativo
xdg-entry-input = Dispositivos de Entrada
xdg-entry-input-comment = Axustes de teclado e rato
xdg-entry-input-keywords = COSMIC;Entrada;Teclado;Rato;Ratos;
xdg-entry-keyboard = Teclado
xdg-entry-keyboard-comment = Fontes de entrada, cambio, introdución de caracteres especiais e atallos de teclado
xdg-entry-keyboard-keywords = COSMIC;Teclado;Entrada;Fonte;Atallos;
xdg-entry-mouse = Rato
xdg-entry-mouse-comment = Velocidade do rato, aceleración e desprazamento natural
xdg-entry-mouse-keywords = COSMIC;Rato;Aceleración;Desprazamento;Natural;
xdg-entry-network = Rede e conexións sen fíos
xdg-entry-network-comment = Xestionar as conexións de rede
xdg-entry-network-keywords = COSMIC;Rede;SenFíos;WiFi;VPN;
xdg-entry-notifications = Notificacións
xdg-entry-notifications-comment = Non molestar, notificacións da pantalla de bloqueo e configuración por aplicación
xdg-entry-notifications-keywords = COSMIC;Notificación;Bloqueo;
xdg-entry-panel = Panel
xdg-entry-panel-comment = Barra principal do sistema para menús e miniaplicativos
xdg-entry-panel-keywords = COSMIC;Panel;Miniaplicativo
xdg-entry-power = Enerxía e batería
xdg-entry-power-comment = Modos de enerxía e opcións de aforro de enerxía
xdg-entry-power-keywords = COSMIC;Enerxía;Batería
xdg-entry-region-language = Rexión e Linguaxe
xdg-entry-region-language-comment = Formatar datas, horas e números segundo a túa rexión
xdg-entry-region-language-keywords = COSMIC;Rexión;Lingua;Data;Formato;Hora;Configuración rexional;Localización;
xdg-entry-sound = Son
xdg-entry-sound-comment = Configuración de son para dispositivos, alertas e aplicacións
xdg-entry-sound-keywords = COSMIC;Son;Audio;Alerta;PipeWire;
xdg-entry-startup-apps = Aplicacións de Inicio
xdg-entry-startup-apps-comment = Configurar as aplicacións que se executan ao iniciar sesión
xdg-entry-startup-apps-keywords = COSMIC;Inicio;Aplicación;
xdg-entry-system = Sistema e contas
xdg-entry-system-comment = Información do sistema, contas e actualizacións de firmware
xdg-entry-system-keywords = COSMIC;Sistema;Información;Contas;Firmware;
xdg-entry-time-language = Hora e lingua
xdg-entry-time-language-comment = Xestionar a data, a hora, a rexión e a lingua do sistema
xdg-entry-time-language-keywords = COSMIC;Sistema;Hora;Data;Rexión;Lingua;
xdg-entry-touchpad = Panel táctil
xdg-entry-touchpad-comment = Velocidade do panel táctil, opcións de clic e xestos
xdg-entry-touchpad-keywords = COSMIC;PanelTáctil;Xesto;
xdg-entry-users = Usuarios
xdg-entry-users-comment = Autenticación e contas de usuario
xdg-entry-users-keywords = COSMIC;Usuario;Conta;
xdg-entry-vpn-comment = Conexións VPN e perfís de conexión
xdg-entry-vpn-keywords = COSMIC;VPN;Rede;Conexión;OpenVPN;OpenConnect;
xdg-entry-wallpaper = Fondo de pantalla
xdg-entry-wallpaper-comment = Imaxes de fondo de pantalla, cores e opcións da presentación de diapositivas
xdg-entry-wallpaper-keywords = COSMIC;FondoDePantalla;Fondo;PresentaciónDeDiapositivas;
xdg-entry-window-management = Xestión de xanelas
xdg-entry-window-management-comment = Acción da tecla Super, opcións de control das xanelas e opcións adicionais de mosaico de xanelas
xdg-entry-window-management-keywords = COSMIC;Xanela;Xestión;Mosaico;Super;Tecla;
xdg-entry-wired = Cableado
xdg-entry-wired-comment = Conexións con fíos e perfís de conexión
xdg-entry-wired-keywords = COSMIC;Con Fíos;LAN;Rede;Conexión;
xdg-entry-wireless = WiFi
xdg-entry-wireless-comment = Conexións Wi-Fi e perfís de conexión
xdg-entry-wireless-keywords = COSMIC;WiFi;Wi-Fi;Rede;Conexión;
xdg-entry-workspaces = Espazos de traballo
xdg-entry-workspaces-comment = Orientación dos espazos de traballo, vista xeral e comportamento con varias pantallas
xdg-entry-workspaces-keywords = COSMIC;EspazoDeTraballo;Orientación;VistaXeral;Monitor;
xdg-entry-x11-applications = Compatibilidade de aplicacións X11
xdg-entry-x11-applications-comment = Escala das aplicacións do sistema de xanelas X11, pantalla principal e atallos globais
xdg-entry-x11-applications-keywords = COSMIC;X11;Aplicación;Xogo;Compatibilidade;
add-network = Engadir rede
.profile = Engadir perfil
add-vpn = Engadir VPN
airplane-on = O modo avión está activado.
cable-unplugged = Cable desconectado
connecting = Conectando…
disconnect = Desconectar
forget = Olvidar
known-networks = Redes coñecidas
network-and-wireless = Rede e conexións sen fíos
network-name = Nome da rede
no-networks = Non se atoparon redes.
no-vpn = Non hay conexións VPN dispoñibles.
qr-code-unavailable = O codigo QR non está dispoñible
scan-to-connect-description = Escanea o código QR para conectarte a esta rede.
share = Compartir rede
visible-networks = Redes visibles
auth-dialog = Autenticación necesaria
.vpn-description = Introduce o nome de usuario e o contrasinal requiridos polo servizo VPN.
.wifi-description = Introduce o contrasinal ou a chave de cifrado. Tamén podes conectarte premendo o botón «WPS» do router.
network-device-state =
.activated = Conectado
.config = Conectando
.deactivating = Desconectando
.disconnected = Desconectado
.failed = Fallou a conexión
.ip-check = Comprobando conexión
.ip-config = Solicitando IP e información de enrutamento
.need-auth = Precisa autenticación
.prepare = Preparando a conexión
.secondaries = Agardando pola conexión secundaria
.unavailable = Non dispoñible
.unknown = Estado descoñecido
.unmanaged = Non xestionado
.unplugged = Cable desconectado
remove-connection-dialog = Eliminar o perfil de conexión?
.vpn-description = Terás que introducir o contrasinal de novo para usar esta rede no futuro.
.wired-description = Terás que crear de novo este perfil para usalo no futuro.
vpn = VPN
.connections = Conexións VPN
.error = Fallou ao engadir a configuración VPN
.remove = Eliminar perfil de conexión
.select-file = Seleccionar un ficheiro de configuración VPN
vpn-error = Erro VPN
.config = Fallou ao engadir a configuración VPN
.connect = Fallou ao conectar coa VPN
.connection-editor = Fallou o editor de conexións
.connection-settings = Fallou ao obter a configuración das conexións activas
.openvpn-config-path = Ruta de ficheiro de configuración OpenVPN non válida
.openvpn-config-path-desc = O ficheiro seleccionado debe estar nun sistema de ficheiros local.
.updating-state = Fallou ao actualizar o estado do xestor de rede
.wireguard-config-path = Ruta de ficheiro de configuración WireGuard non válida
.wireguard-config-path-desc = O ficheiro seleccionado debe estar nun sistema de ficheiros local.
.wireguard-device = Fallou ao crear o dispositivo WireGuard
.with-password =
Fallou ao establecer { $field ->
*[username] o nome de usuario
[password] o contrasinal
[password-flags] as marcas do contrasinal
} da VPN con nmcli
wired = Wired
.adapter = Adaptador con fíos { $id }
.connections = Conexións con fíos
.devices = Dispositivos con fíos
.remove = Eliminar perfil de conexión
wifi = Wi-Fi
.adapter = Adaptador Wi-Fi { $id }
.forget = Esquecer esta rede
wireguard-dialog = Engadir dispositivo WireGuard
.description = Escolle un nome de dispositivo para a configuración de WireGuard.
online-accounts = Contas en liña
.desc = Engade contas, IMAP e SMTP, inicios de sesión empresariais
activate = Activar
enable = Activar
bluetooth = Bluetooth
.status = Este sistema é visible como { $aliases } mentres a configuración de Bluetooth está aberta.
.connected = Conectado
.connecting = Conectando
.disconnecting = Desconectando
.connect = Conectar
.disconnect = Desconectar
.forget = Esquecer
.dbus-error = Produciuse un erro ao interactuar con DBus: { $why }
.disabled = O servizo Bluetooth está desactivado
.inactive = O servizo Bluetooth non está activo
.unknown = Non se puido activar o servizo Bluetooth. Está instalado BlueZ?
bluetooth-paired = Dispositivos conectados anteriormente
.connect = Conectar
.battery = Batería ao { $percentage }%
bluetooth-confirm-pin = Confirmar PIN de Bluetooth
.description = Confirma que o seguinte PIN coincide co que se mostra en { $device }
bluetooth-display-passkey = Emparellamento Bluetooth
.description = Introduce a seguinte clave de acceso en { $device } e preme Intro
bluetooth-display-pin = Emparellamento Bluetooth
.description = Introduce o seguinte PIN en { $device } e preme Intro
bluetooth-available = Dispositivos próximos
bluetooth-adapters = Adaptadores Bluetooth
accessibility = Accessibilidade
.vision = Visión
.on = Activado
.off = Desactivado
.unavailable = Non dispoñible
.screen-reader = Lector de pantalla
.high-contrast = Modo de alto contraste
.invert-colors = Inverter cores
.color-filters = Filtros de cor
hearing = Audición
.mono = Reproducir o audio estéreo como mono
default = Por defecto
magnifier = Lupa
.controls =
Ou usa estes atallos: { $zoom_in ->
[zero] { "" }
*[other]
{ "" }
{ $zoom_in } para achegar,
}{ $zoom_out ->
[zero] { "" }
*[other]
{ "" }
{ $zoom_out } para afastar,
}
Super + desprazamento co rato
.scroll_controls = Activar o zoom co rato ou panel táctil mediante Super + desprazamento
.show_overlay = Mostrar a superposición da lupa
.increment = Incremento do zoom
.signin = Iniciar a lupa ao iniciar sesión
.applet = Activar ou desactivar a lupa no miniaplicativo do panel
.movement = O movemento da vista ampliada
.continuous = Continuamente co punteiro
.onedge = Cando o punteiro chega ao bordo
.centered = Para manter o punteiro centrado
color-filter = Tipo de filtro de cor
.unknown = Filtro activo descoñecido
.greyscale = Escala de grises
.deuteranopia = Verde/Vermello (debilidade do verde, Deuteranopia)
.protanopia = Vermello/Verde (debilidade do vermello, Protanopia)
.tritanopia = Azul/Amarelo (debilidade do azul, Tritanopia)
desktop = Escritorio
wallpaper = Wallpaper
.change = Cambiar imaxe cada
.fit = Axuste do fondo de pantalla
.folder-dialog = Escoller cartafol de fondos de pantalla
.image-dialog = Escoller imaxe de fondo de pantalla
.plural = Fondos de pantalla
.same = O mesmo fondo de pantalla en todas as pantallas
.slide = Presentación de diapositivas
add-color = Engadir color
add-image = Engadir imaxe
all-displays = Todas as pantallas
colors = Cores
dialog-add = Engadir
fill = Encher
fit-to-screen = Axustar a pantalla
open-new-folder = Abrir novo cartafol
recent-folders = Cartafoles recentes
x-minutes =
{ $number } { $number ->
[one] minuto
*[other] minutos
}
x-hours =
{ $number } { $number ->
[one] hora
*[other] horas
}
never = Nunca
accent-color = Cor de acento
app-background = Fondo da xanela
auto = Automatico
close = Pechar
color-picker = Selector de cor
copied-to-clipboard = Copiado ao portapapeis
copy-to-clipboard = Copiar ao portapapeis
export = Exportar
hex = Hexadecimal
import = Importar
mode-and-colors = Modo e cores
recent-colors = Cores recentes
reset-to-default = Restablecer aos valores predeterminados
rgb = RGB
window-hint-accent = Cor de indicación da xanela activa
window-hint-accent-toggle = Usar a cor de acento do tema como indicación da xanela activa
auto-switch = Cambiar automaticamente entre os modos claro e escuro
.sunrise = Cambia ao modo claro ao amencer
.sunset = Cambia ao modo escuro ao solpor
.next-sunrise = Cambia ao modo claro no seguinte amencer
.next-sunset = Cambia ao modo escuro no seguinte solpor
shadows-floating = Xanelas flotantes
.clip = Coincidir coas esquinas do sistema e aplicar sombras
shadows-tiling = Xanelas en mosaico
.clip = Coincidir coas esquinas do sistema
.shadow = Aplicar sombras
container-background = Fondo do contedor
.desc-detail = A cor do fondo do contedor úsase para a barra lateral de navegación, o caixón lateral, os diálogos e outros elementos similares. Por defecto, a cor do fondo do contedor derívase automaticamente do fondo da xanela.
.reset = Restablecer a automático
.desc = Úsase para a barra lateral de navegación, o caixón lateral, os diálogos e outros elementos similares
control-tint = Matiz dos compoñentes de control
.desc = Úsase para os fondos dos botóns estándar, campos de busca, campos de texto e compoñentes similares
frosted = Efecto de vidro esmerilado na interface do sistema
.desc = Aplica desenfoque de fondo ao panel, Dock, miniaplicativos, lanzador e biblioteca de aplicacións
enable-export = Aplicar o tema actual ás aplicacións de GNOME
.desc = Non todos os kits de ferramentas son compatibles co cambio automático. As aplicacións que non son COSMIC poden precisar reiniciarse despois dun cambio de tema.
icon-theme = Tema de iconas
.desc = Aplica un conxunto diferente de iconas ás aplicacións
text-tint = Matiz do texto da interface
.desc = Úsase para derivar as cores do texto da interface que teñen contraste suficiente en varias superficies
style = Style
.round = Redondeado
.slightly-round = Lixeiramente redondeado
.square = Cadrado
.frosted-glass = Vidro esmerilado
.frosted-system-interface = Interface do sistema
.frosted-system-interface-desc = Aplica vidro esmerilado ao Lanzador, biblioteca de aplicacións, vista xeral dos espazos de traballo e controis en pantalla
.frosted-windows = Xanelas
.frosted-windows-desc = Aplica vidro esmerilado ás xanelas e diálogos
.frosted-panels = Paneis
.frosted-panels-desc = Aplica vidro esmerilado ao panel e ao Dock
.frosted-applets = Miniaplicativos
.frosted-applets-desc = Aplica vidro esmerilado aos menús dos miniaplicativos do panel e do Dock
.frosted-thickness = Grosor do esmerilado
.less = menos
.more = máis
.glass-opacity = Opacidade do vidro
interface-density = Densidade da interface
.comfortable = Cómoda
.compact = Compacta
.spacious = Espazosa
window-management-appearance = Xestión de xanelas
.active-hint = Tamaño da indicación da xanela activa
.gaps = Separacións arredor das xanelas en mosaico
experimental-settings = Axustes experimentais
icons-and-toolkit = Tematización de iconas e kits de ferramentas
interface-font = Tipografía do sistema
monospace-font = Tipografia monoespaciada
shadow-and-corners = Sombra e esquinas das xanelas
notifications = Notificacións
panel = Panel
add = Engadir
add-applet = Engadir miniaplicativo
all = Todo
applets = Miniaplicativo
center-segment = Segmento central
place-here = Coloca os miniaplicativos aquí
end-segment = Segmento final
large = Grande
no-applets-found = Non se atoparon miniaplicativos...
panel-bottom = Abaixo
panel-left = Esquerda
panel-right = Dereita
panel-top = Arriba
search-applets = Buscar miniaplicativos...
small = Pequeno
start-segment = Segmento inicial
panel-appearance = Aparencia
.match = Coincidir co escritorio
.light = Claro
.dark = Escuro
panel-behavior-and-position = Comportamento e posicións
.autohide = Ocultar automaticamente o panel
.dock-autohide = Ocultar automaticamente o Dock
.position = Posición na pantalla
.display = Mostrar na pantalla
panel-style = Style
.anchor-gap = Separación entre o panel e os bordos da pantalla
.dock-anchor-gap = Separación entre o Dock e os bordos da pantalla
.extend = Estender o panel ata os bordos da pantalla
.dock-extend = Estender o Dock ata os bordos da pantalla
.appearance = Aparencia
.size = Tamaño
.background-opacity = Opacidade do fondo
panel-applets = Configuración
.dock-desc = Configura os miniaplicativos do Dock
.desc = Configura os miniaplicativos do panel
panel-missing = Falta a configuración do panel
.desc = O ficheiro de configuración do panel non está dispoñible debido ao uso dunha configuración personalizada ou está danado.
.fix = Restablecer aos valores predeterminados
dock = Dock
window-management = Xestión de xanelas
super-key = Acción da tecla Super
.launcher = Abrir o Lanzador
.workspaces = Abrir os Espazos de traballo
.applications = Abrir as Aplicacións
.none = Ningunha
edge-gravity = As xanelas flotantes axústanse aos bordos próximos
window-controls = Controis das xanelas
.maximize = Mostrar o botón de maximizar
.minimize = Mostrar o botón de minimizar
.active-window-hint = Mostrar indicación da xanela activa
focus-navigation = Navegación do foco
.focus-follows-cursor = O foco segue o cursor
.focus-follows-cursor-delay = Retardo do foco ao seguir o cursor en ms
.cursor-follows-focus = O cursor segue o foco
workspaces = Espazos de traballo
workspaces-overview = Vista xeral dos espazos de traballo
.action-on-typing = Acción ao escribir
.none = Ningunha
.launcher = Abrir o Lanzador
.applications = Abrir as Aplicacións
workspaces-behavior = Comportamento dos espazos de traballo
.dynamic = Espazos de traballo dinámicos
.dynamic-desc = Elimina automaticamente os espazos de traballo baleiros.
.fixed = Número fixo de espazos de traballo
.fixed-desc = Engade ou elimina espazos de traballo na vista xeral.
workspaces-multi-behavior = Comportamento con varios monitores
.span = Os espazos de traballo abranguen todas as pantallas
.separate = As pantallas teñen espazos de traballo separados
workspaces-overview-thumbnails = Miniaturas da vista xeral dos espazos de traballo
.show-number = Mostrar o número do espazo de traballo
.show-name = Mostrar o nome do espazo de traballo
workspaces-orientation = Orientación dos espazos de traballo
.vertical = Vertical
.horizontal = Horizontal
hot-corner = Esquina activa
.top-left-corner = Activar a esquina activa superior esquerda para os Espazos de traballo
workspaces-navigation = Navegación
.wraparound = Moverse entre o primeiro e o último espazo de traballo usando atallos de teclado e xestos
-requires-restart = Require reinicio
color = Cor
.depth = Profundidade de cor
.profile = Perfil de cor
.sidebar = Perfís de cor
.temperature = Temperatura da cor
display = Pantallas
.arrangement = Disposición das pantallas
.arrangement-desc = Arrastra as pantallas para reorganizalas
.enable = Activar pantalla
.external = Pantalla externa { $size } { $output }
.laptop = Pantalla de portátil { $size }
.options = Opcións da pantalla
.refresh-rate = Frecuencia de actualización
.resolution = Resolución
.scale = Escala
.additional-scale-options = Opcións adicionais de escala
mirroring = Espello
.id = Espello { $id }
.dont = Non facer espello
.mirror = Facer espello de { $display }
.project =
Proxectar en { $display ->
[all] todas as pantallas
*[other] { $display }
}
.project-count =
Proxectando noutras { $count } { $count ->
[1] pantalla
*[other] pantallas
}
night-light = Luz nocturna
.auto = Automática (do solpor ao amencer)
.desc = Reduce a luz azul usando cores máis cálidas
orientation = Orientación
.standard = Estándar
.rotate-90 = Xirar 90°
.rotate-180 = Xirar 180°
.rotate-270 = Xirar 270°
vrr = Frecuencia de actualización variable
.enabled = Activada
.force = Sempre
.auto = Automática
.disabled = Desactivada
scheduling = Programación
.manual = Programación manual
dialog = Diálogo
.title = Manter esta configuración da pantalla?
.keep-changes = Manter os cambios
.change-prompt = Os cambios de configuración reverteranse automaticamente en { $time } segundos.
.revert-settings = Reverter a configuración
sound = Son
sound-output = Saída
.volume = Volume de saída
.device = Dispositivo de saída
.level = Nivel de saída
.config = Configuración
.balance = Balance
.left = Esquerda
.right = Dereita
sound-input = Entrada
.volume = Volume de entrada
.device = Dispositivo de entrada
.level = Nivel de entrada
amplification = Amplificación
.desc = Permite aumentar o volume ata o 150%
sound-alerts = Alertas
.volume = Volume das alertas
.sound = Son das alertas
sound-applications = Aplicacións
.desc = Volumes e configuracións das aplicacións
.none = Ningún aplicativo esta a soar
sound-device-port-unplugged = Desconectado
sound-hd-audio = Audio HD
sound-usb-audio = Audio USB
sound-device-profiles = Perfís dos dispositivos
power = Enerxía e batería
battery = Batería
.minute =
{ $value } { $value ->
[one] minuto
*[other] minutos
}
.hour =
{ $value } { $value ->
[one] hora
*[other] horas
}
.day =
{ $value } { $value ->
[one] día
*[other] días
}
.less-than-minute = Menos dun minuto
.and = e
.remaining-time =
{ $time } ata { $action ->
[full] completo
*[other] baleiro
}
connected-devices = Dispositivos conectados
.unknown = Dispositivo descoñecido
power-mode = Modo de enerxía
.battery = Maior duración da batería
.battery-desc = Menor consumo de enerxía e funcionamento silencioso
.balanced = Equilibrado
.balanced-desc = Funcionamento silencioso e consumo moderado de enerxía
.performance = Alto rendemento
.performance-desc = Máximo rendemento e consumo de enerxía
.no-backend = Non se atopou o backend. Instala system76-power ou power-profiles-daemon.
power-saving = Opcións de aforro de enerxía
.turn-off-screen-after = Apagar a pantalla despois de
.auto-suspend = Suspensión automática
.auto-suspend-ac = Suspensión automática ao estar conectado á corrente
.auto-suspend-battery = Suspensión automática usando a batería
acceleration-desc = Axuste automático da sensibilidade de seguimento segundo a velocidade
disable-while-typing = Desactivar ao escribir
input-devices = Dispositivos de entrada
primary-button = Botón principal
.desc = Establece a orde dos botóns físicos
.left = Esquerdo
.right = Dereito
scrolling = Desprazamento
.two-finger = Desprazamento con dous dedos
.edge = Desprazamento polo bordo cun dedo
.speed = Velocidade de desprazamento
.natural = Desprazamento natural
.natural-desc = Desprazar o contido en vez da vista
slow = Amodo
fast = Rápido
short = Curto
long = Longo
keyboard = Teclado
keyboard-sources = Fontes de entrada
.desc = As fontes de entrada pódense cambiar usando a combinación de teclas Super+Espazo. Isto pódese personalizar na configuración dos atallos de teclado.
.move-up = Mover arriba
.move-down = Mover abaixo
.settings = Configuracións
.view-layout = Ver a distribución do teclado
.remove = Eliminar
.add = Engadir fonte de entrada
keyboard-special-char = Entrada de caracteres especiais
.alternate = Tecla de caracteres alternativos
.compose = Tecla Compose
.compose-desc = A tecla Compose permite introducir unha gran variedade de caracteres. Para usala, preme Compose e despois unha secuencia de caracteres. Por exemplo, a tecla Compose seguida de C e o introduce ©, mentres que a tecla Compose seguida de a e ‘ introduce á.
.caps = Tecla Bloq Maiús
.ctrl = Control
.ctrl-right = Ctrl dereito
.swap-with-ctrl = Intercambiar con Control
.alt = Alt
.alt-left = Alt esquerda
.alt-right = Alt dereita
.super = Super
.super-left = Super esquerda
.super-right = Super dereita
.menu = Tecla Menú
.backspace = Retroceso
.escape = Escape
.swap-with-escape = Intercambiar con Escape
.print-screen = Imprimir pantalla
.scroll-lock = Bloq Desprazamento
.none = Ningunha
keyboard-typing-assist = Escritura
.repeat-rate = Velocidade de repetición
.repeat-delay = Retardo de repetición
keyboard-numlock-boot = Bloq Num
.boot-state = Estado no arranque
.last-boot = Último arranque
.on = Activado
.off = Desactivado
.set = Establecer o estado de Bloq Num no arranque
added = Engadido
type-to-search = Escribe para buscar...
no-search-results = Non hai redes que coincidan coa túa busca.
show-extended-input-sources = Mostrar fontes de entrada ampliadas
keyboard-shortcuts = Atallos de teclado
.desc = Ver e personalizar os atallos de teclado
add-another-keybinding = Engadir outro atallo de teclado
command = Comando
custom = Personalizado
debug = Depuración
disabled = Deshabilitado
input-source-switch = Cambiar fonte de entrada do idioma do teclado
migrate-workspace-prev = Migrar espazo de traballo á pantalla anterior
migrate-workspace-next = Migrar espazo de traballo á seguinte pantalla
migrate-workspace =
Migrar espazo de traballo á pantalla { $direction ->
*[down] abaixo
[left] esquerda
[right] dereita
[up] arriba
}
navigate = Navegar
shortcut-name = Nome do atallo
system-controls = Controles do sistema
terminate = Finalizar
toggle-stacking = Alternar pila de xanelas
type-key-combination = Escribir combinación de teclas
custom-shortcuts = Atallos personalizados
.add = Engadir atallo
.context = Engadir atallo personalizado
.none = Non hai atallos personalizados
modified = { $count } modificado
nav-shortcuts = Navegación
.prev-output = Enfocar a pantalla anterior
.next-output = Enfocar a seguinte pantalla
.last-workspace = Enfocar o último espazo de traballo
.prev-workspace = Enfocar o espazo de traballo anterior
.next-workspace = Enfocar o seguinte espazo de traballo
.focus =
Enfocar ventá { $direction ->
*[down] abaixo
[in] dentro
[left] esquerda
[out] fóra
[right] dereita
[up] arriba
}
.output =
Cambiar á pantalla { $direction ->
*[down] abaixo
[left] esquerda
[right] dereita
[up] arriba
}
.workspace = Cambiar ao espazo de traballo { $num }
manage-windows = Xestionar ventás
.close = Pechar ventá
.maximize = Maximizar ventá
.fullscreen = Ventá a pantalla completa
.minimize = Minimizar ventá
.resize-inwards = Reducir tamaño da ventá cara dentro
.resize-outwards = Aumentar tamaño da ventá cara fóra
.toggle-sticky = Alternar ventá persistente
move-windows = Mover ventás
.direction =
Mover ventá { $direction ->
*[down] abaixo
[left] esquerda
[right] dereita
[up] arriba
}
.display =
Mover ventá un monitor { $direction ->
*[down] abaixo
[left] esquerda
[right] dereita
[up] arriba
}
.workspace =
Mover ventá un espazo de traballo { $direction ->
*[below] abaixo
[left] esquerda
[right] dereita
[above] arriba
}
.workspace-num = Mover ventá ao espazo de traballo { $num }
.prev-workspace = Mover ventá ao espazo de traballo anterior
.next-workspace = Mover ventá ao seguinte espazo de traballo
.last-workspace = Mover ventá ao último espazo de traballo
.next-display = Mover ventá á seguinte pantalla
.prev-display = Mover ventá á pantalla anterior
.send-to-prev-workspace = Enviar ventá ao espazo de traballo anterior
.send-to-next-workspace = Enviar ventá ao seguinte espazo de traballo
system-shortcut = Sistema
.app-library = Abrir a biblioteca de aplicacións
.brightness-down = Reducir o brillo da pantalla
.brightness-up = Aumentar o brillo da pantalla
.display-toggle = Alternar a pantalla interna
.home-folder = Abrir o cartafol persoal
.keyboard-brightness-down = Reducir o brillo do teclado
.keyboard-brightness-up = Aumentar o brillo do teclado
.launcher = Abrir o Lanzador
.log-out = Pechar sesión
.lock-screen = Bloquear a pantalla
.mute = Silenciar a saída de audio
.mute-mic = Silenciar a entrada do micrófono
.play-pause = Reproducir/pausar
.play-next = Seguinte pista
.play-prev = Pista anterior
.poweroff = Apagar
.screenshot = Facer unha captura de pantalla
.suspend = Suspender
.terminal = Abrir un terminal
.touchpad-toggle = Alternar o panel táctil
.volume-lower = Reducir o volume de saída de audio
.volume-raise = Aumentar o volume de saída de audio
.web-browser = Abrir un navegador web
.window-switcher = Cambiar entre ventás abertas
.window-switcher-previous = Cambiar entre ventás abertas en orde inversa
.workspace-overview = Abrir a vista xeral dos espazos de traballo
window-tiling = Mosaico de xanelas
.horizontal = Establecer orientación horizontal
.vertical = Establecer orientación vertical
.swap-window = Intercambiar ventá
.toggle-tiling = Alternar mosaico de ventás
.toggle-stacking = Alternar empilhamento de ventás
.toggle-floating = Alternar ventá flotante
.toggle-orientation = Alternar orientación
replace-shortcut-dialog = Substituír atallo?
.desc = { $shortcut } está sendo usado por { $name }. Se o substitúes, { $name } será desactivado.
zoom-in = Ampliar Zoom
zoom-out = Reducir Zoom
mouse = Rato
.speed = Velocidade do rato
.acceleration = Activar aceleración do rato
click-behavior = Comportamento dos clics
.click-finger = Clic secundario con dous dedos e clic central con tres dedos
.button-areas = Clic secundario na esquina inferior dereita e clic central no centro inferior
pinch-to-zoom = Zoom con pinza
.desc = Usa dous dedos para ampliar o contido nas aplicacións que admitan zoom
tap-to-click = Tocar para facer clic
.desc = Activa o toque cun dedo para o clic principal, o toque con dous dedos para o clic secundario e o toque con tres dedos para o clic central
touchpad = Panel táctil
.acceleration = Activar aceleración do panel táctil
.speed = Velocidade do panel táctil
gestures = Xestos
.four-finger-down = Desprazamento cara abaixo con catro dedos
.four-finger-left = Desprazamento cara á esquerda con catro dedos
.four-finger-right = Desprazamento cara á dereita con catro dedos
.four-finger-up = Desprazamento cara arriba con catro dedos
.three-finger-any = Desprazamento en calquera dirección con tres dedos
switch-workspaces = Cambiar de espazos de traballo
.horizontal = Desprazamento horizontal con catro dedos (esquerda/dereita)
.vertical = Desprazamento vertical con catro dedos (arriba/abaixo)
switch-between-windows = Alternar entre xanelas
open-application-library = Abrir a biblioteca de aplicacións
open-workspaces-view = Abrir a vista xeral dos espazos de traballo
time = Hora e linguaxe
time-date = Data e hora
.auto = Establecer automaticamente
.auto-ntp = A data e hora actualizaranse automaticamente cando estea configurada a zona horaria
time-zone = Zona horaria
.auto = Zona horaria automática
.auto-info = Require servizos de localización e acceso a Internet
time-format = Formato de data e hora
.twenty-four = Formato de 24 horas
.show-seconds = Mostrar segundos
.first = Primeiro día da semana
.show-date = Mostrar a data no applet da hora
.friday = Venres
.saturday = Sábado
.sunday = Domingo
.monday = Luns
time-region = Rexión e linguaxe
formatting = Formato
.dates = Datas
.time = Hora
.date-and-time = Data e hora
.numbers = Números
.measurement = Medición
.paper = Papel
preferred-languages = Idiomas preferidos
.desc = A orde dos idiomas determina que idioma se usa para a interface de usuario. Os cambios aplicaranse no seguinte inicio de sesión.
add-language = Engadir linguaxe
.context = Engadir lingua
install-additional-languages = Instalar linguaxes adicionais
region = Rexión
default-apps = Aplicacións predeterminadas
.web-browser = Navegador web
.file-manager = Xestor de ficheiros
.mail-client = Cliente de correo
.music = Música
.video = Vídeo
.photos = Fotos
.calendar = Calendario
.terminal = Terminal
.other-associations = Outras asociacións
.text-editor = Editor de texto
.not-installed = Non instalado
startup-apps = Aplicacións de inicio
.add = Engadir aplicación
.user = Aplicacións iniciadas cando inicias sesión
.none = Non hai aplicacións de inicio engadidas
.remove-dialog-title = Eliminar { $name }?
.remove-dialog-description = Queres eliminar esta aplicación de inicio?
.add-startup-app = Engadir aplicación de inicio
legacy-applications = Compatibilidade de aplicacións X11
legacy-app-global-shortcuts = Atallos globais en aplicacións X11
.desc = Os atallos globais permiten que as pulsacións de teclas e os eventos dos botóns do rato realizados nas aplicacións sexan recoñecidos por outras aplicacións para funcións como premer para falar ou premer para silenciar. Por defecto, os atallos globais están desactivados nas aplicacións X11 para garantir que outras aplicacións non poidan supervisar eventos de teclado e rato que conteñan información sensible.
.none = Sen teclas
.modifiers = Modificadores (Super, Maiús, Control, Alt)
.combination = Todas as teclas mentres se manteñen premidos os modificadores Super, Control ou Alt
.all = Todas as teclas
.mouse = Eventos dos botóns do rato nas aplicacións X11
legacy-app-scaling = Escalado de aplicacións do sistema de ventás X11
.scaled-gaming = Optimizar para xogos e aplicacións a pantalla completa
.gaming-description = As aplicacións X11 poden aparecer lixeiramente máis grandes ou máis pequenas en comparación coas aplicacións Wayland
.scaled-applications = Optimizar para aplicacións
.applications-description = Os xogos e as aplicacións X11 a pantalla completa poden non coincidir coa resolución da túa pantalla
.scaled-compatibility = Modo de máxima compatibilidade
.compatibility-description = As aplicacións X11 poden aparecer borrosas en pantallas HiDPI
.preferred-display = Pantalla preferida para xogos e aplicacións X11 a pantalla completa
.no-display = Ningunha
system = Sistema e contas
about = Sobre
about-device = Nome do dispositivo
.desc = Este nome aparece para outros dispositivos da rede ou Bluetooth
about-hardware = Hardware
.model = Modelo de hardware
.memory = Memoria
.processor = Procesador
.graphics = Gráficos
.disk-capacity = Capacidade do disco
about-os = Sistema operativo
.os = Sistema operativo
.os-architecture = Arquitectura do sistema operativo
.kernel = Versión do kernel
.desktop-environment = Contorno de escritorio
.windowing-system = Sistema de xanelas (servidor gráfico)
about-related = Axustes relacionados
.support = Obter asistencia
firmware = Firmware
users = Usuarios
.admin = Administrador
.standard = Estándar
.profile-add = Escoller imaxe de perfil
administrator = Administrador
.desc = Os administradores poden cambiar os axustes de todos os usuarios, engadir e eliminar outros usuarios
add-user = Engadir usuario
change-password = Cambiar o contrasinal
remove-user = Eliminar o usuario
full-name = Nome completo
invalid-username = Nome de usuario non válido
password-mismatch = O contrasinal e a confirmación deben coincidir

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