Compare commits

...
Sign in to create a new pull request.

29 commits

Author SHA1 Message Date
da53a9f45f feat: add themed launcher icon catalog
Some checks failed
Continuous Integration / formatting (push) Has been cancelled
Continuous Integration / linting (push) Has been cancelled
2026-05-26 12:03:59 +02:00
339ac4e3e4 fix: open launcher editor in existing popup
Some checks are pending
Continuous Integration / formatting (push) Waiting to run
Continuous Integration / linting (push) Waiting to run
2026-05-26 11:49:46 +02:00
cc501c7637 feat: add editable dock launchers
Some checks are pending
Continuous Integration / formatting (push) Waiting to run
Continuous Integration / linting (push) Waiting to run
2026-05-26 10:39:01 +02:00
93ab0f391d chore: fix applets redeploy path
Some checks are pending
Continuous Integration / formatting (push) Waiting to run
Continuous Integration / linting (push) Waiting to run
2026-05-26 09:26:01 +02:00
4416a5e9ea chore: use local COSMIC support crates 2026-05-26 09:21:53 +02:00
6c7f31e1ae chore: use local cosmic-text checkout 2026-05-26 09:21:18 +02:00
f2f53fc4d2 yoda: use local dbus settings bindings 2026-05-26 09:21:18 +02:00
82e00a3e16 chore: align applets with local stack 2026-05-26 09:20:01 +02:00
Votre Nom
bcc8072a3b fix(audio): accumuler les rafales scroll Pixels au lieu de signum()
Avec Wayland axis_v120 (scroll haute-résolution sur souris HID modernes),
un cran physique génère 5–8 events ScrollDelta::Pixels (~15–20px chacun).
L'ancien code passait chaque sub-event par .signum() puis -1/+1 à sink_volume,
donc un seul cran physique faisait varier le volume de 5 à 40% — résultat :
scroll up sur l'icône audio panel / dock coupait le son si le volume était
déjà bas.

Fix : thread_local accumulator des deltas Pixels, émission seulement
au passage du seuil de 15px par cran logique. Lines (souris classique
sans axis_v120) reste proportionnel y * WHEEL_STEP. round() au lieu de
truncation finale pour ne pas perdre les fractions de pourcent.

Leyoda 2026 - GPLv3
2026-05-26 09:17:36 +02:00
Votre Nom
57a435e830 chore: add redeploy.sh for /usr/local/bin install
Builds workspace release, backs up existing binaries, installs
cosmic-applets/cosmic-app-list/cosmic-panel-button to /usr/local/bin
(precedence over pacman package via $PATH).

Leyoda 2026 – GPLv3
2026-05-26 09:17:36 +02:00
Votre Nom
6fe087f4fd fix(wayland): graceful exit on compositor disconnect
Replace 3 panicking unwrap() in cosmic-app-list/wayland_handler.rs
(event loop dispatch + 2 conn.flush in screencopy) with logged
errors that break/return None instead.

Wrap cosmic-applets/main.rs entry point in panic::catch_unwind to
catch panics propagating from libcosmic/iced/winit (which we cannot
patch locally without forking) when the COSMIC compositor closes
the Wayland connection at logout. This eliminates the cascade of
~12 SIGABRT coredumps observed at session shutdown.

Panic strategy is unwind (default), catch_unwind is sound here.

Leyoda 2026 – GPLv3
2026-05-26 09:17:36 +02:00
0fa93ba21f yoda: smooth animated fisheye for dock hover (phase B v3, closes b)
Inter-icon hover changes were snapping because icon_scale_for read the
hovered icon's real rectangle directly. This adds a small animation
layer so the bell center lerps toward the target and the whole effect
fades in/out at the dock's edges.

CosmicAppList gains three fields:
- anim_hover_center: Option<(f32, f32)> — virtual cursor position,
  chases the hovered icon's center across ticks.
- anim_hover_intensity: f32 (0..1) — global fade-in/out of the
  fisheye. Target 1.0 while a dock icon is hovered, 0.0 otherwise.
- anim_last_tick: Option<Instant> — for dt-based exponential smoothing
  (time-constant tau = 60ms, ~99% of target reached in ~120ms).

A new Message::AnimTick(Instant) is emitted at ~60 fps by a
conditional iced::time::every subscription — only active when the
pointer is over a dock icon OR intensity hasn't faded back to ~0 yet,
so the panel stays idle when no one is hovering the dock.

icon_scale_for now reads anim_hover_center instead of rectangles[hovered]
and multiplies the bell's peak by anim_hover_intensity. Behaviour:
- Pointer slides A → B: bell glides continuously, both icons animate.
- Pointer enters dock: icons inflate smoothly over ~120 ms.
- Pointer leaves dock: icons deflate smoothly over ~120 ms.

Fallback paths (first frame, missing rectangles) still respond
instantly so the feature never looks 'stuck' before the animation
kicks in.
2026-05-26 09:17:36 +02:00
8fc11581ad yoda: fisheye magnification for dock hover (phase B v2 / c)
Replaces the binary 1.3× hover with a true gaussian bell curve — the
hovered icon still peaks at ~1.35×, but the ±1 neighbours also bulge
noticeably, ±2 a bit, and ±3+ relax to 1.0×. Footprint ~5 icons wide,
matching the macOS Dock fisheye feel.

Implementation in fn icon_scale_for(id):
- Reads the hovered icon's and the current icon's bounds from
  self.rectangles (already populated by the existing RectangleTracker
  subscription — no new plumbing).
- Distance = |this_center - hovered_center| along the panel's long axis
  (horizontal for Top/Bottom anchors, vertical for Left/Right).
- sigma = hovered_extent * 1.4 so the bell's half-width matches one
  icon width (neighbors clearly pulled, far icons untouched).
- scale = 1.0 + PEAK * exp(-(d/sigma)²) with PEAK = 0.35.
- Falls back to binary 1.35×/1.0× when rectangle data isn't populated
  yet (first render / resize) — visibly responsive even before the
  tracker catches up.

No widget signature changes vs v1, just a smarter formula. All five
as_icon call sites already pass the result of icon_scale_for so this
update propagates everywhere.

Still on the TODO list: smooth animation (b). Right now icon→icon
transitions snap instantly; a smoothed_hover_center + tick subscription
would lerp it. Deferred to a follow-up commit.
2026-05-26 09:17:36 +02:00
d090e60370 yoda: dock icon hover magnification (macOS Tahoe-style, phase B v1)
First pass at the signature macOS Dock effect — the icon under the
pointer grows, adjacent icons stay at base size. Full fisheye (smooth
bell-curve scaling on neighbors) can be a later iteration.

Changes in cosmic-app-list/src/app.rs:
- CosmicAppList gains a hovered_dock_item: Option<DockItemId>
  auto-initialized to None via #[derive(Default)].
- New Message::DockItemHover(Option<DockItemId>) handled in update()
  by just writing the field; view() then reads it to decide scale.
- DockItem::as_icon gains an icon_scale: f32 parameter. Inside it the
  cosmic_icon width/height = (base_icon_size * icon_scale) clamped
  to u16; indicator dot and other surrounding layout stay at base
  size so only the icon visually bulges.
- New App::icon_scale_for(id) helper: 1.3 if Some(id) == hovered,
  1.0 otherwise. Single place to tune the magnification factor.
- The two main dock rows (favorites + filtered_active_list) wrap
  their rendered applet_tooltip in widget::mouse_area with
  on_enter(DockItemHover(Some(id))) / on_exit(DockItemHover(None))
  and call icon_scale_for before rendering.
- The three remaining as_icon call sites (DnD preview, favorites
  overflow popup, active overflow popup) pass icon_scale = 1.0 —
  hover magnification on those surfaces would look jittery and isn't
  needed anyway.

Build: cargo build --release -p cosmic-app-list (≈ 7s). Binary
installed at /usr/local/bin/cosmic-app-list, backup kept as
.pre-magnification.
2026-05-26 09:17:36 +02:00
Akrm Al-Hakimi
03c302d138 chore(deps): bump nmrs to 3.1.3 2026-05-19 10:27:27 -04:00
Akrm Al-Hakimi
2362e7ce40 chore(deps): bump nmrs version to include patch 2026-05-19 10:27:27 -04:00
Akrm Al-Hakimi
8a39826623 chore: bump nmrs version 2026-05-19 10:27:27 -04:00
Akrm Al-Hakimi
8d84396e57 feat(network): integrate nmrs for VPN, airplane mode, and secret agent
Replace cosmic-settings-network-manager-subscription channel-based
NetworkManager calls with direct nmrs API calls:
- VPN list: nm.list_saved_connections() instead of hand-walking
  NetworkManagerSettings over D-Bus.
- VPN connect/disconnect: nm.connect_vpn_by_uuid / disconnect_vpn_by_uuid.
- Forget Wi-Fi: nm.forget(&ssid).
- Wi-Fi radio toggle: nm.set_wireless_enabled.
- Airplane mode: nm.set_airplane_mode, dropping
  cosmic-settings-airplane-mode-subscription.
- Secret agent: nmrs::agent::SecretAgent registered per-popup,
  replacing nm-secret-agent-manager. VPN secrets are replied via
  responder.vpn_secrets(...); the applets own Authenticate flow still
  drives Wi-Fi password handoff and releases NM with NoSecrets.
2026-05-19 10:27:27 -04:00
Andrei Ivanou
65a9e142b5 fix(process): prevent zombie shell processes 2026-05-18 20:34:01 +02:00
Ashley Wulber
666f0110d6 clippy 2026-05-13 12:41:17 -04:00
Ashley Wulber
78a6f78621 chore: release profile 2026-05-13 12:41:17 -04:00
Ashley Wulber
8b2ff3df73 chore(battery): update deps with fix 2026-05-13 12:41:17 -04:00
Michael Murphy
89a149034d
i18n: translation update from Hosted Weblate (#1382) 2026-05-12 17:02:41 +02:00
Hosted Weblate
737aaff4b0
i18n: translation updates from weblate
Co-authored-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
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: Fedorov Alexei <aleksejfedorov963@gmail.com>
Co-authored-by: Feike Donia <feikedonia@proton.me>
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: Konstantinos <kostas.lampropoulos94@gmail.com>
Co-authored-by: Nara Díaz Viñolas <nara.diaz.vinolas@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: Марко М. Костић <marko.m.kostic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/fr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/kk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/ru/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-a11y/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-a11y/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-audio/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-audio/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-battery/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-battery/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-battery/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-bluetooth/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-bluetooth/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-bluetooth/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-notifications/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-notifications/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-power/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-power/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-power/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-tiling/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-tiling/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-time/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-time/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/ca/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/uk/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-input-sources-applet/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-input-sources-applet/sr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-workspaces-applet/el/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-workspaces-applet/sr/
Translation: Pop OS/COSMIC Accessibility Applet
Translation: Pop OS/COSMIC App List Applet
Translation: Pop OS/COSMIC Applets Desktop Entries
Translation: Pop OS/COSMIC Audio Applet
Translation: Pop OS/COSMIC Battery Applet
Translation: Pop OS/COSMIC Bluetooth Applet
Translation: Pop OS/COSMIC Input Sources Applet
Translation: Pop OS/COSMIC Network Applet
Translation: Pop OS/COSMIC Notifications Applet
Translation: Pop OS/COSMIC Power Applet
Translation: Pop OS/COSMIC Tiling Applet
Translation: Pop OS/COSMIC Time Applet
Translation: Pop OS/COSMIC Workspaces Applet
2026-05-11 12:24:01 +02:00
Michael Aaron Murphy
c003924f08 chore: update all dependencies 2026-04-28 20:35:07 +02:00
Michael Aaron Murphy
0932bf4edf debian: update changelog 2026-04-28 20:35:07 +02:00
Michael Aaron Murphy
11d99c5df3 chore: add zed editor config 2026-04-28 20:35:07 +02:00
Michael Aaron Murphy
b7b768a998 chore: update libcosmic 2026-04-28 20:35:07 +02:00
Michael Aaron Murphy
ce51b784b7 chore: update MSRV to Rust 1.93 2026-04-28 20:35:07 +02:00
101 changed files with 2955 additions and 1059 deletions

View file

@ -19,11 +19,11 @@ jobs:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.90.0
toolchain: 1.93.1
components: clippy
- name: install dependencies
run: sudo apt update && sudo apt install -y libxkbcommon-dev libwayland-dev libdbus-1-dev libpulse-dev libpipewire-0.3-dev libinput-dev
- uses: actions-rs-plus/clippy-check@v2
with:
toolchain: 1.90.0
toolchain: 1.93.1
args: --all --all-targets --all-features

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"],
},
},
},
},
}

787
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,11 +25,11 @@ resolver = "3"
[workspace.dependencies]
anyhow = "1.0.102"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be" }
cctk = { package = "cosmic-client-toolkit", path = "../cosmic-protocols/client-toolkit" }
cosmic-applets-config = { path = "cosmic-applets-config" }
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", default-features = false, features = [
cosmic-protocols = { path = "../cosmic-protocols", default-features = false, features = [
"client",
], rev = "d0e95be" }
]}
futures = "0.3"
futures-util = "0.3"
@ -38,7 +38,7 @@ i18n-embed = { version = "0.16.0", features = [
"desktop-requester",
] }
i18n-embed-fl = "0.10"
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = [
cosmic = { package = "libcosmic-yoda", path = "../libcosmic", default-features = false, features = [
"applet",
"applet-token",
"dbus-config",
@ -48,6 +48,7 @@ libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = fa
"desktop-systemd-scope",
"winit",
] }
cosmic-comp-config = { path = "../cosmic-comp/cosmic-comp-config" }
rust-embed = "8.11.0"
rust-embed-utils = "8.11.0"
rustc-hash = "2.1"
@ -58,14 +59,13 @@ tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-log = "0.2.0"
tokio = { version = "1.49.0", features = ["full"] }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
cosmic-config = { git = "https://github.com/pop-os/libcosmic" }
cosmic-config = { path = "../libcosmic/cosmic-config" }
serde = { version = "1.0.228", features = ["derive"] }
[profile.release]
# opt-level = 3
# panic = "abort"
# lto = "thin"
opt-level = 1
opt-level = 3
panic = "abort"
lto = "thin"
[workspace.metadata.cargo-machete]
ignored = ["libcosmic"]
@ -82,12 +82,51 @@ ignored = ["libcosmic"]
# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" }
# winit = { path = "../winit" }
[patch."https://github.com/pop-os/libcosmic"]
cosmic-config = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" }
cosmic-theme = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-theme" }
iced = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced" }
iced_accessibility = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/accessibility" }
iced_core = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/core" }
iced_futures = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/futures" }
iced_graphics = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/graphics" }
iced_renderer = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/renderer" }
iced_runtime = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/runtime" }
iced_tiny_skia = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/tiny_skia" }
iced_wgpu = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/wgpu" }
iced_widget = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/widget" }
iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit" }
[patch."https://github.com/smithay/client-toolkit.git"]
sctk = { package = "smithay-client-toolkit", version = "0.20.0" }
[patch."https://github.com/pop-os/cosmic-protocols"]
cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", branch = "main" }
cosmic-protocols = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols" }
cosmic-client-toolkit = { path = "/home/lionel/Projets/COSMIC/cosmic-protocols/client-toolkit" }
[patch."https://github.com/pop-os/cosmic-panel"]
cosmic-panel-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/cosmic-panel-config" }
xdg-shell-wrapper-config = { path = "/home/lionel/Projets/COSMIC/cosmic-panel/xdg-shell-wrapper-config" }
[patch."https://github.com/pop-os/cosmic-notifications"]
cosmic-notifications-config = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-config" }
cosmic-notifications-util = { path = "/home/lionel/Projets/COSMIC/cosmic-notifications/cosmic-notifications-util" }
[patch."https://github.com/pop-os/cosmic-settings"]
cosmic-settings-a11y-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/a11y-manager" }
cosmic-settings-accessibility-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/accessibility" }
cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" }
cosmic-settings-daemon-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/settings-daemon" }
cosmic-settings-network-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/network-manager" }
cosmic-settings-sound-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/sound" }
cosmic-settings-upower-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/upower" }
[patch."https://github.com/pop-os/cosmic-settings/"]
cosmic-settings-airplane-mode-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/airplane-mode" }
cosmic-settings-network-manager-subscription = { path = "/home/lionel/Projets/COSMIC/cosmic-settings/subscriptions/network-manager" }
[patch."https://github.com/pop-os/cosmic-text.git"]
cosmic-text = { path = "../cosmic-text" }
# [patch.'https://github.com/pop-os/dbus-settings-bindings']
# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" }

View file

@ -13,13 +13,13 @@ futures.workspace = true
i18n-embed.workspace = true
i18n-embed-fl.workspace = true
image = { version = "0.25.9", default-features = false }
libcosmic.workspace = true
cosmic.workspace = true
memmap2 = "0.9.10"
fastrand = "2.3.0"
rust-embed.workspace = true
rustix.workspace = true
rustc-hash.workspace = true
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" }
switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" }
tokio.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true

View file

@ -6,5 +6,5 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
libcosmic.workspace = true
cosmic.workspace = true
serde = { workspace = true, features = ["derive"] }

View file

@ -0,0 +1,8 @@
new-window = Nova Finestra
quit = Surt
run = Executa
run-on = Executa a { $gpu }
quit-all = Surt de totes
run-on-default = (Per defecte)
cosmic-app-list = Safata d'Aplicacions
pin = Ancora a la safata

View file

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

View file

@ -5,4 +5,15 @@ quit-all = Quit All
new-window = New Window
run = Run
run-on = Run on {$gpu}
run-on-default = (Default)
run-on-default = (Default)
edit-launcher = Edit launcher
launcher-name = Name
launcher-command = Command
launcher-icon = Icon
launcher-icon-theme = Theme
launcher-icon-search = Search icons
launcher-icon-catalog-loading = Loading icons
launcher-icon-catalog-empty = No icons
launcher-icons = icons
save = Save
cancel = Cancel

View file

@ -3,6 +3,17 @@ pin = Épingler à la barre d'applis
quit = Quitter
quit-all = Tout quitter
new-window = Nouvelle fenêtre
run = Lancer
run = Exécuter
run-on = Lancer avec { $gpu }
run-on-default = (Défaut)
edit-launcher = Modifier le lanceur
launcher-name = Nom
launcher-command = Commande
launcher-icon = Icône
launcher-icon-theme = Thème
launcher-icon-search = Rechercher une icône
launcher-icon-catalog-loading = Chargement des icônes
launcher-icon-catalog-empty = Aucune icône
launcher-icons = icônes
save = Enregistrer
cancel = Annuler

View file

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

View file

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

View file

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

View file

@ -3,6 +3,6 @@ pin = Закріпити
quit = Вийти
quit-all = Закрити всі
new-window = Нове вікно
run = Запустити
run = Виконати
run-on = Запустити на { $gpu }
run-on-default = (Основна)

View file

@ -2,7 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
fl,
fl, icon_catalog,
launcher_edit::{self, LauncherEditRequest},
wayland_subscription::{
OutputUpdate, ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
wayland_subscription,
@ -43,11 +44,11 @@ use cosmic::{
surface,
theme::{self, Button, Container},
widget::{
DndDestination, Image, button, container, divider, dnd_source,
DndDestination, Image, button, container, divider, dnd_source, grid,
icon::{self, from_name},
image::Handle,
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
svg, text,
scrollable, svg, text, text_input,
},
};
use cosmic::{
@ -64,6 +65,8 @@ use tokio::time::sleep;
use url::Url;
static MIME_TYPE: &str = "text/uri-list";
const MAX_VISIBLE_ICON_CHOICES: usize = 120;
const ICON_CATALOG_COLUMNS: usize = 6;
pub fn run() -> cosmic::iced::Result {
cosmic::applet::run::<CosmicAppList>(())
@ -187,6 +190,11 @@ impl DockItem {
dot_border_radius: [f32; 4],
window_id: window::Id,
filter: Option<&dyn Fn(&ToplevelInfo) -> bool>,
// Yoda: multiplier on the computed icon size (1.0 = default,
// >1.0 = magnified e.g. on hover for the macOS Tahoe effect).
// Applied to the icon's rendered width/height only — indicator
// dot and surrounding layout stay at base size.
icon_scale: f32,
) -> Element<'_, Message> {
let Self {
toplevels,
@ -205,17 +213,35 @@ impl DockItem {
};
let toplevel_count = filtered_toplevels.len();
// Cairo-like : pastille plus petite + atténuée quand toutes les fenêtres
// de cette app sont minimisées.
let all_minimized = toplevel_count > 0
&& filtered_toplevels
.iter()
.all(|(info, _)| info.state.contains(&State::Minimized));
let app_icon = AppletIconData::new(applet);
// Yoda: scaled icon size for hover magnification. Clamped so
// tiny floats don't round to 0 and huge ones stay within u16.
let scaled_icon_size = ((f32::from(app_icon.icon_size) * icon_scale).round() as i32)
.clamp(1, u16::MAX as i32) as u16;
let cosmic_icon = cosmic::widget::icon(
fde::IconSource::from_unknown(desktop_info.icon().unwrap_or_default()).as_cosmic_icon(),
)
// sets the preferred icon size variant
.size(128)
.width(app_icon.icon_size.into())
.height(app_icon.icon_size.into());
.width(scaled_icon_size.into())
.height(scaled_icon_size.into());
let indicator = {
// Padding réduit quand minimisée → pastille plus petite.
let effective_radius = if all_minimized {
(app_icon.dot_radius * 0.55).max(1.0)
} else {
app_icon.dot_radius
};
let container = if toplevel_count <= 1 {
vertical_space().height(Length::Fixed(0.0))
} else {
@ -229,22 +255,34 @@ impl DockItem {
}
}
.apply(container)
.padding(app_icon.dot_radius);
.padding(effective_radius);
if toplevel_count == 0 {
container
} else {
container.class(theme::Container::custom(move |theme| container::Style {
background: if is_focused {
Some(Background::Color(theme.cosmic().accent_color().into()))
container.class(theme::Container::custom(move |theme| {
let cosmic = theme.cosmic();
let accent: iced::Color = cosmic.accent_color().into();
let on_bg: iced::Color = cosmic.on_bg_color().into();
// Teinte neutre atténuée quand toutes les fenêtres sont minimisées.
let muted = iced::Color { a: 0.45, ..on_bg };
let fill = if all_minimized {
muted
} else if is_focused {
accent
} else {
Some(Background::Color(theme.cosmic().on_bg_color().into()))
},
border: Border {
radius: dot_border_radius.into(),
on_bg
};
container::Style {
background: Some(Background::Color(fill)),
border: Border {
radius: dot_border_radius.into(),
..Default::default()
},
..Default::default()
},
..Default::default()
}
}))
}
};
@ -352,10 +390,35 @@ pub struct Popup {
popup_type: PopupType,
}
#[derive(Debug, Clone)]
struct LauncherEditState {
original_app_id: String,
source_path: PathBuf,
original_name: String,
original_exec: String,
name: String,
exec: String,
icon: String,
terminal: bool,
saving: bool,
error: Option<String>,
icon_catalog: IconCatalogState,
}
#[derive(Debug, Clone)]
struct IconCatalogState {
theme: String,
query: String,
entries: Vec<icon_catalog::IconCatalogEntry>,
loading: bool,
truncated: bool,
}
#[derive(Clone, Default)]
struct CosmicAppList {
core: cosmic::app::Core,
popup: Option<Popup>,
launcher_edit: Option<LauncherEditState>,
subscription_ctr: u32,
item_ctr: u32,
desktop_entries: Vec<DesktopEntry>,
@ -374,6 +437,21 @@ struct CosmicAppList {
output_list: FxHashMap<WlOutput, OutputInfo>,
locales: Vec<String>,
hovered_toplevel: Option<ExtForeignToplevelHandleV1>,
/// Yoda: which dock icon the pointer is currently over (for hover
/// magnification). None = no dock icon hovered.
hovered_dock_item: Option<DockItemId>,
/// Yoda: animated "virtual cursor" center used by the fisheye
/// formula — lerps toward the real hovered icon's center on each
/// AnimTick, so the bell curve slides smoothly from one icon to the
/// next instead of snapping.
anim_hover_center: Option<(f32, f32)>,
/// Yoda: fade-in/out intensity of the magnification effect
/// (0.0 = icons flat, 1.0 = full fisheye). Targets 1.0 while the
/// pointer is over any dock icon, 0.0 otherwise. Lerped on AnimTick.
anim_hover_intensity: f32,
/// Yoda: timestamp of the last AnimTick, for dt-based exponential
/// smoothing. `None` on first tick.
anim_last_tick: Option<std::time::Instant>,
overflow_favorites_popup: Option<window::Id>,
overflow_active_popup: Option<window::Id>,
}
@ -382,13 +460,33 @@ struct CosmicAppList {
pub enum PopupType {
RightClickMenu,
ToplevelList,
LauncherEditor,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
Wayland(WaylandUpdate),
PinApp(u32),
UnpinApp(u32),
EditLauncher(u32),
LauncherNameChanged(String),
LauncherExecChanged(String),
LauncherIconChanged(String),
LauncherIconSearchChanged(String),
LauncherIconSelected(String),
ReloadLauncherIconCatalog,
LauncherIconCatalogLoaded(icon_catalog::IconCatalog),
SaveLauncherEdit,
CancelLauncherEdit,
LauncherEditSaved(Result<launcher_edit::LauncherEditResult, String>),
/// Yoda: pointer entered (Some) or left (None) a dock icon — drives
/// the macOS Tahoe-style hover magnification effect.
DockItemHover(Option<DockItemId>),
/// Yoda: ticked at ~60fps by the animation subscription. Advances
/// anim_hover_center + anim_hover_intensity toward their targets so
/// the fisheye effect transitions smoothly instead of snapping.
AnimTick(std::time::Instant),
Popup(u32, window::Id),
Pressed(window::Id),
ToplevelListPopup(u32, window::Id),
@ -626,6 +724,143 @@ pub fn menu_control_padding() -> Padding {
[spacing.space_xxs, spacing.space_s].into()
}
fn launcher_icon_editor(edit: &LauncherEditState) -> Element<'_, Message> {
let spacing = theme::spacing();
let selected_icon = edit.icon.trim();
let query = edit.icon_catalog.query.trim().to_ascii_lowercase();
let mut total_matches = 0usize;
let mut visible_count = 0usize;
let mut icon_grid = grid()
.width(Length::Fill)
.column_spacing(spacing.space_xxs)
.row_spacing(spacing.space_xxs);
for entry in edit.icon_catalog.entries.iter().filter(|entry| {
query.is_empty() || entry.name.to_ascii_lowercase().contains(query.as_str())
}) {
total_matches += 1;
if visible_count >= MAX_VISIBLE_ICON_CHOICES {
continue;
}
if visible_count > 0 && visible_count % ICON_CATALOG_COLUMNS == 0 {
icon_grid = icon_grid.insert_row();
}
let selected = selected_icon == entry.name;
let icon_preview = cosmic::widget::icon(
fde::IconSource::from_unknown(entry.name.as_str()).as_cosmic_icon(),
)
.size(32)
.width(Length::Fixed(32.0))
.height(Length::Fixed(32.0));
let label = text::caption(entry.name.as_str())
.ellipsize(Ellipsize::End(EllipsizeHeightLimit::Lines(1)))
.width(Length::Fill)
.center();
let tile = column![icon_preview, label]
.align_x(Alignment::Center)
.spacing(4)
.width(Length::Fixed(70.0));
let tile_button = button::custom(tile)
.class(if selected {
Button::Suggested
} else {
Button::Image
})
.selected(selected)
.on_press(Message::LauncherIconSelected(entry.name.clone()))
.padding(6)
.width(Length::Fixed(74.0))
.height(Length::Fixed(76.0));
icon_grid = icon_grid.push(tile_button);
visible_count += 1;
}
let current_icon =
cosmic::widget::icon(fde::IconSource::from_unknown(edit.icon.as_str()).as_cosmic_icon())
.size(32)
.width(Length::Fixed(36.0))
.height(Length::Fixed(36.0));
let icon_value = row![
current_icon,
text_input("", edit.icon.as_str())
.label(fl!("launcher-icon"))
.on_input(Message::LauncherIconChanged)
.on_submit(|_| Message::SaveLauncherEdit)
.width(Length::Fill)
.size(14),
button::icon(from_name("view-refresh-symbolic"))
.on_press(Message::ReloadLauncherIconCatalog)
.padding(spacing.space_xxs),
]
.spacing(spacing.space_xs)
.align_y(Alignment::Center);
let visible_total = if edit.icon_catalog.truncated {
format!(
"{}/{}+ {}",
visible_count,
total_matches,
fl!("launcher-icons")
)
} else {
format!(
"{}/{} {}",
visible_count,
total_matches,
fl!("launcher-icons")
)
};
let catalog_header = row![
text::caption(format!(
"{}: {}",
fl!("launcher-icon-theme"),
edit.icon_catalog.theme
)),
horizontal_space(),
text::caption(visible_total),
]
.align_y(Alignment::Center);
let catalog_body: Element<_> = if edit.icon_catalog.loading {
container(text::body(fl!("launcher-icon-catalog-loading")))
.center(Length::Fill)
.height(Length::Fixed(220.0))
.into()
} else if total_matches == 0 {
container(text::body(fl!("launcher-icon-catalog-empty")))
.center(Length::Fill)
.height(Length::Fixed(220.0))
.into()
} else {
scrollable(icon_grid)
.height(Length::Fixed(240.0))
.width(Length::Fill)
.into()
};
column![
icon_value,
catalog_header,
text_input("", edit.icon_catalog.query.as_str())
.label(fl!("launcher-icon-search"))
.on_input(Message::LauncherIconSearchChanged)
.width(Length::Fill)
.size(14),
catalog_body,
]
.spacing(spacing.space_s)
.width(Length::Fill)
.into()
}
fn find_desktop_entries<'a>(
desktop_entries: &'a [fde::DesktopEntry],
app_ids: &'a [String],
@ -647,6 +882,79 @@ impl CosmicAppList {
.collect::<Vec<_>>();
}
/// Yoda: macOS-Tahoe fisheye-style magnification. Returns the
/// per-icon size multiplier based on the distance (in pixels) from
/// the currently hovered icon's center to this icon's center.
///
/// Uses a gaussian bell curve so the hovered icon peaks at
/// 1.0 + PEAK, immediate neighbors still bulge noticeably, and icons
/// further away relax back to 1.0× — that's the smooth neighbour
/// deformation people associate with the macOS Dock.
///
/// Falls back to binary 1.3×/1.0× when the rectangle tracker hasn't
/// populated yet (first render, or just after layout changes).
///
/// Uses the animated hover center (anim_hover_center) and intensity
/// (anim_hover_intensity) so inter-icon transitions slide smoothly
/// and the whole effect fades in/out at the dock's edges.
fn icon_scale_for(&self, id: &DockItemId) -> f32 {
const PEAK: f32 = 0.35;
// sigma expressed in multiples of the hovered icon's size —
// 1.4 means the ±1 neighbors sit ~0.7σ away and still bulge
// visibly, while ±3+ has collapsed to ~1.0× (fisheye footprint
// close to 5 icons wide, Tahoe-ish).
const SIGMA_FACTOR: f32 = 1.4;
// No intensity at all → skip the rest.
if self.anim_hover_intensity < 0.001 {
return 1.0;
}
// Prefer the animated center (smooth); fall back to the real
// hovered icon's center (first render, before tick fires).
let hover_center = self.anim_hover_center.or_else(|| {
let hovered_id = self.hovered_dock_item.as_ref()?;
let r = self.rectangles.get(hovered_id)?;
Some((r.x + r.width / 2.0, r.y + r.height / 2.0))
});
let Some(hover_center) = hover_center else {
// No coords yet — visibly peak on the exact hovered id so
// the very first frame still responds.
return if self.hovered_dock_item.as_ref() == Some(id) {
1.0 + PEAK * self.anim_hover_intensity
} else {
1.0
};
};
let this_rect = match self.rectangles.get(id) {
Some(r) => r,
None => return 1.0,
};
let is_horizontal = matches!(
self.core.applet.anchor,
PanelAnchor::Top | PanelAnchor::Bottom
);
let this_center = if is_horizontal {
this_rect.x + this_rect.width / 2.0
} else {
this_rect.y + this_rect.height / 2.0
};
let hover_axis = if is_horizontal { hover_center.0 } else { hover_center.1 };
let distance = (this_center - hover_axis).abs();
let icon_extent = if is_horizontal {
this_rect.width
} else {
this_rect.height
}
.max(1.0);
let sigma = icon_extent * SIGMA_FACTOR;
// exp(-t²) bell curve, t = distance / sigma
let t = distance / sigma;
1.0 + PEAK * self.anim_hover_intensity * (-t * t).exp()
}
fn is_on_current_monitor_and_workspace(&self, toplevel_info: &ToplevelInfo) -> bool {
use cosmic_app_list_config::ToplevelFilter;
@ -691,10 +999,45 @@ impl CosmicAppList {
.collect();
}
fn sync_pinned_list_from_config(&mut self) {
for item in self.pinned_list.drain(..) {
if !item.toplevels.is_empty() {
self.active_list.push(item);
}
}
self.pinned_list = find_desktop_entries(&self.desktop_entries, &self.config.favorites)
.zip(&self.config.favorites)
.map(|(de, original_id)| {
if let Some(p) = self
.active_list
.iter()
.position(|dock_item| dock_item.desktop_info.id() == de.id())
{
let mut d = self.active_list.remove(p);
d.desktop_info = de.clone();
d.original_app_id.clone_from(original_id);
d
} else {
self.item_ctr += 1;
DockItem {
id: self.item_ctr,
toplevels: Vec::new(),
desktop_info: de.clone(),
original_app_id: original_id.clone(),
}
}
})
.collect();
}
/// Close any open popups.
fn close_popups(&mut self) -> Task<cosmic::Action<Message>> {
let mut commands = Vec::new();
if let Some(popup) = self.popup.take() {
if popup.popup_type == PopupType::LauncherEditor {
self.launcher_edit = None;
}
commands.push(destroy_popup(popup.id));
}
if let Some(popup) = self.overflow_active_popup.take() {
@ -1068,6 +1411,198 @@ impl cosmic::Application for CosmicAppList {
return destroy_popup(popup_id);
}
}
Message::EditLauncher(id) => {
let Some(dock_item) = self.pinned_list.iter().find(|t| t.id == id).cloned() else {
return Task::none();
};
let Some(exec) = dock_item.desktop_info.exec() else {
return Task::none();
};
let Some(existing_popup) = self.popup.as_mut() else {
return Task::none();
};
let original_name = dock_item
.desktop_info
.desktop_entry("Name")
.map(ToString::to_string)
.or_else(|| {
dock_item
.desktop_info
.name(&self.locales)
.map(Cow::into_owned)
})
.unwrap_or_else(|| dock_item.original_app_id.clone());
let original_exec = exec.to_string();
let original_icon = dock_item
.desktop_info
.icon()
.unwrap_or_default()
.to_string();
let icon_theme = cosmic::icon_theme::default();
self.launcher_edit = Some(LauncherEditState {
original_app_id: dock_item.original_app_id.clone(),
source_path: dock_item.desktop_info.path.clone(),
original_name: original_name.clone(),
original_exec: original_exec.clone(),
name: original_name,
exec: original_exec,
icon: original_icon.clone(),
terminal: dock_item.desktop_info.terminal(),
saving: false,
error: None,
icon_catalog: IconCatalogState {
theme: icon_theme.clone(),
query: String::new(),
entries: Vec::new(),
loading: true,
truncated: false,
},
});
existing_popup.dock_item = dock_item;
existing_popup.popup_type = PopupType::LauncherEditor;
return Task::perform(
icon_catalog::load_icon_catalog(icon_theme, original_icon),
|catalog| cosmic::Action::App(Message::LauncherIconCatalogLoaded(catalog)),
);
}
Message::LauncherNameChanged(name) => {
if let Some(edit) = self.launcher_edit.as_mut()
&& !edit.saving
{
edit.name = name;
edit.error = None;
}
}
Message::LauncherExecChanged(exec) => {
if let Some(edit) = self.launcher_edit.as_mut()
&& !edit.saving
{
edit.exec = exec;
edit.error = None;
}
}
Message::LauncherIconChanged(icon) => {
if let Some(edit) = self.launcher_edit.as_mut()
&& !edit.saving
{
edit.icon = icon;
edit.error = None;
}
}
Message::LauncherIconSearchChanged(query) => {
if let Some(edit) = self.launcher_edit.as_mut() {
edit.icon_catalog.query = query;
}
}
Message::LauncherIconSelected(icon) => {
if let Some(edit) = self.launcher_edit.as_mut()
&& !edit.saving
{
edit.icon = icon;
edit.error = None;
}
}
Message::ReloadLauncherIconCatalog => {
if let Some(edit) = self.launcher_edit.as_mut() {
edit.icon_catalog.theme = cosmic::icon_theme::default();
edit.icon_catalog.entries.clear();
edit.icon_catalog.loading = true;
edit.icon_catalog.truncated = false;
return Task::perform(
icon_catalog::load_icon_catalog(
edit.icon_catalog.theme.clone(),
edit.icon.clone(),
),
|catalog| cosmic::Action::App(Message::LauncherIconCatalogLoaded(catalog)),
);
}
}
Message::LauncherIconCatalogLoaded(catalog) => {
if let Some(edit) = self.launcher_edit.as_mut()
&& edit.icon_catalog.theme == catalog.theme
{
edit.icon_catalog.entries = catalog.entries;
edit.icon_catalog.loading = false;
edit.icon_catalog.truncated = catalog.truncated;
}
}
Message::SaveLauncherEdit => {
let Some(edit) = self.launcher_edit.as_mut() else {
return Task::none();
};
if edit.saving {
return Task::none();
}
if let Err(error) =
launcher_edit::validate_launcher_fields(&edit.name, &edit.exec, &edit.icon)
{
edit.error = Some(error);
return Task::none();
}
let request = LauncherEditRequest {
current_app_id: edit.original_app_id.clone(),
source_path: edit.source_path.clone(),
name: edit.name.clone(),
exec: edit.exec.clone(),
icon: edit.icon.clone(),
terminal: edit.terminal,
replace_localized_name: edit.name.trim() != edit.original_name.trim(),
disable_dbus_activation: edit.exec.trim() != edit.original_exec.trim(),
};
edit.saving = true;
edit.error = None;
return Task::perform(launcher_edit::save_launcher_edit(request), |result| {
cosmic::Action::App(Message::LauncherEditSaved(result))
});
}
Message::CancelLauncherEdit => {
return self.close_popups();
}
Message::LauncherEditSaved(result) => match result {
Ok(result) => {
tracing::info!(
app_id = result.new_app_id,
path = ?result.path,
"saved editable launcher"
);
let mut favorites = self.config.favorites.clone();
let mut favorites_changed = false;
for favorite in &mut favorites {
if *favorite == result.old_app_id && *favorite != result.new_app_id {
*favorite = result.new_app_id.clone();
favorites_changed = true;
}
}
if favorites_changed {
self.config.update_pinned(
favorites.clone(),
&Config::new(APP_ID, AppListConfig::VERSION).unwrap(),
);
self.config.favorites = favorites;
}
self.update_desktop_entries();
self.sync_pinned_list_from_config();
self.launcher_edit = None;
return self.close_popups();
}
Err(error) => {
if let Some(edit) = self.launcher_edit.as_mut() {
edit.saving = false;
edit.error = Some(error);
}
}
},
Message::Activate(handle) => {
if let Some(tx) = self.wayland_sender.as_ref() {
let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
@ -1520,6 +2055,9 @@ impl cosmic::Application for CosmicAppList {
},
Message::ClosePopup => {
if let Some(p) = self.popup.take() {
if p.popup_type == PopupType::LauncherEditor {
self.launcher_edit = None;
}
return destroy_popup(p.id);
}
}
@ -1534,42 +2072,16 @@ impl cosmic::Application for CosmicAppList {
}
Message::ConfigUpdated(config) => {
self.config = config;
// drain to active list
for item in self.pinned_list.drain(..) {
if !item.toplevels.is_empty() {
self.active_list.push(item);
}
}
// pull back configured items into the favorites list
self.pinned_list =
find_desktop_entries(&self.desktop_entries, &self.config.favorites)
.zip(&self.config.favorites)
.map(|(de, original_id)| {
if let Some(p) = self
.active_list
.iter()
// match using heuristic id
.position(|dock_item| dock_item.desktop_info.id() == de.id())
{
let mut d = self.active_list.remove(p);
// but use the id from the config
d.original_app_id.clone_from(original_id);
d
} else {
self.item_ctr += 1;
DockItem {
id: self.item_ctr,
toplevels: Vec::new(),
desktop_info: de.clone(),
original_app_id: original_id.clone(),
}
}
})
.collect();
self.update_desktop_entries();
self.sync_pinned_list_from_config();
}
Message::CloseRequested(id) => {
if Some(id) == self.popup.as_ref().map(|p| p.id) {
if let Some(popup) = &self.popup
&& popup.id == id
{
if popup.popup_type == PopupType::LauncherEditor {
self.launcher_edit = None;
}
self.popup = None;
}
if self.overflow_active_popup.is_some_and(|p| p == id) {
@ -1582,6 +2094,52 @@ impl cosmic::Application for CosmicAppList {
Message::GpuRequest(gpus) => {
self.gpus = gpus;
}
Message::DockItemHover(id) => {
self.hovered_dock_item = id;
// Seed the animated center on the very first hover so
// the bell doesn't "fly in" from (0,0).
if self.anim_hover_center.is_none()
&& let Some(hovered_id) = self.hovered_dock_item.as_ref()
&& let Some(r) = self.rectangles.get(hovered_id)
{
self.anim_hover_center = Some((
r.x + r.width / 2.0,
r.y + r.height / 2.0,
));
}
}
Message::AnimTick(now) => {
// dt-based exponential smoothing: reach ~99% of the
// target in ~120ms at 60fps (about 7 ticks).
let dt = self
.anim_last_tick
.map(|prev| now.saturating_duration_since(prev).as_secs_f32())
.unwrap_or(0.016)
.min(0.1); // clamp so a long pause doesn't snap
self.anim_last_tick = Some(now);
// Intensity: target 1.0 when any icon is hovered, 0.0 else.
let intensity_target = if self.hovered_dock_item.is_some() { 1.0 } else { 0.0 };
let tau = 0.060_f32; // time-constant (s); smaller = snappier
let alpha = 1.0 - (-dt / tau).exp();
self.anim_hover_intensity += (intensity_target - self.anim_hover_intensity) * alpha;
// Hovered-center smoothing: chase the real rect's center.
if let Some(hovered_id) = self.hovered_dock_item.as_ref()
&& let Some(r) = self.rectangles.get(hovered_id)
{
let target = (r.x + r.width / 2.0, r.y + r.height / 2.0);
let current = self.anim_hover_center.unwrap_or(target);
self.anim_hover_center = Some((
current.0 + (target.0 - current.0) * alpha,
current.1 + (target.1 - current.1) * alpha,
));
} else if self.anim_hover_intensity < 0.01 {
// Nothing hovered + intensity faded out → forget the
// animated center so the next hover seeds fresh.
self.anim_hover_center = None;
}
}
Message::OpenActive => {
let create_new = self.overflow_active_popup.is_none();
let mut cmds = vec![self.close_popups()];
@ -1763,7 +2321,9 @@ impl cosmic::Application for CosmicAppList {
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
self.core
let dock_id = dock_item.id;
let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id));
let tooltip = self.core
.applet
.applet_tooltip::<Message>(
dock_item.as_icon(
@ -1776,6 +2336,7 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
icon_scale,
),
dock_item
.desktop_info
@ -1785,7 +2346,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_some(),
Message::Surface,
None,
)
);
cosmic::widget::mouse_area(tooltip)
.on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id))))
.on_exit(Message::DockItemHover(None))
.into()
})
.collect();
@ -1836,6 +2400,10 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
// Yoda: no magnification on DnD-preview icons — these
// float around as the user drags, so static 1.0 is
// less visually confusing.
1.0,
),
);
} else if self.is_listening_for_dnd && self.pinned_list.is_empty() {
@ -1875,8 +2443,10 @@ impl cosmic::Application for CosmicAppList {
.iter()
.filter(|(info, _)| self.is_on_current_monitor_and_workspace(info))
.any(|y| focused_item.contains(&y.0.foreign_toplevel));
let dock_id = dock_item.id;
let icon_scale = self.icon_scale_for(&DockItemId::from(dock_id));
self.core
let tooltip = self.core
.applet
.applet_tooltip(
dock_item.as_icon(
@ -1889,6 +2459,7 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
self.core.main_window_id().unwrap(),
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
icon_scale,
),
dock_item
.desktop_info
@ -1898,7 +2469,10 @@ impl cosmic::Application for CosmicAppList {
self.popup.is_some(),
Message::Surface,
None,
)
);
cosmic::widget::mouse_area(tooltip)
.on_enter(Message::DockItemHover(Some(DockItemId::from(dock_id))))
.on_exit(Message::DockItemHover(None))
.into()
})
.collect();
@ -2179,6 +2753,19 @@ impl cosmic::Application for CosmicAppList {
}),
);
if is_pinned && desktop_info.exec().is_some() {
content = content.push(
menu_button(
row![
icon::icon(from_name("edit-symbolic").into()).size(16),
text::body(fl!("edit-launcher"))
]
.spacing(8),
)
.on_press(Message::EditLauncher(*id)),
);
}
if !toplevels.is_empty() {
content = content.push(divider::horizontal::light());
content = match toplevels.len() {
@ -2225,6 +2812,84 @@ impl cosmic::Application for CosmicAppList {
)
.into()
}
PopupType::LauncherEditor => {
let Some(edit) = self.launcher_edit.as_ref() else {
return text::body("").into();
};
let spacing = theme::spacing();
let can_save = !edit.saving
&& launcher_edit::validate_launcher_fields(
&edit.name, &edit.exec, &edit.icon,
)
.is_ok();
let mut form = column![
text::title4(fl!("edit-launcher")),
text_input("", edit.name.as_str())
.label(fl!("launcher-name"))
.on_input(Message::LauncherNameChanged)
.on_submit(|_| Message::SaveLauncherEdit)
.width(Length::Fill)
.size(14),
text_input("", edit.exec.as_str())
.label(fl!("launcher-command"))
.on_input(Message::LauncherExecChanged)
.on_submit(|_| Message::SaveLauncherEdit)
.width(Length::Fill)
.size(14),
launcher_icon_editor(edit),
]
.spacing(spacing.space_s)
.width(Length::Fill);
if let Some(error) = edit.error.as_ref() {
form = form.push(text::caption(error.as_str()).class(
cosmic::theme::Text::Color(theme.cosmic().destructive_color().into()),
));
}
let cancel =
button::custom(text::body(fl!("cancel")).center().width(Length::Fill))
.on_press_maybe(if edit.saving {
None
} else {
Some(Message::CancelLauncherEdit)
})
.padding([spacing.space_xxs, spacing.space_s])
.width(142);
let save = button::custom(text::body(fl!("save")).center().width(Length::Fill))
.class(Button::Suggested)
.on_press_maybe(if can_save {
Some(Message::SaveLauncherEdit)
} else {
None
})
.padding([spacing.space_xxs, spacing.space_s])
.width(142);
let actions = row![horizontal_space(), cancel, save]
.spacing(spacing.space_xxs)
.align_y(Alignment::Center);
let content = column![form, actions]
.spacing(spacing.space_m)
.padding(spacing.space_m)
.width(Length::Fill);
self.core
.applet
.popup_container(container(content).width(Length::Fill))
.limits(
Limits::NONE
.min_width(480.)
.min_height(1.)
.max_width(520.)
.max_height(1000.),
)
.into()
}
PopupType::ToplevelList => match self.core.applet.anchor {
PanelAnchor::Left | PanelAnchor::Right => {
let mut content =
@ -2313,6 +2978,10 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
id,
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
// Yoda: icons in the overflow popup are
// already smaller-grid — keep them at 1.0
// so the popup doesn't reshuffle on hover.
1.0,
),
dock_item
.desktop_info
@ -2421,6 +3090,10 @@ impl cosmic::Application for CosmicAppList {
dot_radius,
id,
Some(&|info| self.is_on_current_monitor_and_workspace(info)),
// Yoda: popup icons stay at 1.0 (hover
// magnification is applied to the main
// dock row only).
1.0,
),
dock_item
.desktop_info
@ -2479,7 +3152,21 @@ impl cosmic::Application for CosmicAppList {
}
fn subscription(&self) -> Subscription<Message> {
// Yoda: ~60fps animation ticks for the fisheye magnification.
// Only emitted when an animation is actually in progress (hover
// intensity >0 OR still fading out) — keeps the panel idle when
// the pointer is nowhere near the dock.
let anim_active = self.hovered_dock_item.is_some()
|| self.anim_hover_intensity > 0.001;
let anim_subscription = if anim_active {
cosmic::iced::time::every(std::time::Duration::from_millis(16))
.map(Message::AnimTick)
} else {
Subscription::none()
};
Subscription::batch([
anim_subscription,
wayland_subscription().map(Message::Wayland),
listen_with(|e, _, id| match e {
cosmic::iced::core::Event::PlatformSpecific(event::PlatformSpecific::Wayland(

View file

@ -0,0 +1,440 @@
use std::{
cmp::Ordering,
collections::{HashMap, HashSet, VecDeque},
fs,
path::{Component, Path, PathBuf},
};
const FALLBACK_THEMES: &[&str] = &["Cosmic", "hicolor", "gnome", "Yaru"];
const MAX_THEME_CHAIN: usize = 24;
const MAX_SCAN_DEPTH: usize = 5;
const MAX_CATALOG_ENTRIES: usize = 2_500;
#[derive(Debug, Clone)]
pub struct IconCatalog {
pub theme: String,
pub entries: Vec<IconCatalogEntry>,
pub truncated: bool,
}
#[derive(Debug, Clone)]
pub struct IconCatalogEntry {
pub name: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct CandidateRank {
preferred: u8,
category: u8,
symbolic: u8,
theme_depth: usize,
extension: u8,
}
impl Ord for CandidateRank {
fn cmp(&self, other: &Self) -> Ordering {
(
self.preferred,
self.category,
self.symbolic,
self.theme_depth,
self.extension,
)
.cmp(&(
other.preferred,
other.category,
other.symbolic,
other.theme_depth,
other.extension,
))
}
}
impl PartialOrd for CandidateRank {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone)]
struct Candidate {
name: String,
rank: CandidateRank,
}
pub async fn load_icon_catalog(theme: String, preferred_icon: String) -> IconCatalog {
build_icon_catalog(theme, preferred_icon)
}
fn build_icon_catalog(theme: String, preferred_icon: String) -> IconCatalog {
let theme = if theme.trim().is_empty() {
"Cosmic".to_string()
} else {
theme
};
let preferred_name = icon_name_from_value(preferred_icon.trim());
let theme_chain = theme_chain(&theme);
let mut candidates = HashMap::new();
for (theme_depth, theme_name) in theme_chain.iter().enumerate() {
for theme_dir in theme_dirs(theme_name) {
scan_icon_tree(
&theme_dir,
theme_depth,
preferred_name.as_deref(),
&mut candidates,
);
}
}
for pixmap_dir in pixmap_dirs() {
scan_icon_tree(
&pixmap_dir,
theme_chain.len() + 1,
preferred_name.as_deref(),
&mut candidates,
);
}
let mut entries = candidates.into_values().collect::<Vec<_>>();
entries.sort_by(|a, b| a.rank.cmp(&b.rank).then_with(|| a.name.cmp(&b.name)));
let truncated = entries.len() > MAX_CATALOG_ENTRIES;
entries.truncate(MAX_CATALOG_ENTRIES);
IconCatalog {
theme,
entries: entries
.into_iter()
.map(|candidate| IconCatalogEntry {
name: candidate.name,
})
.collect(),
truncated,
}
}
fn theme_chain(theme: &str) -> Vec<String> {
let mut queue = VecDeque::from([theme.to_string()]);
let mut seen = HashSet::new();
let mut chain = Vec::new();
while let Some(theme_name) = queue.pop_front() {
let key = theme_name.to_ascii_lowercase();
if !seen.insert(key) {
continue;
}
chain.push(theme_name.clone());
if chain.len() >= MAX_THEME_CHAIN {
break;
}
for parent in read_theme_inherits(&theme_name) {
queue.push_back(parent);
}
}
for fallback in FALLBACK_THEMES {
if chain.len() >= MAX_THEME_CHAIN {
break;
}
let key = fallback.to_ascii_lowercase();
if seen.insert(key) {
chain.push((*fallback).to_string());
}
}
chain
}
fn read_theme_inherits(theme: &str) -> Vec<String> {
theme_dirs(theme)
.into_iter()
.find_map(|dir| fs::read_to_string(dir.join("index.theme")).ok())
.map(|contents| parse_inherits(&contents))
.unwrap_or_default()
}
fn parse_inherits(contents: &str) -> Vec<String> {
let mut in_icon_theme = false;
for raw_line in contents.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
in_icon_theme = line == "[Icon Theme]";
continue;
}
if in_icon_theme && let Some(value) = line.strip_prefix("Inherits=") {
return value
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.collect();
}
}
Vec::new()
}
fn scan_icon_tree(
root: &Path,
theme_depth: usize,
preferred_name: Option<&str>,
candidates: &mut HashMap<String, Candidate>,
) {
let mut stack = vec![(root.to_path_buf(), 0usize)];
while let Some((dir, depth)) = stack.pop() {
if depth > MAX_SCAN_DEPTH {
continue;
}
let Ok(entries) = fs::read_dir(&dir) else {
continue;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() {
stack.push((path, depth + 1));
} else if (file_type.is_file() || file_type.is_symlink())
&& let Some(candidate) = candidate_from_path(&path, theme_depth, preferred_name)
{
insert_candidate(candidates, candidate);
}
}
}
}
fn insert_candidate(candidates: &mut HashMap<String, Candidate>, candidate: Candidate) {
let key = candidate.name.to_ascii_lowercase();
match candidates.get_mut(&key) {
Some(existing) if candidate.rank < existing.rank => {
*existing = candidate;
}
None => {
candidates.insert(key, candidate);
}
_ => {}
}
}
fn candidate_from_path(
path: &Path,
theme_depth: usize,
preferred_name: Option<&str>,
) -> Option<Candidate> {
let extension = extension_rank(path)?;
let name = path.file_stem()?.to_str()?.trim();
if name.is_empty() {
return None;
}
let symbolic = u8::from(name.ends_with("-symbolic") || path_has_component(path, "symbolic"));
let preferred = u8::from(preferred_name != Some(name));
Some(Candidate {
name: name.to_string(),
rank: CandidateRank {
preferred,
category: category_rank(path),
symbolic,
theme_depth,
extension,
},
})
}
fn extension_rank(path: &Path) -> Option<u8> {
match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
"svg" => Some(0),
"png" => Some(1),
"xpm" => Some(2),
_ => None,
}
}
fn category_rank(path: &Path) -> u8 {
for component in path.components().filter_map(component_str) {
match component {
"apps" | "applications" => return 0,
"categories" => return 1,
"places" => return 2,
"devices" => return 3,
"mimetypes" => return 4,
"actions" => return 5,
"status" => return 6,
_ => {}
}
}
7
}
fn path_has_component(path: &Path, needle: &str) -> bool {
path.components()
.filter_map(component_str)
.any(|component| component == needle)
}
fn component_str(component: Component<'_>) -> Option<&str> {
component.as_os_str().to_str()
}
fn icon_name_from_value(value: &str) -> Option<String> {
if value.is_empty() {
return None;
}
let path = Path::new(value);
if value.contains('/') {
return path
.file_stem()
.and_then(|name| name.to_str())
.map(ToOwned::to_owned);
}
path.file_stem()
.and_then(|name| name.to_str())
.map(ToOwned::to_owned)
.or_else(|| Some(value.to_string()))
}
fn theme_dirs(theme: &str) -> Vec<PathBuf> {
icon_base_dirs()
.into_iter()
.map(|base| base.join(theme))
.filter(|path| path.is_dir())
.collect()
}
fn icon_base_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(home) = std::env::home_dir() {
push_existing_unique(&mut dirs, home.join(".icons"));
}
if let Some(data_home) = xdg_data_home() {
push_existing_unique(&mut dirs, data_home.join("icons"));
}
for data_dir in xdg_data_dirs() {
push_existing_unique(&mut dirs, data_dir.join("icons"));
}
dirs
}
fn pixmap_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(data_home) = xdg_data_home() {
push_existing_unique(&mut dirs, data_home.join("pixmaps"));
}
for data_dir in xdg_data_dirs() {
push_existing_unique(&mut dirs, data_dir.join("pixmaps"));
}
push_existing_unique(&mut dirs, PathBuf::from("/usr/share/pixmaps"));
dirs
}
fn xdg_data_home() -> Option<PathBuf> {
std::env::var_os("XDG_DATA_HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::home_dir().map(|home| home.join(".local/share")))
}
fn xdg_data_dirs() -> Vec<PathBuf> {
std::env::var_os("XDG_DATA_DIRS")
.filter(|value| !value.is_empty())
.map(|value| std::env::split_paths(&value).collect())
.unwrap_or_else(|| {
vec![
PathBuf::from("/usr/local/share"),
PathBuf::from("/usr/share"),
]
})
}
fn push_existing_unique(dirs: &mut Vec<PathBuf>, path: PathBuf) {
if path.exists() && !dirs.iter().any(|existing| existing == &path) {
dirs.push(path);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_inherits_only_from_icon_theme_section() {
let contents = r#"
[Other]
Inherits=Wrong
[Icon Theme]
Name=Demo
Inherits=Cosmic, hicolor , Adwaita
"#;
assert_eq!(parse_inherits(contents), ["Cosmic", "hicolor", "Adwaita"]);
}
#[test]
fn normalizes_icon_names_from_plain_names_and_paths() {
assert_eq!(icon_name_from_value("firefox").as_deref(), Some("firefox"));
assert_eq!(
icon_name_from_value("/usr/share/icons/hicolor/scalable/apps/firefox.svg").as_deref(),
Some("firefox")
);
}
#[test]
fn keeps_the_best_duplicate_candidate() {
let mut candidates = HashMap::new();
insert_candidate(
&mut candidates,
Candidate {
name: "demo".to_string(),
rank: CandidateRank {
preferred: 1,
category: 7,
symbolic: 1,
theme_depth: 4,
extension: 2,
},
},
);
insert_candidate(
&mut candidates,
Candidate {
name: "demo".to_string(),
rank: CandidateRank {
preferred: 0,
category: 0,
symbolic: 0,
theme_depth: 0,
extension: 0,
},
},
);
assert_eq!(candidates["demo"].rank.preferred, 0);
assert_eq!(candidates["demo"].rank.category, 0);
}
}

View file

@ -0,0 +1,378 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::{
env,
ffi::OsString,
io::ErrorKind,
path::{Path, PathBuf},
};
use tokio::fs;
#[derive(Clone, Debug)]
pub struct LauncherEditRequest {
pub current_app_id: String,
pub source_path: PathBuf,
pub name: String,
pub exec: String,
pub icon: String,
pub terminal: bool,
pub replace_localized_name: bool,
pub disable_dbus_activation: bool,
}
#[derive(Clone, Debug)]
pub struct LauncherEditResult {
pub old_app_id: String,
pub new_app_id: String,
pub path: PathBuf,
}
#[derive(Clone, Debug)]
struct ValidLauncherEdit {
current_app_id: String,
source_path: PathBuf,
name: String,
exec: String,
icon: String,
terminal: bool,
replace_localized_name: bool,
disable_dbus_activation: bool,
}
pub fn validate_launcher_fields(name: &str, exec: &str, icon: &str) -> Result<(), String> {
validate_required("Name", name)?;
validate_required("Command", exec)?;
validate_optional("Icon", icon)?;
Ok(())
}
pub async fn save_launcher_edit(
request: LauncherEditRequest,
) -> Result<LauncherEditResult, String> {
let request = validate_request(request)?;
let (new_app_id, target_path) =
target_launcher_path(&request.current_app_id, &request.source_path)?;
let source = read_source_desktop_entry(&request).await?;
let rendered = render_editable_desktop_entry(&source, &request);
let Some(parent) = target_path.parent() else {
return Err("Desktop file target has no parent directory".to_string());
};
fs::create_dir_all(parent)
.await
.map_err(|err| format!("Could not create applications directory: {err}"))?;
let tmp_path = temporary_path(&target_path)?;
fs::write(&tmp_path, rendered)
.await
.map_err(|err| format!("Could not write temporary desktop file: {err}"))?;
if let Err(err) = fs::rename(&tmp_path, &target_path).await {
let _ = fs::remove_file(&tmp_path).await;
return Err(format!("Could not install desktop file: {err}"));
}
Ok(LauncherEditResult {
old_app_id: request.current_app_id,
new_app_id,
path: target_path,
})
}
fn validate_request(request: LauncherEditRequest) -> Result<ValidLauncherEdit, String> {
let name = validate_required("Name", &request.name)?;
let exec = validate_required("Command", &request.exec)?;
let icon = validate_optional("Icon", &request.icon)?;
Ok(ValidLauncherEdit {
current_app_id: request.current_app_id,
source_path: request.source_path,
name,
exec,
icon,
terminal: request.terminal,
replace_localized_name: request.replace_localized_name,
disable_dbus_activation: request.disable_dbus_activation,
})
}
fn validate_required(label: &str, value: &str) -> Result<String, String> {
let value = validate_optional(label, value)?;
if value.is_empty() {
return Err(format!("{label} cannot be empty"));
}
Ok(value)
}
fn validate_optional(label: &str, value: &str) -> Result<String, String> {
if value.contains('\0') || value.contains('\n') || value.contains('\r') {
return Err(format!("{label} cannot contain line breaks"));
}
Ok(value.trim().to_string())
}
async fn read_source_desktop_entry(request: &ValidLauncherEdit) -> Result<String, String> {
if request.source_path.as_os_str().is_empty() {
return Ok(minimal_desktop_entry());
}
match fs::read_to_string(&request.source_path).await {
Ok(source) => Ok(source),
Err(err) if err.kind() == ErrorKind::NotFound => Ok(minimal_desktop_entry()),
Err(err) => Err(format!("Could not read source desktop file: {err}")),
}
}
fn minimal_desktop_entry() -> String {
"[Desktop Entry]\nType=Application\n".to_string()
}
fn user_applications_dir() -> Result<PathBuf, String> {
if let Some(xdg_data_home) = env::var_os("XDG_DATA_HOME").filter(|value| !value.is_empty()) {
return Ok(PathBuf::from(xdg_data_home).join("applications"));
}
let Some(home) = env::var_os("HOME").filter(|value| !value.is_empty()) else {
return Err("Neither XDG_DATA_HOME nor HOME is set".to_string());
};
Ok(PathBuf::from(home).join(".local/share/applications"))
}
fn target_launcher_path(app_id: &str, source_path: &Path) -> Result<(String, PathBuf), String> {
let applications_dir = user_applications_dir()?;
if source_path.starts_with(&applications_dir) {
return Ok((app_id.to_string(), source_path.to_path_buf()));
}
let desktop_id = normalize_desktop_id(app_id)?;
Ok((
desktop_id.clone(),
applications_dir.join(format!("{desktop_id}.desktop")),
))
}
fn normalize_desktop_id(app_id: &str) -> Result<String, String> {
if app_id.contains('\0') || app_id.contains('\n') || app_id.contains('\r') {
return Err("Desktop ID cannot contain line breaks".to_string());
}
let desktop_id = app_id
.chars()
.map(|c| if c == '/' { '-' } else { c })
.collect::<String>();
if desktop_id.trim().is_empty() {
return Err("Desktop ID cannot be empty".to_string());
}
Ok(desktop_id)
}
fn temporary_path(target_path: &Path) -> Result<PathBuf, String> {
let Some(file_name) = target_path.file_name() else {
return Err("Desktop file target has no filename".to_string());
};
let mut tmp_file_name = OsString::from(".");
tmp_file_name.push(file_name);
tmp_file_name.push(format!(".tmp-{}", std::process::id()));
Ok(target_path.with_file_name(tmp_file_name))
}
fn render_editable_desktop_entry(source: &str, request: &ValidLauncherEdit) -> String {
let mut updates = vec![
("Type", "Application".to_string()),
("Name", request.name.clone()),
("Exec", request.exec.clone()),
("Icon", request.icon.clone()),
(
"Terminal",
if request.terminal { "true" } else { "false" }.to_string(),
),
("X-COSMIC-UserEditable", "true".to_string()),
("X-COSMIC-SourceDesktopId", request.current_app_id.clone()),
];
if !request.source_path.as_os_str().is_empty() {
updates.push((
"X-COSMIC-SourceDesktopPath",
request.source_path.to_string_lossy().to_string(),
));
}
if request.disable_dbus_activation {
updates.push(("DBusActivatable", "false".to_string()));
}
set_desktop_entry_keys(
source,
&updates,
request.replace_localized_name,
request.disable_dbus_activation,
)
}
fn set_desktop_entry_keys(
source: &str,
updates: &[(&str, String)],
replace_localized_name: bool,
remove_try_exec: bool,
) -> String {
let mut out = Vec::new();
let mut seen = vec![false; updates.len()];
let mut in_desktop_entry = false;
let mut saw_desktop_entry = false;
for line in source.lines() {
if let Some(section) = section_name(line) {
if in_desktop_entry {
insert_missing_keys(&mut out, updates, &seen);
}
in_desktop_entry = section == "Desktop Entry";
saw_desktop_entry |= in_desktop_entry;
if in_desktop_entry {
seen.fill(false);
}
out.push(line.to_string());
continue;
}
if in_desktop_entry && let Some(key) = desktop_entry_key(line) {
if replace_localized_name && key.starts_with("Name[") {
continue;
}
if remove_try_exec && key == "TryExec" {
continue;
}
if let Some(index) = updates
.iter()
.position(|(update_key, _)| *update_key == key)
{
out.push(format!("{}={}", updates[index].0, updates[index].1));
seen[index] = true;
continue;
}
}
out.push(line.to_string());
}
if in_desktop_entry {
insert_missing_keys(&mut out, updates, &seen);
}
if !saw_desktop_entry {
let mut with_desktop_entry = Vec::with_capacity(out.len() + updates.len() + 2);
with_desktop_entry.push("[Desktop Entry]".to_string());
insert_missing_keys(
&mut with_desktop_entry,
updates,
&vec![false; updates.len()],
);
if !out.is_empty() {
with_desktop_entry.push(String::new());
with_desktop_entry.extend(out);
}
out = with_desktop_entry;
}
let mut rendered = out.join("\n");
rendered.push('\n');
rendered
}
fn insert_missing_keys(out: &mut Vec<String>, updates: &[(&str, String)], seen: &[bool]) {
for (index, (key, value)) in updates.iter().enumerate() {
if !seen[index] {
out.push(format!("{key}={value}"));
}
}
}
fn section_name(line: &str) -> Option<&str> {
let trimmed = line.trim();
trimmed
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
}
fn desktop_entry_key(line: &str) -> Option<&str> {
let line = line.trim_start();
if line.starts_with('#') || line.starts_with(';') {
return None;
}
line.split_once('=').map(|(key, _)| key.trim_end())
}
#[cfg(test)]
mod tests {
use super::*;
fn request() -> ValidLauncherEdit {
ValidLauncherEdit {
current_app_id: "org.example.App".to_string(),
source_path: PathBuf::from("/usr/share/applications/org.example.App.desktop"),
name: "Example".to_string(),
exec: "example --new".to_string(),
icon: "example-custom".to_string(),
terminal: false,
replace_localized_name: true,
disable_dbus_activation: true,
}
}
#[test]
fn updates_desktop_entry_without_dropping_action_groups() {
let source = "\
[Desktop Entry]
Type=Application
Name=Old
Name[fr]=Ancien
Exec=old
TryExec=old
Icon=old
DBusActivatable=true
Actions=new-window;
[Desktop Action new-window]
Name=New Window
Exec=old --new-window
";
let rendered = render_editable_desktop_entry(source, &request());
assert!(rendered.contains("Name=Example\n"));
assert!(!rendered.contains("Name[fr]="));
assert!(rendered.contains("Exec=example --new\n"));
assert!(rendered.contains("Icon=example-custom\n"));
assert!(rendered.contains("DBusActivatable=false\n"));
assert!(!rendered.contains("TryExec="));
assert!(rendered.contains("[Desktop Action new-window]\n"));
assert!(rendered.contains("Exec=old --new-window\n"));
}
#[test]
fn preserves_localized_names_when_name_is_unchanged() {
let mut edit = request();
edit.replace_localized_name = false;
let rendered = render_editable_desktop_entry(
"[Desktop Entry]\nName=Example\nName[fr]=Exemple\nExec=old\n",
&edit,
);
assert!(rendered.contains("Name=Example\n"));
assert!(rendered.contains("Name[fr]=Exemple\n"));
}
}

View file

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

View file

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

View file

@ -9,7 +9,7 @@ cctk.workspace = true
cosmic-protocols.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
rust-embed.workspace = true
tokio.workspace = true
tracing-log.workspace = true
@ -17,9 +17,7 @@ tracing-subscriber.workspace = true
tracing.workspace = true
[dependencies.cosmic-settings-a11y-manager-subscription]
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/a11y-manager"
path = "../../cosmic-settings/subscriptions/a11y-manager"
[dependencies.cosmic-settings-accessibility-subscription]
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/accessibility"
path = "../../cosmic-settings/subscriptions/accessibility"

View file

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

View file

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

View file

@ -26,10 +26,9 @@ use cosmic::{
};
use cosmic_settings_a11y_manager_subscription::{
self as cosmic_a11y_manager, AccessibilityEvent, AccessibilityRequest, ColorFilter,
AccessibilityEvent, AccessibilityRequest, ColorFilter,
};
use cosmic_settings_accessibility_subscription::{self as accessibility};
use std::sync::LazyLock;
use tokio::sync::mpsc::UnboundedSender;
pub fn run() -> cosmic::iced::Result {
@ -52,6 +51,7 @@ struct CosmicA11yApplet {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
TogglePopup,
CloseRequested(window::Id),

View file

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

View file

@ -7,9 +7,8 @@ license = "GPL-3.0-only"
[dependencies]
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
mpris2-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
# mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
cosmic.workspace = true
mpris2-zbus = { path = "../../dbus-settings-bindings/mpris2" }
rust-embed.workspace = true
serde.workspace = true
tokio.workspace = true
@ -21,5 +20,4 @@ urlencoding = "2.1.3"
zbus.workspace = true
[dependencies.cosmic-settings-sound-subscription]
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/sound"
path = "../../cosmic-settings/subscriptions/sound"

View file

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

View file

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

View file

@ -487,11 +487,31 @@ impl cosmic::Application for Audio {
.icon_button(self.output_icon_name())
.on_press_down(Message::TogglePopup);
const WHEEL_STEP: f32 = 5.0; // 5% per wheel event
const WHEEL_STEP: f32 = 5.0; // 5% par cran logique
// Wayland axis_v120 envoie un cran physique en rafale de plusieurs
// ScrollDelta::Pixels (58 events ~1520px), pour ~120px par cran. On
// accumule ces sub-events dans un thread_local et on n'émet qu'au
// passage d'un seuil — sinon `signum()` faisait croire que chaque
// sub-event = un cran, et un seul cran physique faisait chuter le
// volume jusqu'à 0 ("coupe le son").
const PIXEL_THRESHOLD: f32 = 15.0; // px par cran logique
std::thread_local! {
static PIXEL_ACC: std::cell::Cell<f32> = const { std::cell::Cell::new(0.0) };
}
let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
let scroll_vector = match delta {
iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1
iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1
iced::mouse::ScrollDelta::Lines { y, .. } => {
PIXEL_ACC.with(|a| a.set(0.0));
y * WHEEL_STEP
}
iced::mouse::ScrollDelta::Pixels { y, .. } => {
PIXEL_ACC.with(|acc_cell| {
let acc = acc_cell.get() + y;
let steps = (acc / PIXEL_THRESHOLD).trunc();
acc_cell.set(acc - steps * PIXEL_THRESHOLD);
steps * WHEEL_STEP
})
}
};
if scroll_vector == 0.0 {
return Message::Ignore;
@ -499,7 +519,7 @@ impl cosmic::Application for Audio {
let new_volume = (self.model.sink_volume as f64 + (scroll_vector as f64))
.clamp(0.0, self.max_sink_volume as f64);
Message::SetSinkVolume(new_volume as u32)
Message::SetSinkVolume(new_volume.round() as u32)
});
let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {

View file

@ -5,7 +5,7 @@ use cosmic::iced::core::Point;
use cosmic::iced::core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
event::{self, Event},
event::Event,
layout, mouse, overlay, renderer, touch,
widget::{Operation, Tree, tree},
};

View file

@ -12,7 +12,7 @@ drm = "0.14.1"
futures.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
rust-embed.workspace = true
rustc-hash.workspace = true
tokio.workspace = true
@ -24,9 +24,7 @@ zbus.workspace = true
serde.workspace = true
[dependencies.cosmic-settings-upower-subscription]
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/upower"
path = "../../cosmic-settings/subscriptions/upower"
[dependencies.cosmic-settings-daemon-subscription]
git = "https://github.com/pop-os/cosmic-settings"
# path = "../../cosmic-settings/subscriptions/settings-daemon"
path = "../../cosmic-settings/subscriptions/settings-daemon"

View file

@ -0,0 +1,14 @@
battery = Μπαταρία
power-settings = Ρυθμίσεις ενέργειας και μπαταρίας...
hours = ώ
until-empty = μέχρι την αποφόρτιση
battery-desc = Μειωμένη χρήση ενέργειας και επιδόσεις.
max-charge = Επεκτείνετε τη διάρκεια ζωής της μπαταρίας σας ορίζοντας ως μέγιστη τιμή φόρτισης το 80%
balanced = Ισορροπημένη
seconds = δ
dgpu-running = Η ανεξάρτητη GPU είναι ενεργή και μπορεί να μειώσει τη διάρκεια ζωής της μπαταρίας
performance = Υψηλές επιδόσεις
performance-desc = Υψηλές επιδόσεις και χρήση ενέργειας.
minutes = λ
balanced-desc = Τυπικές επιδόσεις και χρήση μπαταρίας.
dgpu-applications = Εφαρμογές που χρησιμοποιούν την ανεξάρτητη GPU { $gpu_name }

View file

@ -0,0 +1,14 @@
power-settings = Подешавања напајања и батерије...
hours = ч
until-empty = до празне
battery-desc = Смањен учинак и употреба струје.
balanced = Уравнотежено
battery = Батерија
seconds = с
performance = Најбрже
performance-desc = Висок учинак и употреба струје.
minutes = м
balanced-desc = Уобичајени учинак и употреба батерије.
max-charge = Повећајте век трајања батерије подешавањем највише вредности пуњења на 80%
dgpu-running = Засебна графичка је покренута и може умањити век трајања батерије
dgpu-applications = Програми који користе засебну графичку { $gpu_name }

View file

@ -1,4 +1,4 @@
battery =
battery = 電
battery-desc = 降低效能與耗電量。
balanced = 平衡
balanced-desc = 標準效能與耗電量。

View file

@ -40,7 +40,7 @@ use cosmic_settings_upower_subscription::{
};
use rustc_hash::FxHashMap;
use std::{path::PathBuf, sync::LazyLock, time::Duration};
use std::{path::PathBuf, time::Duration};
use tokio::sync::mpsc::UnboundedSender;
// XXX improve
@ -172,6 +172,7 @@ impl CosmicBatteryApplet {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
TogglePopup,
CloseRequested(window::Id),
@ -368,6 +369,10 @@ impl cosmic::Application for CosmicBatteryApplet {
let _ = tx.send(());
}
let mut tasks = vec![get_popup(popup_settings)];
if let Some(tx) = &self.settings_daemon_sender {
let _ = tx.send(settings_daemon::Request::GetDisplayBrightness);
let _ = tx.send(settings_daemon::Request::GetMaxDisplayBrightness);
}
// Try again every time a popup is opened
if self.charging_limit.is_none() {
tasks.push(Task::perform(get_charging_limit(), |limit| {
@ -507,7 +512,7 @@ impl cosmic::Application for CosmicBatteryApplet {
Task::none()
}
fn view(&self) -> Element<Message> {
fn view(&self) -> Element<'_, Message> {
let is_horizontal = match self.core.applet.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => true,
PanelAnchor::Left | PanelAnchor::Right => false,

View file

@ -10,7 +10,7 @@ bluer = { version = "0.17", features = ["bluetoothd", "id"] }
futures.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
fastrand = "2.3.0"
rust-embed.workspace = true
rustc-hash.workspace = true

View file

@ -0,0 +1,12 @@
confirm-pin = Si us plau, verifiqueu que el següent PIN coincideix amb el mostrat a { $deviceName }
bluetooth = Bluetooth
unsuccessful = Error d'Emparellament
pairable = Emparellable
try-again = Reintentar
check-device = Si us plau, verifiqueu que { $deviceName } estigui encès, a l'abast i llest per emparellar-se.
other-devices = Altres dispositius Bluetooth
cancel = Cancel·lar
connected = Connectat
confirm = Confirmar
settings = Configuració del Bluetooth...
discoverable = Descobrible

View file

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

View file

@ -0,0 +1,12 @@
confirm-pin = Потврдите да се овај ПИН подудара са оним приказаним на { $deviceName }
bluetooth = Блутут
unsuccessful = Неуспешно упаривање
pairable = Упарив
try-again = Покушај поново
check-device = Постарајте се да је { $deviceName } укључен, у домету и спреман за упаривање.
other-devices = Други блутут уређаји
cancel = Откажи
connected = Повезано
confirm = Потврди
settings = Подешавања блутута...
discoverable = Откривљив

View file

@ -24,7 +24,7 @@ use cosmic::{
widget::{button, divider, icon, scrollable, text},
};
use futures::FutureExt;
use std::{collections::HashMap, sync::LazyLock, time::Duration};
use std::{collections::HashMap, time::Duration};
use tokio::sync::mpsc::Sender;
use crate::{
@ -63,6 +63,7 @@ impl CosmicBluetoothApplet {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
TogglePopup,
CloseRequested(window::Id),
@ -145,7 +146,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
}
Message::BluetoothEvent(e) => match e {
BluerEvent::RequestResponse {
req,
req: _,
state,
err_msg,
} => {

View file

@ -149,8 +149,6 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
},
BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
_ => return,
};
_ = output.send(message).await;
@ -199,6 +197,7 @@ pub enum BluerRequest {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum BluerEvent {
RequestResponse {
req: BluerRequest,

View file

@ -5,10 +5,10 @@ edition = "2024"
license = "GPL-3.0-only"
[dependencies]
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
cosmic-comp-config.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
rust-embed.workspace = true
tokio.workspace = true
tracing-log.workspace = true

View file

@ -0,0 +1,2 @@
show-keyboard-layout = Εμφάνιση διάταξης πληκτρολογίου...
keyboard-settings = Ρυθμίσεις πληκτρολογίου...

View file

@ -0,0 +1,2 @@
show-keyboard-layout = Прикажи распоред тастатуре...
keyboard-settings = Подешавања тастатуре...

View file

@ -3,7 +3,6 @@
mod localize;
use cosmic::iced::{Alignment, Length};
use cosmic::{
app,
app::Core,
@ -14,16 +13,14 @@ use cosmic::{
iced::{
Rectangle, Task,
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
widget::{column, row},
window::Id,
},
iced::{core::window, runtime::Appearance},
iced::core::window,
prelude::*,
surface, theme,
widget::{
self, autosize,
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
space,
},
};
use cosmic_comp_config::CosmicCompConfig;

View file

@ -9,7 +9,7 @@ anyhow.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
image = { version = "0.25.9", default-features = false }
libcosmic.workspace = true
cosmic.workspace = true
memmap2 = "0.9.10"
rust-embed.workspace = true
rustix.workspace = true

View file

@ -316,7 +316,7 @@ impl cosmic::Application for Minimize {
} else {
(cross_padding, major_padding)
};
let theme = self.core.system_theme().cosmic();
let _theme = self.core.system_theme().cosmic();
let icon_buttons = self.apps[..max_icon_count].iter().map(|app| {
self.core
.applet
@ -420,7 +420,7 @@ impl cosmic::Application for Minimize {
(cross_padding, major_padding)
};
let theme = self.core.system_theme().cosmic();
let space_xxs = theme.space_xxs();
let _space_xxs = theme.space_xxs();
let icon_buttons = self.apps[max_icon_count..].iter().map(|app| {
tooltip(
Element::from(crate::window_image::WindowImage::new(

View file

@ -7,12 +7,12 @@ license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
async-fn-stream = "0.3"
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-dbus-networkmanager = { path = "../../dbus-settings-bindings/networkmanager" }
futures.workspace = true
futures-util.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic = { workspace = true, features = [
cosmic = { workspace = true, features = [
"applet",
"applet-token",
"tokio",
@ -26,16 +26,12 @@ tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
zbus.workspace = true
nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings/" }
indexmap = "2.13.0"
secure-string = "0.3.0"
uuid = { version = "1.21.0", features = ["v4"] }
nmrs = "3.1.3"
[dependencies.cosmic-settings-network-manager-subscription]
git = "https://github.com/pop-os/cosmic-settings/"
# path = "../../cosmic-settings/subscriptions/network-manager"
[dependencies.cosmic-settings-airplane-mode-subscription]
git = "https://github.com/pop-os/cosmic-settings/"
# path = "../../cosmic-settings/subscriptions/airplane-mode"
path = "../../cosmic-settings/subscriptions/network-manager"

View file

@ -0,0 +1,4 @@
cancel = Cancel·lar
connected = Connectat
wifi = Wi-Fi
identity = Identitat

View file

@ -1 +1,24 @@
cancel = Ακύρωση
connect = Σύνδεση
network = Δίκτυο
check-wifi-connection = Βεβαιωθείτε ότι το Wi-Fi είναι συνδεδεμένο στο διαδίκτυο και ότι ο κωδικός πρόσβασης είναι σωστός
reset = Επαναφορά
visible-wireless-networks = Ορατά ασύρματα δίκτυα
enter-password = Εισαγάγετε τον κωδικό πρόσβασης ή το κλειδί κρυπτογράφησης
ipv6 = Διεύθυνση IPv6
airplane-mode-on = Η λειτουργία πτήσης είναι ενεργή
connecting = Σύνδεση
airplane-mode = Λειτουργία πτήσης
wifi = Wi-Fi
ipv4 = Διεύθυνση IPv4
identity = Ταυτότητα
unable-to-connect = Δεν είναι δυνατή η σύνδεση στο δίκτυο
turn-off-airplane-mode = Απενεργοποιήστε τη για ενεργοποίηση του Wi-Fi, του Bluetooth και της κινητής ευρυζωνικής σύνδεσης.
connected = Συνδέθηκε
vpn-connections = Συνδέσεις VPN
mac = MAC
router-wps-button = Μπορείτε επίσης να συνδεθείτε πατώντας το κουμπί «WPS» του δρομολογητή
settings = Ρυθμίσεις δικτύου...
megabits-per-second = Mbps
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -0,0 +1,24 @@
cancel = Откажи
connected = Повезано
connect = Повежи
identity = Идентитет
wifi = Бежична
check-wifi-connection = Уверите се да је бежична мрежа повезана са интернетом и да је лозинка тачна
reset = Врати
visible-wireless-networks = Видљиве бежичне мреже
enter-password = Унесите лозинку или кључ за шифровање
ipv6 = ИПв6 адреса
airplane-mode-on = Авионски режим је укључен
connecting = Повезује се
airplane-mode = Авио режим
ipv4 = ИПв4 адреса
unable-to-connect = Немогуће повезати се са мрежом
turn-off-airplane-mode = Искључите да бисте омогућили Блутут, бежичну и мобилни интернет.
network = Мрежа
vpn-connections = ВПН везе
mac = МАК
router-wps-button = Такође се можете повезати притиском на дугме „WPS“ на рутеру
settings = Подешавања мреже...
megabits-per-second = Mbps
gigabits-per-second = Gbps
terabits-per-second = Tbps

View file

@ -8,15 +8,15 @@ ipv6 = IPv6-адреса
mac = MAC
megabits-per-second = Мбіт/с
connected = З'єднано
connecting = Підключення
connect = З'єднати
connecting = З’єднання
connect = Зєднати
cancel = Скасувати
settings = Налаштування мережі...
visible-wireless-networks = Видимі бездротові мережі
enter-password = Введіть пароль або ключ шифрування
router-wps-button = Також можна підключитися, натиснувши кнопку «WPS» на маршрутизаторі
unable-to-connect = Не вдалося підключитися до мережі
check-wifi-connection = Переконайтеся, що Wi-Fi підключено до Інтернету, а пароль — правильний
router-wps-button = Також можна під’єднатися, натиснувши кнопку «WPS» на маршрутизаторі
unable-to-connect = Не вдалося під’єднатися до мережі
check-wifi-connection = Переконайтеся, що Wi-Fi під’єднано до Інтернету, а пароль — правильний
reset = Скинути
identity = Ідентичність
vpn-connections = VPN-з'єднання

File diff suppressed because it is too large Load diff

View file

@ -8,7 +8,7 @@ license = "GPL-3.0-only"
cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" }
cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" }
anyhow.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
tokio.workspace = true
# cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" }
# cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" }

View file

@ -0,0 +1,19 @@
hours-ago =
{ $duration ->
[0] Μόλις τώρα
[one] Πριν από 1 ώρα
*[other] Πριν από { $duration } ώρες
}
show-more = Εμφάνιση { $more } ακόμη
clear-all = Απαλοιφή όλων των ειδοποιήσεων
notification-settings = Ρυθμίσεις ειδοποιήσεων...
no-notifications = Καμία ειδοποίηση
do-not-disturb = Μην ενοχλείτε
show-less = Εμφάνιση λιγότερων
clear-group = Απαλοιφή ομάδας
minutes-ago =
{ $duration ->
[0] Μόλις τώρα
[one] Πριν από 1 λεπτό
*[other] Πριν από { $duration } λεπτά
}

View file

@ -0,0 +1,19 @@
show-more = Прикажи још { $more }
clear-all = Очисти сва обавештења
notification-settings = Подешавања обавештења...
no-notifications = Без обавештења
do-not-disturb = Не узнемиравај
show-less = Прикажи мање
clear-group = Очисти групу
hours-ago =
{ $duration ->
[0] Управо сада
[one] Пре 1 сата
*[other] Пре { $duration } сати
}
minutes-ago =
{ $duration ->
[0] Управо сада
[one] Пре 1 минута
*[other] Пре { $duration } минута
}

View file

@ -7,7 +7,7 @@ license = "GPL-3.0-only"
[dependencies]
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
logind-zbus = "5.3.2"
rust-embed.workspace = true
rustix.workspace = true

View file

@ -0,0 +1,2 @@
cancel = Cancel·lar
confirm = Confirmar

View file

@ -4,3 +4,33 @@ log-out = Αποσύνδεση
restart = Επανεκκίνηση
suspend = Αναστολή
confirm = Επιβεβαίωση
confirm-button =
{ $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] Τερματισμός
[log-out] { log-out }
*[other] { confirm }
}
power = Ενέργεια
confirm-body =
Θα εκτελεστεί αυτόματα { $action ->
[restart] επανεκκίνηση
[suspend] αναστολή
[shutdown] τερματισμός
[lock-screen] κλείδωμα της οθόνης
[log-out] αποσύνδεση
*[other] η επιλεγμένη ενέργεια
} του συστήματος σε { $countdown } δευτερόλεπτα.
lock-screen = Κλείδωμα οθόνης
log-out-shortcut = Super + Shift + Escape
settings = Ρυθμίσεις...
lock-screen-shortcut = Super + Escape
confirm-title =
{ $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] { shutdown }
[log-out] Έξοδος από όλες τις εφαρμογές και αποσύνδεση
*[other] Εφαρμογή της επιλεγμένης ενέργειας
} τώρα;

View file

@ -0,0 +1,36 @@
power = Напајање
lock-screen = Закључај екран
shutdown = Угаси
log-out = Одјава
restart = Поново покрени
log-out-shortcut = Супер + Shift + Esc
cancel = Откажи
suspend = Обустави
confirm = Потврди
settings = Подешавања...
lock-screen-shortcut = Супер + Esc
confirm-button =
{ $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] Искључи
[log-out] { log-out }
*[other] { confirm }
}
confirm-body =
Систем ће { $action ->
[restart] поново покренути
[suspend] обуставити
[shutdown] искључити
[lock-screen] закључати екран
[log-out] одјавити се
*[other] применити изабрану радњу
} самостално за { $countdown } секунде.
confirm-title =
{ $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] { shutdown }
[log-out] Затвори све програме и одјави се
*[other] Примени изабрану радњу
} сада?

View file

@ -16,9 +16,8 @@ use cosmic::{
window,
},
surface, theme,
widget::{Space, button, divider, icon, space, text},
widget::{button, divider, icon, space, text},
};
use std::sync::LazyLock;
use logind_zbus::{
manager::ManagerProxy,
@ -35,9 +34,6 @@ pub mod session_manager;
use crate::{cosmic_session::CosmicSessionProxy, session_manager::SessionManagerProxy};
static SUBSURFACE_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("subsurface"));
pub fn run() -> cosmic::iced::Result {
localize::localize();
@ -49,7 +45,6 @@ struct Power {
icon_name: String,
popup: Option<window::Id>,
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
subsurface_id: window::Id,
}
#[derive(Debug, Clone, Copy)]
@ -75,6 +70,7 @@ impl PowerAction {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
Action(PowerAction),
TogglePopup,
@ -104,7 +100,6 @@ impl cosmic::Application for Power {
Self {
core,
icon_name: "system-shutdown-symbolic".to_string(),
subsurface_id: window::Id::unique(),
token_tx: None,
popup: Option::default(),
},

View file

@ -6,7 +6,7 @@ license = "GPL-3.0-only"
[dependencies]
futures.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
serde.workspace = true
rustc-hash.workspace = true
tokio.workspace = true

View file

@ -11,7 +11,6 @@ use cosmic::{
iced::{
self, Length, Subscription,
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
theme::Style,
window,
},
surface,
@ -25,6 +24,7 @@ use crate::{
};
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum Msg {
None,
Activate(usize),
@ -577,7 +577,7 @@ fn menu_icon_button<'a>(
let icon = menu.icon_handle().clone();
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let _theme = theme.cosmic();
let suggested = applet.suggested_size(true);
let padding = applet.suggested_padding(true).1;

View file

@ -8,7 +8,7 @@ use cosmic::{
iced,
widget::icon::{self, IconFallback},
};
use std::path::{Path, PathBuf};
use std::path::Path;
use crate::subscriptions::status_notifier_item::{IconUpdate, Layout, StatusNotifierItem};

View file

@ -10,21 +10,18 @@
//! can be socket-activated and not conflict with anything else running as a status notifier
//! watcher.
//!
//! The daemon runs as long as as there is at least one client still connected. Which it checks
//! for every `REFRESH_INTERVAL`.
//! The daemon runs as long as there is at least one client still connected.
use crate::subscriptions::status_notifier_watcher::server::create_service;
use crate::unique_names::UniqueNames;
use futures::StreamExt;
use std::{collections::HashSet, time::Duration};
use std::collections::HashSet;
use zbus::fdo;
use zbus::message::Header;
const DBUS_NAME: &str = "com.system76.CosmicStatusNotifierWatcher";
const OBJECT_PATH: &str = "/CosmicStatusNotifierWatcher";
const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
/// Run daemon
pub fn run() -> cosmic::iced::Result {
if let Err(err) = run_inner() {
@ -42,7 +39,7 @@ pub async fn cosmic_register(conn: &zbus::Connection) -> zbus::Result<()> {
tokio::spawn(async move {
while let Some(value) = stream.next().await {
if let Some(_unique_name) = value {
/// Register with new owner
// Register with new owner.
let _ = cosmic_watcher.register_applet().await;
}
}

View file

@ -5,10 +5,10 @@ edition = "2024"
license = "GPL-3.0-only"
[dependencies]
libcosmic.workspace = true
cosmic.workspace = true
anyhow.workspace = true
cctk.workspace = true
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" }
cosmic-comp-config.workspace = true
cosmic-protocols.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true

View file

@ -0,0 +1,20 @@
arrow-keys = βέλη
move-window = Μετακίνηση παραθύρου
shift = Shift
super = Super
new-workspace = Συμπεριφορά νέων χώρων εργασίας
tiled = Παράθεση
active-hint = Ένδειξη ενεργού παραθύρου
navigate-windows = Πλοήγηση στα παράθυρα
toggle-floating-window = Εναλλαγή αιώρησης παραθύρων
all-workspaces = Όλοι οι χώροι εργασίας
per-workspace = Ανά χώρο εργασίας
autotile-behavior = Παράθεση παραθύρων στους χώρους εργασίας
tile-current = Παράθεση στον τρέχοντα χώρο εργασίας
floating = Αιώρηση
gaps = Κενά
tile-windows = Αυτόματη παράθεση παραθύρων
view-all-shortcuts = Προβολή όλων των συντομεύσεων...
shortcuts = Συντομεύσεις
floating-window-exceptions = Εξαιρέσεις αιωρούμενων παραθύρων...
window-management-settings = Ρυθμίσεις διαχείρισης παραθύρων...

View file

@ -0,0 +1,20 @@
arrow-keys = стрелице
tiled = Поплочано
active-hint = Активна помоћ
navigate-windows = Крећите се прозорима
toggle-floating-window = Окини пловни прозор
all-workspaces = Сви радни простори
per-workspace = По радном простору
autotile-behavior = Поплочај прозоре на радним просторима
tile-current = Поплочај тренутни радни простор
floating = Плутајуће
gaps = Размаци
move-window = Премести прозор
tile-windows = Самостално поплочај прозоре
view-all-shortcuts = Прикажи све пречице...
shortcuts = Пречице
shift = Shift
floating-window-exceptions = Изузеци за пловне прозоре...
super = Супер
window-management-settings = Подешавања за управљање прозорима...
new-workspace = Понашање новог радног простора

View file

@ -26,7 +26,7 @@ use cosmic::{
};
use cosmic_comp_config::{CosmicCompConfig, TileBehavior};
use cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::TilingState;
use std::{thread, time::Instant};
use std::thread;
use tracing::error;
const ID: &str = "com.system76.CosmicAppletTiling";
@ -46,6 +46,7 @@ pub struct Window {
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum Message {
TogglePopup,
PopupClosed(Id),

View file

@ -9,7 +9,7 @@ cosmic-applets-config.workspace = true
jiff = "0.2"
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
rust-embed.workspace = true
tokio.workspace = true
tracing-log.workspace = true
@ -17,5 +17,5 @@ tracing-subscriber.workspace = true
tracing.workspace = true
icu = { version = "2.1.1", features = ["compiled_data"] }
zbus.workspace = true
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
timedate-zbus = { path = "../../dbus-settings-bindings/timedate" }
logind-zbus = "5.3.2"

View file

@ -0,0 +1 @@
datetime-settings = Ρυθμίσεις ημερομηνίας, ώρας και ημερολογίου...

View file

@ -0,0 +1 @@
datetime-settings = Датум, време и подешавања календара...

View file

@ -94,6 +94,7 @@ pub struct Window {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum Message {
TogglePopup,
CloseRequested(window::Id),
@ -361,7 +362,7 @@ impl cosmic::Application for Window {
}
fn subscription(&self) -> Subscription<Message> {
fn time_subscription(mut show_seconds: watch::Receiver<bool>) -> Subscription<Message> {
fn time_subscription(show_seconds: watch::Receiver<bool>) -> Subscription<Message> {
struct Wrapper {
inner: watch::Receiver<bool>,
id: &'static str,
@ -376,7 +377,7 @@ impl cosmic::Application for Window {
inner: show_seconds,
id: "time-sub",
},
|Wrapper { inner, id }| {
|Wrapper { inner, id: _ }| {
let mut show_seconds = inner.clone();
stream::channel(1, move |mut output: mpsc::Sender<Message>| async move {
// Mark this receiver's state as changed so that it always receives an initial

View file

@ -6,7 +6,7 @@ edition = "2024"
license = "GPL-3.0-only"
[dependencies]
libcosmic.workspace = true
cosmic.workspace = true
cctk.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1 @@
cosmic-applet-workspaces = Χώροι εργασίας COSMIC

View file

@ -0,0 +1 @@
cosmic-applet-workspaces = КОСМИК радни простори

View file

@ -84,6 +84,7 @@ impl IcedWorkspacesApplet {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
WorkspaceUpdate(WorkspacesUpdate),
WorkspacePressed(ExtWorkspaceHandleV1),

View file

@ -23,7 +23,7 @@ cosmic-applet-time = { path = "../cosmic-applet-time" }
cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" }
cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" }
cosmic-panel-button = { path = "../cosmic-panel-button" }
libcosmic.workspace = true
cosmic.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tracing-log.workspace = true

View file

@ -12,26 +12,44 @@ fn main() -> cosmic::iced::Result {
};
let start = applet.rfind('/').map_or(0, |v| v + 1);
let cmd = &applet.as_str()[start..];
let cmd = applet.as_str()[start..].to_string();
tracing::info!("Starting `{cmd}` with version {VERSION}");
match cmd {
"cosmic-app-list" => cosmic_app_list::run(),
"cosmic-applet-a11y" => cosmic_applet_a11y::run(),
"cosmic-applet-audio" => cosmic_applet_audio::run(),
"cosmic-applet-battery" => cosmic_applet_battery::run(),
"cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(),
"cosmic-applet-minimize" => cosmic_applet_minimize::run(),
"cosmic-applet-network" => cosmic_applet_network::run(),
"cosmic-applet-notifications" => cosmic_applet_notifications::run(),
"cosmic-applet-power" => cosmic_applet_power::run(),
"cosmic-applet-status-area" => cosmic_applet_status_area::run(),
"cosmic-applet-tiling" => cosmic_applet_tiling::run(),
"cosmic-applet-time" => cosmic_applet_time::run(),
"cosmic-applet-workspaces" => cosmic_applet_workspaces::run(),
"cosmic-applet-input-sources" => cosmic_applet_input_sources::run(),
"cosmic-panel-button" => cosmic_panel_button::run(),
_ => Ok(()),
let cmd_for_run = cmd.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
match cmd_for_run.as_str() {
"cosmic-app-list" => cosmic_app_list::run(),
"cosmic-applet-a11y" => cosmic_applet_a11y::run(),
"cosmic-applet-audio" => cosmic_applet_audio::run(),
"cosmic-applet-battery" => cosmic_applet_battery::run(),
"cosmic-applet-bluetooth" => cosmic_applet_bluetooth::run(),
"cosmic-applet-minimize" => cosmic_applet_minimize::run(),
"cosmic-applet-network" => cosmic_applet_network::run(),
"cosmic-applet-notifications" => cosmic_applet_notifications::run(),
"cosmic-applet-power" => cosmic_applet_power::run(),
"cosmic-applet-status-area" => cosmic_applet_status_area::run(),
"cosmic-applet-tiling" => cosmic_applet_tiling::run(),
"cosmic-applet-time" => cosmic_applet_time::run(),
"cosmic-applet-workspaces" => cosmic_applet_workspaces::run(),
"cosmic-applet-input-sources" => cosmic_applet_input_sources::run(),
"cosmic-panel-button" => cosmic_panel_button::run(),
_ => Ok(()),
}
}));
match result {
Ok(r) => r,
Err(payload) => {
let msg = payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "<non-string panic>".to_string());
tracing::error!(
"`{cmd}` panicked (likely compositor disconnect), exiting cleanly: {msg}"
);
Ok(())
}
}
}

View file

@ -5,7 +5,7 @@ edition = "2024"
license = "GPL-3.0-only"
[dependencies]
libcosmic.workspace = true
cosmic.workspace = true
rustc-hash.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

View file

@ -48,7 +48,7 @@ impl Button {
icon: cosmic::widget::icon::Handle,
) -> cosmic::widget::Button<'a, Message> {
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let _theme = theme.cosmic();
let suggested = self.core.applet.suggested_size(icon.symbolic);
let (major_padding, applet_padding_minor_axis) =
@ -123,11 +123,15 @@ impl cosmic::Application for Button {
fn update(&mut self, message: Msg) -> app::Task<Msg> {
match message {
Msg::Press => {
let _ = Command::new("sh")
if let Ok(mut child) = Command::new("sh")
.arg("-c")
.arg(&self.desktop.exec)
.arg(format!("exec {}", self.desktop.exec))
.spawn()
.unwrap();
{
std::thread::spawn(move || {
let _ = child.wait();
});
}
}
Msg::ConfigUpdated(conf) => {
self.config = conf

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
cosmic-applets (1.0.12) noble; urgency=medium
* Update changelog
-- Michael Murphy <michael@mmurphy.dev> Tue, 28 Apr 2026 15:11:17 +0200
cosmic-applets (1.0.2) noble; urgency=medium
[ Ashley Wulber ]

View file

@ -0,0 +1 @@
cosmic-app-list = Safata d'Aplicacions

View file

@ -0,0 +1,51 @@
cosmic-app-list = Περιοχή εφαρμογών
cosmic-app-list-comment = Εκκίνηση καρφιτσωμένων εφαρμογών και διαχείριση ανοικτών παραθύρων
cosmic-applet-a11y = Προσβασιμότητα
cosmic-applet-audio = Ήχος
cosmic-applet-bluetooth = Bluetooth
cosmic-applet-bluetooth-comment = Διαχείριση συσκευών Bluetooth
cosmic-applet-minimize = Ελαχιστοποιημένα παράθυρα
cosmic-applet-minimize-comment = Διαχείριση ελαχιστοποιημένων παραθύρων
cosmic-applet-network = Δίκτυο
cosmic-applet-network-comment = Διαχείριση συνδέσεων δικτύου
cosmic-applet-time = Ημερομηνία, ώρα και ημερολόγιο
cosmic-applet-workspaces = Αριθμημένοι χώροι εργασίας
cosmic-panel-workspaces-button = Κουμπί χώρων εργασίας
cosmic-applet-audio-comment = Επιλογή συσκευής ήχου, έλεγχος έντασης και στοιχεία ελέγχου πολυμέσων MPRIS
cosmic-applet-audio-keywords = Μικροεφαρμογή;Ήχος;COSMIC;Applet;Sound;Audio;MPRIS;
cosmic-applet-bluetooth-keywords = Μικροεφαρμογή;COSMIC;Applet;Bluetooth;
cosmic-applet-input-sources = Πηγές εισόδου
cosmic-applet-input-sources-comment = Εναλλαγή μεταξύ πηγών εισόδου
cosmic-applet-input-sources-keywords = Μικροεφαρμογή;Πηγή;Είσοδος;COSMIC;Applet;Input;Source;
cosmic-applet-minimize-keywords = Μικροεφαρμογή;Ελαχιστοποίηση;COSMIC;Applet;Minimize;
cosmic-applet-network-keywords = Μικροεφαρμογή;Δίκτυο;COSMIC;Applet;Network;
cosmic-applet-notifications = Κέντρο ειδοποιήσεων
cosmic-applet-notifications-comment = Διαχείριση ειδοποιήσεων και λειτουργία «Μην ενοχλείτε»
cosmic-applet-notifications-keywords = Μικροεφαρμογή;Ειδοποίηση;COSMIC;Applet;Notification;
cosmic-applet-power = Συνεδρία χρήστη
cosmic-applet-power-comment = Κλείδωμα οθόνης, αποσύνδεση, αναστολή, επανεκκίνηση και τερματισμός
cosmic-applet-power-keywords = Μικροεφαρμογή;Χρήστης;Συνεδρία;Κλείδωμα;Αποσύνδεση;Επανεκκίνηση;Τερματισμός;Αναστολή;COSMIC;Applet;User;Session;Lock;Log;Reboot;Shutdown;Suspend;
cosmic-applet-status-area = Περιοχή ειδοποιήσεων
cosmic-applet-workspaces-keywords = Μικροεφαρμογή;Χώρος;Εργασίας;COSMIC;Applet;Workspace;
cosmic-panel-app-button = Κουμπί βιβλιοθήκης εφαρμογών
cosmic-panel-workspaces-button-keywords = Μικροεφαρμογή;Χώρος;Εργασίας;Επισκόπηση;COSMIC;Applet;Workspace;Overview;
cosmic-app-list-keywords = Μικροεφαρμογή;Εφαρμογή;Περιοχή;Λίστα;Εργασία;Γραμμή;COSMIC;Applet;App;Tray;List;Task;Bar;
cosmic-applet-a11y-keywords = Μικροεφαρμογή;Προσβασιμότητα;Προσιτότητα;Οθόνη;Ανάγνωση;Αναγνώστης;Μεγεθυντικός;Φακός;Αντίθεση;Χρώμα;COSMIC;Applet;Accessibility;A11y;Screen;Reader;Magnifier;Contrast;Color;
cosmic-applet-battery = Ενέργεια και μπαταρία
cosmic-applet-battery-comment = Λειτουργίες ενέργειας και επιλογές εξοικονόμησης ενέργειας
cosmic-applet-battery-keywords = Μικροεφαρμογή;Ενέργεια;Ισχύς;Μπαταρία;COSMIC;Applet;Power;Battery;
cosmic-applet-status-area-keywords = Μικροεφαρμογή;Εφαρμογή;Ένδειξη;Ειδοποίηση;Περιοχή;Κατάσταση;COSMIC;Applet;App;Indicator;Notification;Tray;Status;
cosmic-applet-time-keywords = Μικροεφαρμογή;Ημερομηνία;Ώρα;Ημερολόγιο;COSMIC;Applet;Date;Time;Calendar;
cosmic-panel-app-button-comment = Άνοιγμα της βιβλιοθήκης εφαρμογών για εκκίνηση εγκατεστημένων εφαρμογών
cosmic-panel-app-button-keywords = Μικροεφαρμογή;Εφαρμογή;Βιβλιοθήκη;Περιοχή;COSMIC;Applet;App;Library;Tray;
cosmic-applet-a11y-comment = Διαμόρφωση των ρυθμίσεων προσβασιμότητας από τη γραμμή συστήματος
cosmic-applet-status-area-comment = Ενδείξεις εφαρμογών που ενδέχεται να εξάγουν μενού στη γραμμή συστήματος
cosmic-applet-tiling = Παράθεση
cosmic-applet-tiling-comment = Διαχείριση της ένδειξης ενεργού παραθύρου, αυτόματη παράθεση παραθύρων στον τρέχοντα χώρο εργασίας και ανά χώρο εργασίας
cosmic-applet-tiling-keywords = Μικροεφαρμογή;Παράθεση;Ένδειξη;Χώρος;Εργασίας;COSMIC;Applet;Tiling;Hint;Workspace;
cosmic-applet-time-comment = Προβολή της τρέχουσας ώρας στη γραμμή συστήματος με αναδυόμενο ημερολόγιο
cosmic-applet-workspaces-comment = Εναλλαγή μεταξύ των αριθμημένων χώρων εργασίας στη γραμμή συστήματος
cosmic-panel-launcher-button = Κουμπί μενού εκκίνησης
cosmic-panel-launcher-button-comment = Άνοιγμα του μενού εκκίνησης για αναζήτηση εφαρμογών και εκτέλεση εντολών
cosmic-panel-launcher-button-keywords = Μικροεφαρμογή;Μενού;Εκκίνηση;Εκτέλεση;COSMIC;Applet;Launcher;Runner;
cosmic-panel-workspaces-button-comment = Άνοιγμα των χώρων εργασίας για διαχείριση και εναλλαγή χώρων εργασίας

View file

@ -15,20 +15,20 @@ cosmic-applet-workspaces = Számozott Munkaterületek
cosmic-panel-app-button = Alkalmazáskönyvtár gomb
cosmic-panel-launcher-button = Indító gomb
cosmic-panel-workspaces-button = Munkaterületek gomb
cosmic-app-list-keywords = COSMIC;Kisalkalmazás;Alkalmazás;App;Tálca;Lista;Sáv;
cosmic-applet-a11y-keywords = COSMIC;Kisalkalmazás;Akadálymentesség;Akadálymentesítés;Képernyő;Olvasó;Nagyító;Szín;
cosmic-applet-audio-keywords = COSMIC;Kisalkalmazás;Hang;MPRIS;
cosmic-applet-battery-keywords = COSMIC;Kisalkalmazás;Energia;Akkumulátor;
cosmic-app-list-keywords = COSMIC;kisalkalmazás;alkalmazás;app;tálca;lista;sáv;
cosmic-applet-a11y-keywords = COSMIC;kisalkalmazás;akadálymentesség;akadálymentesítés;képernyő;olvasó;nagyító;szín;
cosmic-applet-audio-keywords = COSMIC;kisalkalmazás;hang;MPRIS;
cosmic-applet-battery-keywords = COSMIC;kisalkalmazás;energia;akkumulátor;
cosmic-applet-bluetooth-comment = Bluetooth-eszközök kezelése
cosmic-applet-bluetooth-keywords = COSMIC;Kisalkalmazás;Bluetooth;
cosmic-applet-input-sources-keywords = COSMIC;Kisalkalmazás;Beviteli;Forrás;
cosmic-applet-minimize-keywords = COSMIC;Kisalkalmazás;Minimizálás;
cosmic-applet-network-keywords = COSMIC;Kisalkalmazás;Hálózat;Internet;
cosmic-applet-notifications-keywords = COSMIC;Kisalkalmazás;Értesítések;
cosmic-applet-power-keywords = COSMIC;Kisalkalmazás;Felhasználói munkamenet;Zárolás;Kijelentkezés;Újraindítás;Leállítás;Felfüggesztés;
cosmic-panel-app-button-keywords = COSMIC;Kisalkalmazás;Alkalmazás;Könyvtár;Tálca;
cosmic-panel-launcher-button-keywords = COSMIC;Kisalkalmazás;Indító;Futtató;
cosmic-panel-workspaces-button-keywords = COSMIC;Kisalkalmazás;Munkaterületek;Áttekintés;
cosmic-applet-bluetooth-keywords = COSMIC;kisalkalmazás;bluetooth;
cosmic-applet-input-sources-keywords = COSMIC;kisalkalmazás;beviteli forrás;
cosmic-applet-minimize-keywords = COSMIC;kisalkalmazás;minimizálás;
cosmic-applet-network-keywords = COSMIC;kisalkalmazás;hálózat;internet;
cosmic-applet-notifications-keywords = COSMIC;kisalkalmazás;értesítések;
cosmic-applet-power-keywords = COSMIC;kisalkalmazás;felhasználói munkamenet;zárolás;kijelentkezés;újraindítás;leállítás;felfüggesztés;
cosmic-panel-app-button-keywords = COSMIC;kisalkalmazás;alkalmazás;könyvtár;tálca;
cosmic-panel-launcher-button-keywords = COSMIC;kisalkalmazás;indító;futtató;
cosmic-panel-workspaces-button-keywords = COSMIC;kisalkalmazás;munkaterületek;áttekintés;
cosmic-applet-a11y-comment = Akadálymentességi beállítások kezelése a panelen
cosmic-applet-audio-comment = Hangeszköz kiválasztása, hangerőszabályozás és MPRIS médiavezérlés
cosmic-applet-battery-comment = Energiamódok és energiagazdálkodási beállítások
@ -39,13 +39,13 @@ cosmic-applet-notifications-comment = Értesítések és a Ne zavarjanak kezelé
cosmic-applet-power-comment = Képernyő zárolása, kijelentkezés, felfüggesztés, újraindítás és leállítás
cosmic-app-list-comment = Rögzített alkalmazások indítása és a megnyitott ablakok kezelése
cosmic-applet-status-area-comment = Alkalmazásjelzők, melyek menüt is megjeleníthetnek a panelen
cosmic-applet-status-area-keywords = COSMIC;Kisalkalmazás;Alkalmazás;App;Jelző;Értesítések;Tálca;Állapot;
cosmic-applet-status-area-keywords = COSMIC;kisalkalmazás;alkalmazás;app;jelző;értesítések;tálca;állapot;
cosmic-applet-tiling-comment = Az aktív ablak kiemelésének, valamint az aktuális és munkaterületenkénti automatikus csempézés kezelése
cosmic-applet-tiling-keywords = COSMIC;Kisalkalmazás;Csempézés;Kiemelés;Munkaterületek;
cosmic-applet-tiling-keywords = COSMIC;kisalkalmazás;csempézés;kiemelés;munkaterületek;
cosmic-applet-time-comment = Az aktuális idő megjelenítése a panelen, felugró naptárral
cosmic-applet-time-keywords = COSMIC;Kisalkalmazás;Dátum;Idő;Naptár;
cosmic-applet-time-keywords = COSMIC;kisalkalmazás;dátum;idő;naptár;
cosmic-applet-workspaces-comment = Váltás számozott munkaterületek között a panelen
cosmic-applet-workspaces-keywords = COSMIC;Kisalkalmazás;Munkaterületek;
cosmic-applet-workspaces-keywords = COSMIC;kisalkalmazás;munkaterületek;
cosmic-panel-app-button-comment = Az alkalmazáskönyvtár megnyitása a telepített alkalmazások indításához
cosmic-panel-launcher-button-comment = Az indító megnyitása alkalmazások kereséséhez és parancsok futtatásához
cosmic-panel-workspaces-button-comment = A munkaterületek áttekintőjének megnyitása munkaterületek kezeléséhez és váltásához

View file

View file

@ -0,0 +1,51 @@
cosmic-app-list = Системска касета
cosmic-app-list-comment = Покрени закачене програме и управљај отвореним прозорима
cosmic-app-list-keywords = COSMIC;Applet;App;Tray;List;Task;Bar;Космик;Програмчић;Програм;Касета;Списак;Задатак;Трака;kosmik;programčić;program;kaseta;spisak;zadatak;traka;
cosmic-applet-a11y = Приступачност
cosmic-applet-a11y-comment = Подеси подешавања приступачности из траке
cosmic-applet-a11y-keywords = COSMIC;Applet;Accessibility;A11y;Screen;Reader;Magnifier;Contrast;Color;Космик;Програмчић;Приступачност;А11ј;Екран;Читач;Лупа;Контраст;Боја;kosmik;programčić;pristupačnost;a11j;ekran;čitač;lupa;kontrast;boja;
cosmic-applet-audio = Звук
cosmic-applet-audio-comment = Избор уређаја звука, управљање јачином звука и MPRIS контроле медија
cosmic-applet-audio-keywords = COSMIC;Applet;Sound;Audio;MPRIS;Космик;Програмчић;Аудио;Звук;Јачина звука;МПРИС;Медија;kosmik;programčić;audio;zvuk;jačina zvuka;medija;
cosmic-applet-battery = Напајање и батерија
cosmic-applet-battery-comment = Режими напајања и могућности уштеде енергије
cosmic-applet-battery-keywords = COSMIC;Applet;Power;Battery;Космик;Програмчић;Напајање;Батерија;kosmik;programčić;napajanje;baterija;
cosmic-applet-bluetooth = Блутут
cosmic-applet-bluetooth-comment = Управљај Блутут уређајима
cosmic-applet-bluetooth-keywords = COSMIC;Applet;Bluetooth;Космик;Програмчић;Блутут;kosmik;programčić;blutut;
cosmic-applet-input-sources = Извори уноса
cosmic-applet-input-sources-comment = Пребацивање између извора уноса
cosmic-applet-input-sources-keywords = COSMIC;Applet;Input;Source;Космик;Програмчић;Унос;Извор;kosmik;programčić;unos;izvor;
cosmic-applet-minimize = Умањени прозори
cosmic-applet-minimize-comment = Управљај умањеним прозорима
cosmic-applet-minimize-keywords = COSMIC;Applet;Minimize;Космик;Програмчић;Умањи;kosmik;programčić;umanji;
cosmic-applet-network = Мрежа
cosmic-applet-network-comment = Управљај мрежним везама
cosmic-applet-network-keywords = COSMIC;Applet;Network;Космик;Програмчић;Мрежа;kosmik;programčić;mreža;
cosmic-applet-notifications = Центар за обавештења
cosmic-applet-notifications-comment = Управљај обавештењима и режимом „Не узнемиравај“
cosmic-applet-notifications-keywords = COSMIC;Applet;Notification;Космик;Програмчић;Обавештење;kosmik;programčić;obaveštenje;
cosmic-applet-power = Корисничка сесија
cosmic-applet-power-comment = Закључај екран, одјави се, обустави, поново покрени и искључи
cosmic-applet-power-keywords = COSMIC;Applet;User;Session;Lock;Log;Reboot;Shutdown;Suspend;Космик;Програмчић;Корисник;Сесија;Закључавање;Одјава;Поновно покретање;Искључивање;Обустава;kosmik;programčić;korisnik;sesija;zaključavanje;odjava;ponovno pokretanje;isključivanje;obustava;
cosmic-applet-status-area = Обавештајна касета
cosmic-applet-status-area-comment = Показивачи програма који могу извести изборнике у површ
cosmic-applet-status-area-keywords = COSMIC;Applet;App;Indicator;Notification;Tray;Status;Космик;Програмчић;Програм;Показатељ;Обавештење;Пољце;Статус;kosmik;programčić;program;pokazatelj;obaveštenje;poljce;status;
cosmic-applet-tiling = Поплочавање
cosmic-applet-tiling-comment = Управљај активним саветом, тренутним и самопоплочавањем по радном простору
cosmic-applet-tiling-keywords = COSMIC;Applet;Tiling;Hint;Workspace;Космик;Програмчић;Поплочавање;Помоћ;Радни простор;kosmik;programčić;popločavanje;pomoć;radni prostor;
cosmic-applet-time = Датум, време и календар
cosmic-applet-time-comment = Приказује тренутно време у површи са искакућим календаром
cosmic-applet-time-keywords = COSMIC;Applet;Date;Time;Calendar;Космик;Програмчић;Датум;Време;Календар;kosmik;programčić;datum;vreme;kalendar;
cosmic-applet-workspaces = Радни простори обележени бројевима
cosmic-applet-workspaces-comment = Пребаците између радних простора обележених бројевима у траци
cosmic-applet-workspaces-keywords = COSMIC;Applet;Workspace;Космик;Програмчић;радни простор;kosmik;programčić;radni prostor;
cosmic-panel-app-button = Дугме за библиотеку програма
cosmic-panel-app-button-comment = Отвори библиотеку програма за покретање инсталираних програма
cosmic-panel-app-button-keywords = COSMIC;Applet;App;Library;Tray;Космик;Програмчић;Програм;Библиотека;Касета;kosmik;programčić;program;biblioteka;kaseta;
cosmic-panel-launcher-button = Дугме покретача
cosmic-panel-launcher-button-comment = Отвори покретач да бисте претражили програме и покренули наредбе
cosmic-panel-launcher-button-keywords = COSMIC;Applet;Launcher;Runner;Космик;Програмчић;Покретач;Покретач;kosmik;programčić;pokretač;pokretač;
cosmic-panel-workspaces-button = Дугме радних простора
cosmic-panel-workspaces-button-comment = Отвори преглед радних простора за управљање и пребацивање радних простора
cosmic-panel-workspaces-button-keywords = COSMIC;Applet;Workspace;Overview;Космик;Програмчић;радни простор;Преглед;kosmik;programčić;radni prostor;pregled;

View file

@ -30,9 +30,9 @@ cosmic-applet-input-sources-comment = Перемикання джерел вве
cosmic-applet-input-sources-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Джерело;Введення;Розкладка;Клавіатура;
cosmic-applet-minimize-comment = Керування згорнутими вікнами
cosmic-applet-minimize-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Згорнути;Згорнуті;Вікна;Вікно;
cosmic-applet-network-comment = Керування мережевими підключеннями
cosmic-applet-network-comment = Керування мережевими з’єднаннями
cosmic-applet-network-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Мережа;Інтернет;
cosmic-applet-notifications-comment = Керування сповіщеннями та режимом Не турбувати
cosmic-applet-notifications-comment = Керування сповіщеннями та режимом «Не турбувати»
cosmic-applet-notifications-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Сповіщення;
cosmic-applet-power-comment = Блокування екрана, вихід із сеансу, призупинення, перезапуск і вимкнення
cosmic-applet-power-keywords = COSMIC;КОСМІК;КОСМОС;КОСМІЧНЕ;Віджет;Віджети;Сеанс;Користувач;Блокування;Вихід;Перезапуск;Вимкнення;Призупинення;Сон;

56
redeploy.sh Executable file
View file

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Recompile et déploie le fork local cosmic-applets (multiplexor cosmic-applets +
# binaire séparé cosmic-app-list).
#
# Cibles : /usr/local/bin/cosmic-applets et /usr/local/bin/cosmic-app-list
# (binaires compilés manuellement, hors pacman, qui priment sur ceux du paquet
# via $PATH /usr/local/bin avant /usr/bin).
#
# Branche de déploiement : yoda-dock-magnification (contient les commits
# magnification + le fix Wayland error handling).
set -euo pipefail
REPO="/home/lionel/Projets/COSMIC/cosmic-applets"
TARGETS=(
"cosmic-applets"
"cosmic-app-list"
"cosmic-panel-button"
)
BIN_DIR="/usr/local/bin"
cd "$REPO"
echo "==> Branche actuelle : $(git branch --show-current)"
echo "==> Build release (workspace)..."
cargo build --release --workspace
STAMP=$(date +%Y%m%d-%H%M%S)
for bin in "${TARGETS[@]}"; do
src="$REPO/target/release/$bin"
dst="$BIN_DIR/$bin"
if [[ ! -f "$src" ]]; then
echo "!! Binaire absent après build : $src" >&2
exit 1
fi
if [[ -f "$dst" ]]; then
echo "==> Backup $dst"
sudo cp -a "$dst" "${dst}.bak.${STAMP}"
fi
echo "==> Install $src -> $dst"
sudo install -m755 "$src" "$dst"
done
echo "==> Kill des process applet en cours (cosmic-panel relancera à la demande)"
pkill -u "$USER" -f "/usr/local/bin/cosmic-applet" || true
pkill -u "$USER" -f "/usr/local/bin/cosmic-app-list" || true
pkill -u "$USER" -f "/usr/local/bin/cosmic-panel-button" || true
echo "==> Vérif"
for bin in "${TARGETS[@]}"; do
file "$BIN_DIR/$bin"
done
echo "OK — relogin recommandé pour repartir sur des process applet propres."

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