Compare commits

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

57 commits

Author SHA1 Message Date
a9f15d67a2 fix: handle wheel events before cursor bounds check in MouseArea
Some checks failed
Continuous Integration / formatting (push) Has been cancelled
Continuous Integration / linting (push) Has been cancelled
Move the on_mouse_wheel handler before the cursor.is_over(bounds)
guard. The panel's nested compositor only routes axis events to the
applet surface when the pointer is over it, making the bounds check
redundant for scroll events. This fixes scroll-to-adjust-volume not
working on the audio applet after a fresh login, when the cursor
coordinates sent by the panel may not match the applet's surface-local
coordinate space.
2026-07-29 11:59:32 +02:00
2d78a0abff chore: bump version to 1.3.0
Some checks failed
Continuous Integration / linting (push) Has been cancelled
Continuous Integration / formatting (push) Has been cancelled
2026-07-22 15:57:00 +02:00
ebecfa6b7d chore: refresh lockfile post-upstream rebase
Some checks failed
Continuous Integration / formatting (push) Has been cancelled
Continuous Integration / linting (push) Has been cancelled
Leyoda 2026 – GPLv3
2026-07-04 11:20:22 +02:00
adc97dc8f1 fix: adapt to cosmic-theme background(transparent) accessor
Leyoda 2026 – GPLv3
2026-07-04 11:19:28 +02:00
ced6eb267e Fix status menu build after upstream rebase 2026-07-04 11:19:28 +02:00
04acc50e4d feat: add themed launcher icon catalog 2026-07-04 11:19:28 +02:00
8d56d0b500 fix: open launcher editor in existing popup 2026-07-04 11:19:28 +02:00
d58fe9d74c feat: add editable dock launchers 2026-07-04 11:19:28 +02:00
9677159db6 chore: fix applets redeploy path 2026-07-04 11:19:28 +02:00
3c360142ae chore: use local COSMIC support crates 2026-07-04 11:19:28 +02:00
ebfaa4eb7b chore: use local cosmic-text checkout 2026-07-04 11:19:28 +02:00
0023a462ba yoda: use local dbus settings bindings 2026-07-04 11:19:28 +02:00
b4fbfe168a chore: align applets with local stack 2026-07-04 11:02:35 +02:00
Votre Nom
c117f172cb 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-07-04 10:55:18 +02:00
Votre Nom
66392fc42a 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-07-04 10:55:18 +02:00
Votre Nom
c15a879c31 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-07-04 10:55:18 +02:00
d5c2a19e95 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-07-04 10:55:18 +02:00
6f88c6fea4 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-07-04 10:55:18 +02:00
1ce1d39ea3 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-07-04 10:55:18 +02:00
Ilia Malanin
d726dd890e fix(bluetooth): do not filter out already found devices 2026-07-02 12:46:20 -06:00
William Fish
fddf3d569b
fix(status-area): forward SNI scroll events (#1431)
StatusNotifierItems can expose a Scroll(int delta, string orientation) method for tray hosts to call when the user wheels over an item. The status-area applet handled activation and context menus, but did not forward wheel events to that method.

Track the hovered tray item only while the pointer is over an icon, ignore wheel events already captured by widgets, accumulate fine-grained wheel deltas into discrete scroll steps, and call Scroll on the item proxy. Add unit coverage for the delta/orientation mapping and pixel accumulation.

Signed-off-by: William Fish <william.michael.fish@gmail.com>
2026-07-02 18:02:40 +02:00
William Fish
0441c67b10
fix(battery): show plug icon without battery (#1432) 2026-06-29 16:58:04 -04:00
Fred
1f7c59fb2b
feat(bluetooth): use spinner widget from libcosmic for connecting and pairing states 2026-06-26 23:56:47 +02:00
Fred
c2a00a36a8
feat(network): use libcosmic spinner for connecting states 2026-06-26 18:30:06 +02:00
LKramer
1473e1f008
fix(network): register secret agent at NetworkManager's expected object path
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:05:36 +02:00
Michael Murphy
2c95057716
i18n: translation update from Hosted Weblate (#1418) 2026-06-23 17:11:07 +02:00
Hosted Weblate
f022b6c1a9
i18n: translation updates from weblate
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hugo Carvalho <hugokarvalho@hotmail.com>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: Jun Hwi Ku <siguning@gmail.com>
Co-authored-by: Walter William Beckerleg Bruckman <spayk.99@protonmail.com>
Co-authored-by: Yuya Furukuwa <junclegrow@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Co-authored-by: 김유빈 <k.sein1016@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-app-list/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-a11y/ja/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-a11y/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-audio/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-audio/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-battery/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-battery/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-bluetooth/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-bluetooth/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-notifications/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-notifications/kmr/
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/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-power/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-tiling/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-tiling/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-time/kmr/
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/ja/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/ko/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applets-desktop-entries/pt/
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/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-workspaces-applet/kmr/
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-06-23 08:02:17 +02:00
Ilia Malanin
f97dc5a31e fix(minimize): cache captured data for minimized toplevels 2026-06-22 16:58:50 -06:00
Ashley Wulber
456d052535 improv(audio): adjust popup size based on window size 2026-06-22 22:09:48 +02:00
Michael Murphy
95eda40878
feat(sound): use cosmic-settings-daemon's varlink API 2026-06-15 21:09:25 +02:00
BrianHotopp
8981b0b48e
fix(network): skip assumed connections in VPN list
`cosmic-applet-network` listed every connection from `nm.list_saved_connections()` in the VPN dropdown, including the in-memory-only profiles NetworkManager auto-generates for externally-managed interfaces (e.g. a `wg-quick@wg0.service` tunnel). Toggling such an "assumed" connection off deletes it from NM — it was never persisted — leaving a dead toggle with no way back through the applet or `nmcli con up`.

Skip connections flagged `unsaved` (in-memory only) at the `load_vpns()` site so they no longer get a togglable entry. The active-connections section still shows the interface as connected (read-only). One-line change in `cosmic-applet-network/src/app.rs`.

## Test plan

- Built on Pop!_OS and ran the patched `cosmic-applet-network`: with `wg-quick@wg0` up and NM tracking `wg0` as connected-externally, the VPN dropdown no longer shows the destructive `wg0` toggle; the active-connections section still shows `wg0`; Wi-Fi toggling is unaffected.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:23:45 +02:00
Michael Aaron Murphy
5428a86370 chore: bump versions to 1.0.15 2026-05-29 17:15:35 +02:00
Michael Aaron Murphy
61bd9b0dfd chore: update dependencies with fix for battery applet panic 2026-05-29 17:15:35 +02:00
Michael Aaron Murphy
c86daef96a build: use cargo vendor --locked 2026-05-29 17:15:35 +02:00
Priyam Sarkar
a50c3d499c
fix(bluetooth): bluetooth toggle is off despite active connection 2026-05-29 17:13:51 +02:00
Ashley Wulber
a4e6ecb280 fix(network): align name with spec 2026-05-29 16:20:40 +02:00
Jeremy Soller
ee85d07c48
chore: update cosmic-freedesktop-icons to fix dropbox tray icon (#1419) 2026-05-27 13:10:00 -06:00
Hojjat
010931d6d0 chore: update cosmic-freedesktop-icons to fix dropbox tray icon 2026-05-26 13:31:21 -06:00
Jeremy Soller
377f129611
i18n: translation update from Hosted Weblate (#1403) 2026-05-26 08:33:04 -06:00
Hojjat
46a8b9182b chore: update libcosmic 2026-05-26 10:04:40 -04:00
Hojjat
d73ef7dcfb fix: search the theme path provided by the app to find status-area icons 2026-05-26 10:04:40 -04:00
Hosted Weblate
88da7797bd
i18n: translation updates from weblate
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Isaac Subirana <isaacsubiranac@gmail.com>
Co-authored-by: therealmate <hellogaming91@gmail.com>
Co-authored-by: Димко <Dymkovych@proton.me>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-audio/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-battery/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-bluetooth/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-network/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-notifications/hu/
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/hu/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-applet-tiling/hu/
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/uk/
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 Network Applet
Translation: Pop OS/COSMIC Notifications Applet
Translation: Pop OS/COSMIC Power Applet
Translation: Pop OS/COSMIC Tiling Applet
2026-05-25 11:13:15 +00: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
131 changed files with 4313 additions and 1707 deletions

View file

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

15
.zed/settings.json Normal file
View file

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

1389
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-app-list" name = "cosmic-app-list"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
@ -13,13 +13,13 @@ futures.workspace = true
i18n-embed.workspace = true i18n-embed.workspace = true
i18n-embed-fl.workspace = true i18n-embed-fl.workspace = true
image = { version = "0.25.9", default-features = false } image = { version = "0.25.9", default-features = false }
libcosmic.workspace = true cosmic.workspace = true
memmap2 = "0.9.10" memmap2 = "0.9.10"
fastrand = "2.3.0" fastrand = "2.3.0"
rust-embed.workspace = true rust-embed.workspace = true
rustix.workspace = true rustix.workspace = true
rustc-hash.workspace = true rustc-hash.workspace = true
switcheroo-control = { git = "https://github.com/pop-os/dbus-settings-bindings" } switcheroo-control = { path = "../../dbus-settings-bindings/switcheroo-control" }
tokio.workspace = true tokio.workspace = true
tracing-log.workspace = true tracing-log.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true

View file

@ -1,10 +1,10 @@
[package] [package]
name = "cosmic-app-list-config" name = "cosmic-app-list-config"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
libcosmic.workspace = true cosmic.workspace = true
serde = { workspace = true, features = ["derive"] } 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 = Νέο παράθυρο 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 new-window = New Window
run = Run run = Run
run-on = Run on {$gpu} run-on = Run on {$gpu}
run-on-default = (Default) run-on-default = (Default)
edit-launcher = Edit launcher
launcher-name = Name
launcher-command = Command
launcher-icon = Icon
launcher-icon-theme = Theme
launcher-icon-search = Search icons
launcher-icon-catalog-loading = Loading icons
launcher-icon-catalog-empty = No icons
launcher-icons = icons
save = Save
cancel = Cancel

View file

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

View file

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

View file

@ -0,0 +1,8 @@
run = Bixebitîne
run-on = Li ser { $gpu } bixebitîne
run-on-default = (Berdest)
pin = Bo destgeha sepanan bi dar bixe
quit = Biqedîne
quit-all = Hemûyan biqedîne
new-window = Çarçoveya nû
cosmic-app-list = Destgeha sepanan

View file

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

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 = Вийти
quit-all = Закрити всі quit-all = Закрити всі
new-window = Нове вікно new-window = Нове вікно
run = Запустити run = Виконати
run-on = Запустити на { $gpu } run-on = Запустити на { $gpu }
run-on-default = (Основна) run-on-default = (Основна)

File diff suppressed because it is too large Load diff

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 // SPDX-License-Identifier: GPL-3.0-only
mod app; mod app;
mod icon_catalog;
mod launcher_edit;
mod localize; mod localize;
mod wayland_handler; mod wayland_handler;
mod wayland_subscription; mod wayland_subscription;

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
screen-reader = Xwînerê dîmenderê
magnifier = Mezinker
invert-colors = Rengan berevajî bike
settings = Sazkariyên gihîştinê...
filter-colors = Rengan parzûn bike
high-contrast = Dijbiriya bilind

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
output = Deran
input = Ketan
unknown-artist = Nenas
sound-settings = Sazkariyên dengê...
disconnected = Girêdana PulseAudio qut bû
no-device = Tu amûr nehatine hilbijartin
show-media-controls = Kontrolên mediyayê li ser destgehê nîşan bide

View file

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

View file

@ -2,12 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
mod localize; mod localize;
mod model;
mod mouse_area; mod mouse_area;
use crate::localize::localize; use crate::localize::localize;
use config::{AudioAppletConfig, amplification_sink, amplification_source}; use config::{AudioAppletConfig, amplification_sink, amplification_source};
use cosmic::{ use cosmic::{
Element, Renderer, Task, Theme, app, Apply, Element, Renderer, Task, Theme, app,
applet::{ applet::{
column as applet_column, column as applet_column,
cosmic_panel_config::PanelAnchor, cosmic_panel_config::PanelAnchor,
@ -18,18 +19,24 @@ use cosmic::{
cosmic_config::CosmicConfigEntry, cosmic_config::CosmicConfigEntry,
cosmic_theme::Spacing, cosmic_theme::Spacing,
iced::{ iced::{
self, Alignment, Length, Subscription, self, Alignment, Length, Rectangle, Subscription,
futures::StreamExt, futures::StreamExt,
widget::{self, column, row, slider}, widget::{self, column, row, slider},
window, window,
}, },
surface, theme, surface, theme,
widget::{Row, button, container, divider, icon, space, text, toggler}, widget::{
Row, button, container, divider, icon,
rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
space, text, toggler,
},
}; };
use cosmic_settings_sound_subscription as css; use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use futures::SinkExt;
use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup}; use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup};
use mpris_subscription::{MprisRequest, MprisUpdate}; use mpris_subscription::{MprisRequest, MprisUpdate};
use mpris2_zbus::player::PlaybackStatus; use mpris2_zbus::player::PlaybackStatus;
use std::{cell::RefCell, rc::Rc, sync::Arc};
mod config; mod config;
mod mpris_subscription; mod mpris_subscription;
@ -50,8 +57,10 @@ pub struct Audio {
core: cosmic::app::Core, core: cosmic::app::Core,
/// Track the applet's popup window. /// Track the applet's popup window.
popup: Option<window::Id>, popup: Option<window::Id>,
/// The model from cosmic-settings for managing pipewire devices. /// Varlink connection to `com.system76.CosmicSettings.Audio`.
model: css::Model, audio_client: Option<Rc<RefCell<audio_client::Client>>>,
/// Known audio device state
model: model::Model,
/// Whether to expand the revealer of a source or sink device. /// Whether to expand the revealer of a source or sink device.
is_open: IsOpen, is_open: IsOpen,
/// Max slider volume for the sink device, as determined by the amplification property. /// Max slider volume for the sink device, as determined by the amplification property.
@ -68,12 +77,14 @@ pub struct Audio {
player_status: Option<mpris_subscription::PlayerStatus>, player_status: Option<mpris_subscription::PlayerStatus>,
/// Used to request an activation token for opening cosmic-settings. /// Used to request an activation token for opening cosmic-settings.
token_tx: Option<calloop::channel::Sender<TokenRequest>>, token_tx: Option<calloop::channel::Sender<TokenRequest>>,
rectangle_tracker: Option<RectangleTracker<u32>>,
rectangle: Option<iced::Rectangle>,
} }
impl Audio { impl Audio {
fn output_icon_name(&self) -> &'static str { fn output_icon_name(&self) -> &'static str {
let volume = self.model.sink_volume; let volume = self.model.active_sink.volume;
let mute = self.model.sink_mute; let mute = self.model.active_sink.mute;
if mute || volume == 0 { if mute || volume == 0 {
"audio-volume-muted-symbolic" "audio-volume-muted-symbolic"
} else if volume < 33 { } else if volume < 33 {
@ -88,8 +99,8 @@ impl Audio {
} }
fn input_icon_name(&self) -> &'static str { fn input_icon_name(&self) -> &'static str {
let volume = self.model.source_volume; let volume = self.model.active_source.volume;
let mute = self.model.source_mute; let mute = self.model.active_source.mute;
if mute || volume == 0 { if mute || volume == 0 {
"microphone-sensitivity-muted-symbolic" "microphone-sensitivity-muted-symbolic"
} else if volume < 33 { } else if volume < 33 {
@ -110,8 +121,10 @@ enum IsOpen {
Input, Input,
} }
#[derive(Debug, Clone)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
/// Connection to `com.system76.CosmicSettings`.
Client(Arc<audio_client::Client>),
Ignore, Ignore,
SetSinkVolume(u32), SetSinkVolume(u32),
SetSourceVolume(u32), SetSourceVolume(u32),
@ -129,8 +142,9 @@ pub enum Message {
MprisRequest(MprisRequest), MprisRequest(MprisRequest),
Token(TokenUpdate), Token(TokenUpdate),
OpenSettings, OpenSettings,
Subscription(css::Message), Subscription(audio_client::Event),
Surface(surface::Action), Surface(surface::Action),
Rectangle(RectangleUpdate<u32>),
} }
// TODO // TODO
@ -242,15 +256,9 @@ impl cosmic::Application for Audio {
const APP_ID: &'static str = "com.system76.CosmicAppletAudio"; const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) { fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
let mut model = css::Model::default();
model.unplugged_text = "Unplugged".into();
model.hd_audio_text = "HD Audio".into();
model.usb_audio_text = "USB Audio".into();
( (
Self { Self {
core, core,
model,
..Default::default() ..Default::default()
}, },
Task::none(), Task::none(),
@ -271,6 +279,14 @@ impl cosmic::Application for Audio {
fn update(&mut self, message: Message) -> app::Task<Message> { fn update(&mut self, message: Message) -> app::Task<Message> {
match message { match message {
Message::Rectangle(u) => match u {
RectangleUpdate::Rectangle(r) => {
self.rectangle = Some(r.1);
}
RectangleUpdate::Init(tracker) => {
self.rectangle_tracker.replace(tracker);
}
},
Message::Ignore => {} Message::Ignore => {}
Message::TogglePopup => { Message::TogglePopup => {
if let Some(p) = self.popup.take() { if let Some(p) = self.popup.take() {
@ -291,14 +307,21 @@ impl cosmic::Application for Audio {
(100, &[][..]) (100, &[][..])
}; };
let popup_settings = self.core.applet.get_popup_settings( let mut popup_settings = self.core.applet.get_popup_settings(
self.core.main_window_id().unwrap(), self.core.main_window_id().unwrap(),
new_id, new_id,
None, None,
None, None,
None, None,
); );
if let Some(r) = self.rectangle {
popup_settings.positioner.anchor_rect = Rectangle {
x: r.x as i32,
y: r.y as i32,
width: r.width as i32,
height: r.height as i32,
};
}
return get_popup(popup_settings); return get_popup(popup_settings);
} }
} }
@ -318,52 +341,74 @@ impl cosmic::Application for Audio {
} }
} }
Message::Subscription(message) => { Message::Subscription(message) => {
return self self.model.update(message);
.model
.update(message)
.map(|message| Message::Subscription(message).into());
} }
Message::SetDefaultSink(pos) => { Message::SetDefaultSink(pos) => {
return self if let Some(&pos) = self.model.sinks.sorted_index.get(pos)
.model && let Some(&node_id) = self.model.sinks.id.get(pos as usize)
.set_default_sink(pos) && let Some(client) = self.audio_client.as_mut()
.map(|message| Message::Subscription(message).into()); {
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_default(node_id, true).await;
});
}
} }
Message::SetDefaultSource(pos) => { Message::SetDefaultSource(pos) => {
return self if let Some(&pos) = self.model.sources.sorted_index.get(pos)
.model && let Some(&node_id) = self.model.sources.id.get(pos as usize)
.set_default_source(pos) && let Some(client) = self.audio_client.as_mut()
.map(|message| Message::Subscription(message).into()); {
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_default(node_id, true).await;
});
}
} }
Message::ToggleSinkMute => self.model.toggle_sink_mute(), Message::ToggleSinkMute => {
if let Some(ref mut client) = self.audio_client {
futures::executor::block_on(async {
_ = client.borrow_mut().conn.sink_mute_toggle().await;
});
}
}
Message::ToggleSourceMute => self.model.toggle_source_mute(), Message::ToggleSourceMute => {
if let Some(ref mut client) = self.audio_client {
futures::executor::block_on(async {
_ = client.borrow_mut().conn.source_mute_toggle().await;
});
}
}
Message::SetSinkVolume(volume) => { Message::SetSinkVolume(volume) => {
return self if let Some(ref mut client) = self.audio_client {
.model self.model.active_sink.volume = volume;
.set_sink_volume(volume) self.model.active_sink.volume_text = volume.to_string();
.map(|message| Message::Subscription(message).into()); futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_sink_volume(volume).await;
});
}
} }
Message::SetSourceVolume(volume) => { Message::SetSourceVolume(volume) => {
return self if let Some(ref mut client) = self.audio_client {
.model self.model.active_source.volume = volume;
.set_source_volume(volume) self.model.active_source.volume_text = volume.to_string();
.map(|message| Message::Subscription(message).into()); futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_source_volume(volume).await;
});
}
} }
Message::ToggleMediaControlsInTopPanel(enabled) => { Message::ToggleMediaControlsInTopPanel(enabled) => {
self.config.show_media_controls_in_top_panel = enabled; self.config.show_media_controls_in_top_panel = enabled;
if let Ok(helper) = if let Ok(helper) =
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION) cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
&& let Err(err) = self.config.write_entry(&helper)
{ {
if let Err(err) = self.config.write_entry(&helper) { tracing::error!(?err, "Error writing config");
tracing::error!(?err, "Error writing config");
}
} }
} }
Message::CloseRequested(id) => { Message::CloseRequested(id) => {
@ -461,6 +506,12 @@ impl cosmic::Application for Audio {
cosmic::app::Action::Surface(a), cosmic::app::Action::Surface(a),
)); ));
} }
Message::Client(client) => {
if let Some(client) = Arc::into_inner(client) {
self.audio_client = Some(Rc::new(RefCell::new(client)));
self.model = model::Model::default();
}
}
} }
Task::none() Task::none()
@ -476,7 +527,50 @@ impl cosmic::Application for Audio {
}), }),
mpris_subscription::mpris_subscription(0).map(Message::Mpris), mpris_subscription::mpris_subscription(0).map(Message::Mpris),
activation_token_subscription(0).map(Message::Token), activation_token_subscription(0).map(Message::Token),
Subscription::run(|| css::watch().map(Message::Subscription)), Subscription::run(|| {
iced::stream::channel(
1,
move |mut emitter: futures::channel::mpsc::Sender<_>| async move {
loop {
let mut client = match audio_client::connect().await {
Ok(client) => client,
Err(why) => {
if let zlink::Error::Io(ref why) = why
&& why.kind() == std::io::ErrorKind::NotFound
{
tracing::error!(
"cosmic-settings-daemon varlink service not found."
);
} else {
tracing::error!(
?why,
"failed to connect to cosmic-settings's varlink service"
);
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
continue;
}
};
if let Ok(Ok(mut stream)) = client.recv_events().await {
_ = emitter.send(Message::Client(Arc::new(client))).await;
while let Some(message) = stream.next().await {
match message {
Ok(event) => {
_ = emitter.send(Message::Subscription(event)).await;
}
Err(why) => {
tracing::error!(?why, "event error");
}
}
}
}
}
},
)
}),
rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)),
]) ])
} }
@ -487,63 +581,94 @@ impl cosmic::Application for Audio {
.icon_button(self.output_icon_name()) .icon_button(self.output_icon_name())
.on_press_down(Message::TogglePopup); .on_press_down(Message::TogglePopup);
const WHEEL_STEP: f32 = 5.0; // 5% per wheel event const WHEEL_STEP: f32 = 5.0; // 5% par cran logique
// Wayland axis_v120 envoie un cran physique en rafale de plusieurs
// ScrollDelta::Pixels (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 btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
let scroll_vector = match delta { let scroll_vector = match delta {
iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1 iced::mouse::ScrollDelta::Lines { y, .. } => {
iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1 PIXEL_ACC.with(|a| a.set(0.0));
y * WHEEL_STEP
}
iced::mouse::ScrollDelta::Pixels { y, .. } => {
PIXEL_ACC.with(|acc_cell| {
let acc = acc_cell.get() + y;
let steps = (acc / PIXEL_THRESHOLD).trunc();
acc_cell.set(acc - steps * PIXEL_THRESHOLD);
steps * WHEEL_STEP
})
}
}; };
if scroll_vector == 0.0 { if scroll_vector == 0.0 {
return Message::Ignore; return Message::Ignore;
} }
let new_volume = (self.model.sink_volume as f64 + (scroll_vector as f64)) let new_volume = (self.model.active_sink.volume as f64 + (scroll_vector as f64))
.clamp(0.0, self.max_sink_volume as f64); .clamp(0.0, self.max_sink_volume as f64);
Message::SetSinkVolume(new_volume as u32) Message::SetSinkVolume(new_volume.round() as u32)
}); });
let mut has_playback_buttons = false;
let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| { let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
// if we have a configure for width and height, we're in a overflow popup // if we have a configure for width and height, we're in a overflow popup
c.width > 0. && c.height > 0. c.width > 0. && c.height > 0.
})) }))
.then(|| self.playback_buttons()); .then(|| self.playback_buttons());
self.core let mut ret = if let Some(playback_buttons) = playback_buttons
.applet && !playback_buttons.is_empty()
.autosize_window( {
if let Some(playback_buttons) = playback_buttons has_playback_buttons = true;
&& !playback_buttons.is_empty() Element::from(match self.core.applet.anchor {
{ PanelAnchor::Left | PanelAnchor::Right => Element::from(
match self.core.applet.anchor { applet_column::Column::with_children(playback_buttons)
PanelAnchor::Left | PanelAnchor::Right => Element::from( .push(btn)
applet_column::Column::with_children(playback_buttons) .align_x(Alignment::Center)
.push(btn) .height(Length::Shrink)
.align_x(Alignment::Center) // TODO configurable variable from the panel?
.height(Length::Shrink) .spacing(
// TODO configurable variable from the panel? -(self.core.applet.suggested_padding(true).0 as f32)
.spacing( * self.core.applet.padding_overlap,
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
),
), ),
PanelAnchor::Top | PanelAnchor::Bottom => { ),
applet_row::Row::with_children(playback_buttons) PanelAnchor::Top | PanelAnchor::Bottom => {
.push(btn) applet_row::Row::with_children(playback_buttons)
.align_y(Alignment::Center) .push(btn)
.width(Length::Shrink) .align_y(Alignment::Center)
// TODO configurable variable from the panel? .width(Length::Shrink)
.spacing( // TODO configurable variable from the panel?
-(self.core.applet.suggested_padding(true).0 as f32) .spacing(
* self.core.applet.padding_overlap, -(self.core.applet.suggested_padding(true).0 as f32)
) * self.core.applet.padding_overlap,
.into() )
} .into()
} }
} else { })
btn.into() } else {
}, btn.into()
) };
.into()
if let Some(tracker) = self.rectangle_tracker.as_ref()
&& has_playback_buttons
{
ret = tracker.container(0, ret).into()
}
if !self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
// if we have a configure for width and height, we're in a overflow popup
c.width > 0. && c.height > 0.
}) {
ret = self.core.applet.autosize_window(ret).into();
}
ret
} }
fn view_window(&self, _id: window::Id) -> Element<'_, Message> { fn view_window(&self, _id: window::Id) -> Element<'_, Message> {
@ -551,19 +676,21 @@ impl cosmic::Application for Audio {
space_xxs, space_s, .. space_xxs, space_s, ..
} = theme::active().cosmic().spacing; } = theme::active().cosmic().spacing;
let sink = self let sink: Option<&str> = self
.model .model
.active_sink() .sinks
.and_then(|pos| self.model.sinks().get(pos)); .active()
let source = self .map(|pos| self.model.sinks.sorted_display[pos].as_ref());
let source: Option<&str> = self
.model .model
.active_source() .sources
.and_then(|pos| self.model.sources().get(pos)); .active()
.map(|pos| self.model.sources.sorted_display[pos].as_ref());
let mut audio_content = { let mut audio_content = {
let output_slider = slider( let output_slider = slider(
0..=self.max_sink_volume, 0..=self.max_sink_volume,
self.model.sink_volume, self.model.active_sink.volume,
Message::SetSinkVolume, Message::SetSinkVolume,
) )
.width(Length::FillPortion(5)) .width(Length::FillPortion(5))
@ -571,7 +698,7 @@ impl cosmic::Application for Audio {
let input_slider = slider( let input_slider = slider(
0..=self.max_source_volume, 0..=self.max_source_volume,
self.model.source_volume, self.model.active_source.volume,
Message::SetSourceVolume, Message::SetSourceVolume,
) )
.width(Length::FillPortion(5)) .width(Length::FillPortion(5))
@ -590,7 +717,7 @@ impl cosmic::Application for Audio {
.line_height(24) .line_height(24)
.on_press(Message::ToggleSinkMute), .on_press(Message::ToggleSinkMute),
output_slider, output_slider,
container(text(&self.model.sink_volume_text).size(16)) container(text(&self.model.active_sink.volume_text).size(16))
.width(Length::FillPortion(1)) .width(Length::FillPortion(1))
.align_x(Alignment::End) .align_x(Alignment::End)
] ]
@ -609,7 +736,7 @@ impl cosmic::Application for Audio {
.line_height(24) .line_height(24)
.on_press(Message::ToggleSourceMute), .on_press(Message::ToggleSourceMute),
input_slider, input_slider,
container(text(&self.model.source_volume_text).size(16)) container(text(&self.model.active_source.volume_text).size(16))
.width(Length::FillPortion(1)) .width(Length::FillPortion(1))
.align_x(Alignment::End) .align_x(Alignment::End)
] ]
@ -624,7 +751,7 @@ impl cosmic::Application for Audio {
Some(sink) => sink.to_owned(), Some(sink) => sink.to_owned(),
None => fl!("no-device"), None => fl!("no-device"),
}, },
self.model.sinks(), &self.model.sinks.sorted_display,
Message::OutputToggle, Message::OutputToggle,
Message::SetDefaultSink, Message::SetDefaultSink,
), ),
@ -635,7 +762,7 @@ impl cosmic::Application for Audio {
Some(source) => source.to_owned(), Some(source) => source.to_owned(),
None => fl!("no-device"), None => fl!("no-device"),
}, },
self.model.sources(), &self.model.sources.sorted_display,
Message::InputToggle, Message::InputToggle,
Message::SetDefaultSource, Message::SetDefaultSource,
) )
@ -691,19 +818,15 @@ impl cosmic::Application for Audio {
} }
if let Some(play) = self.is_play() { if let Some(play) = self.is_play() {
control_elements.push( control_elements.push(
button::icon( button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true))
icon::from_name(if play { PLAY } else { PAUSE }) .extra_small()
.size(32) .class(cosmic::theme::Button::AppletIcon)
.symbolic(true), .on_press(if play {
) Message::MprisRequest(MprisRequest::Play)
.extra_small() } else {
.class(cosmic::theme::Button::AppletIcon) Message::MprisRequest(MprisRequest::Pause)
.on_press(if play { })
Message::MprisRequest(MprisRequest::Play) .into(),
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
); );
} }
if let Some(go_next) = self.go_next(32) { if let Some(go_next) = self.go_next(32) {
@ -754,20 +877,21 @@ impl cosmic::Application for Audio {
} }
} }
fn revealer( fn revealer<'a>(
open: bool, open: bool,
title: String, title: String,
selected: String, selected: String,
devices: &[String], devices: &'a [Arc<str>],
toggle: Message, toggle: Message,
mut change: impl FnMut(usize) -> Message + 'static, mut change: impl FnMut(usize) -> Message + 'static,
) -> widget::Column<'static, Message, crate::Theme, Renderer> { ) -> widget::Column<'a, Message, crate::Theme, Renderer> {
if open { if open {
devices.iter().cloned().enumerate().fold( devices.iter().enumerate().fold(
column![revealer_head(open, title, selected, toggle)].width(Length::Fill), column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|col, (id, name)| { move |col, (id, name)| {
col.push( col.push(
menu_button(text::body(name)) text::body(name.as_ref())
.apply(menu_button)
.on_press(change(id)) .on_press(change(id))
.width(Length::Fill) .width(Length::Fill)
.padding([8, 48]), .padding([8, 48]),
@ -785,9 +909,9 @@ fn revealer_head(
selected: String, selected: String,
toggle: Message, toggle: Message,
) -> cosmic::widget::Button<'static, Message> { ) -> cosmic::widget::Button<'static, Message> {
menu_button(column![ cosmic::widget::column::with_capacity(2)
text::body(title).width(Length::Fill), .push(text::body(title).width(Length::Fill))
text::caption(selected), .push(text::caption(selected))
]) .apply(menu_button)
.on_press(toggle) .on_press(toggle)
} }

View file

@ -0,0 +1,393 @@
// Copyright 2026 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::sync::Arc;
use cosmic_settings_audio_client::{self as audio_client, Availability, RouteInfo};
use intmap::IntMap;
pub type DeviceId = u32;
pub type NodeId = u32;
#[derive(Debug, Default)]
pub struct Model {
pub device_routes: IntMap<DeviceId, Vec<RouteInfo>>,
pub node_devices: IntMap<NodeId, Option<u32>>,
pub sinks: Nodes,
pub sources: Nodes,
pub active_sink: ActiveNode,
pub active_source: ActiveNode,
pub default_sink: Option<NodeId>,
pub default_source: Option<NodeId>,
}
#[derive(Debug, Default)]
pub struct Nodes {
active: Option<usize>,
pub sorted_display: Box<[Arc<str>]>,
pub sorted_index: Box<[u16]>,
pub balance: Vec<Option<f32>>,
pub card_profile_device: Vec<Option<u32>>,
pub description: Vec<String>,
pub devices: Vec<Option<NodeId>>,
pub display: Vec<Arc<str>>,
pub mute: Vec<bool>,
pub name: Vec<String>,
pub id: Vec<NodeId>,
pub volume: Vec<u32>,
}
impl Nodes {
pub fn active(&self) -> Option<usize> {
self.active.and_then(|active| {
self.sorted_index
.iter()
.position(|idx| *idx as usize == active)
})
}
pub fn dropdown_sort(&mut self) {
let mut enumerated_displays = self
.display
.clone()
.into_iter()
.enumerate()
.map(|(index, display)| (index as u16, display))
.collect::<Vec<_>>();
enumerated_displays.sort_by_key(|v| v.1.clone());
let (indexes, displays): (Vec<_>, Vec<_>) = enumerated_displays.into_iter().unzip();
self.sorted_display = displays.into_boxed_slice();
self.sorted_index = indexes.into_boxed_slice();
}
pub fn remove(&mut self, node_id: u32) -> bool {
let Some(pos) = self.id.iter().position(|id| node_id == *id) else {
return false;
};
self.balance.remove(pos);
self.card_profile_device.remove(pos);
self.description.remove(pos);
self.devices.remove(pos);
self.display.remove(pos);
self.mute.remove(pos);
self.name.remove(pos);
self.id.remove(pos);
self.volume.remove(pos);
self.dropdown_sort();
if self.active == Some(pos) {
self.active = None;
}
true
}
}
#[derive(Debug, Default)]
pub struct ActiveNode {
pub volume_text: String,
pub volume: u32,
pub mute: bool,
}
impl Model {
pub fn update(&mut self, event: audio_client::Event) {
tracing::debug!(?event, "update");
match event {
audio_client::Event::NodeMute(node_id, mute) => {
if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
self.sinks.mute[pos] = mute;
if self.sinks.active == Some(pos) {
self.active_sink.mute = mute;
}
} else if let Some(pos) = self.sources.id.iter().position(|id| node_id == *id) {
self.sources.mute[pos] = mute;
if self.sources.active == Some(pos) {
self.active_source.mute = mute;
}
}
}
audio_client::Event::NodeVolume(node_id, volume, balance) => {
if let Some(pos) = self.sinks.id.iter().position(|id| node_id == *id) {
self.sinks.volume[pos] = volume;
self.sinks.balance[pos] = balance;
if self.default_sink.as_ref().is_some_and(|&id| id == node_id)
&& let Some(pos) = self.sinks.active
{
self.active_sink.mute = self.sinks.mute[pos];
self.active_sink.volume = self.sinks.volume[pos];
self.active_sink.volume_text = self.active_sink.volume.to_string();
}
} else if let Some(pos) = self.sources.id.iter().position(|id| node_id == *id) {
self.sources.volume[pos] = volume;
self.sources.balance[pos] = balance;
if self
.default_source
.as_ref()
.is_some_and(|&id| id == node_id)
&& let Some(pos) = self.sources.active
{
self.active_source.mute = self.sources.mute[pos];
self.active_source.volume = self.sources.volume[pos];
self.active_source.volume_text = self.active_source.volume.to_string();
}
}
}
audio_client::Event::DefaultSink(node_id) => {
self.default_sink = Some(node_id);
if let Some(pos) = self.sinks.id.iter().position(|&id| id == node_id) {
self.sinks.active = Some(pos);
self.active_sink.mute = self.sinks.mute[pos];
self.active_sink.volume = self.sinks.volume[pos];
self.active_sink.volume_text = self.active_sink.volume.to_string();
}
}
audio_client::Event::DefaultSource(node_id) => {
self.default_source = Some(node_id);
if let Some(pos) = self.sources.id.iter().position(|&id| id == node_id) {
self.sources.active = Some(pos);
self.active_source.mute = self.sources.mute[pos];
self.active_source.volume = self.sources.volume[pos];
self.active_source.volume_text = self.active_source.volume.to_string();
}
}
audio_client::Event::Node(node_id, node) => {
self.node_devices.insert(node_id, node.device_id);
if node.is_sink {
let pos = if let Some(pos) = self.sinks.id.iter().position(|&id| id == node_id)
{
self.sinks.description[pos] = self.translate(&node.description);
self.sinks.name[pos] = node.name;
self.sinks.card_profile_device[pos] = node.card_profile_device;
pos
} else {
self.sinks.display.push(Arc::default());
self.sinks
.description
.push(self.translate(&node.description));
self.sinks.id.push(node_id);
self.sinks.volume.push(0);
self.sinks.balance.push(None);
self.sinks.mute.push(false);
self.sinks.name.push(node.name);
self.sinks.devices.push(node.device_id);
self.sinks
.card_profile_device
.push(node.card_profile_device);
self.sinks.id.len() - 1
};
self.sinks.display[pos] = node
.device_id
.zip(node.card_profile_device)
.and_then(|(device_id, node_card_profile_device)| {
let routes = self.device_routes.get(device_id)?;
for route in routes {
if matches!(route.availability, Availability::No) || !route.is_sink
{
continue;
}
if route.devices.contains(&node_card_profile_device) {
return Some(node_name(
&self.translate(&route.description),
&self.sinks.description[pos],
));
}
}
None
})
.unwrap_or_else(|| {
node_name(
&node.device_profile_description,
&self.sinks.description[pos],
)
});
self.sinks.dropdown_sort();
if let Some(default_node_id) = self.default_sink
&& default_node_id == node_id
{
self.sinks.active = Some(pos);
self.active_sink.mute = self.sinks.mute[pos];
self.active_sink.volume = self.sinks.volume[pos];
self.active_sink.volume_text = self.active_sink.volume.to_string();
}
} else {
let pos =
if let Some(pos) = self.sources.id.iter().position(|&id| id == node_id) {
self.sources.description[pos] = self.translate(&node.description);
self.sources.name[pos] = node.name;
self.sources.card_profile_device[pos] = node.card_profile_device;
pos
} else {
self.sources
.description
.push(self.translate(&node.description));
self.sources.display.push(Arc::default());
self.sources.id.push(node_id);
self.sources.volume.push(0);
self.sources.balance.push(None);
self.sources.mute.push(false);
self.sources.name.push(node.name);
self.sources.devices.push(node.device_id);
self.sources
.card_profile_device
.push(node.card_profile_device);
self.sources.id.len() - 1
};
if let Some(name) = node
.device_id
.zip(node.card_profile_device)
.map(|(device_id, node_card_profile_device)| {
let routes = self.device_routes.get(device_id)?;
for route in routes {
if route.is_sink || matches!(route.availability, Availability::No) {
continue;
}
if route.devices.contains(&node_card_profile_device) {
return Some(node_name(
&self.translate(&route.description),
&self.sources.description[pos],
));
}
}
None
})
.unwrap_or_else(|| {
Some(node_name(
&node.device_profile_description,
&self.sources.description[pos],
))
})
{
self.sources.display[pos] = name;
self.sources.dropdown_sort();
} else {
// Remove sources that are unplugged.
self.sources.remove(node_id);
return;
}
if let Some(default_node_id) = self.default_source
&& default_node_id == node_id
{
self.sources.active = Some(pos);
self.active_source.mute = self.sources.mute[pos];
self.active_source.volume = self.sources.volume[pos];
self.active_source.volume_text = self.active_source.volume.to_string();
}
}
}
audio_client::Event::ActiveRoute(device_id, _index, route) => {
self.update_device_names(device_id, &route);
}
audio_client::Event::Route(device_id, index, route) => {
let routes = self.device_routes.entry(device_id).or_default();
if index == 0 {
*routes = vec![route];
} else {
if routes.len() < index as usize + 1 {
let additional = (index as usize + 1) - routes.capacity();
routes.reserve_exact(additional);
routes.extend(std::iter::repeat_n(RouteInfo::default(), additional));
}
routes[index as usize] = route;
}
}
audio_client::Event::RemoveNode(node_id) => {
self.node_devices.remove(node_id);
if !self.sinks.remove(node_id) {
self.sources.remove(node_id);
}
}
audio_client::Event::RemoveDevice(device_id) => {
self.device_routes.remove(device_id);
}
_ => (),
}
}
fn update_device_names(&mut self, device_id: DeviceId, route: &RouteInfo) {
if matches!(route.availability, Availability::No) {
return;
}
let compatible_nodes = self.node_devices.iter().filter_map(|(node, &dev_id)| {
if dev_id? == device_id {
Some(node)
} else {
None
}
});
if route.is_sink {
for n_id in compatible_nodes {
let Some(pos) = self.sinks.id.iter().position(|&node| node == n_id) else {
continue;
};
let Some(card_profile_device) = self.sinks.card_profile_device[pos] else {
continue;
};
if route.devices.contains(&card_profile_device) {
self.sinks.display[pos] = node_name(
&self.translate(&route.description),
&self.sinks.description[pos],
);
self.sinks.dropdown_sort();
break;
}
}
} else {
for n_id in compatible_nodes {
let Some(pos) = self.sources.id.iter().position(|&node| node == n_id) else {
continue;
};
let Some(card_profile_device) = self.sources.card_profile_device[pos] else {
continue;
};
if route.devices.contains(&card_profile_device) {
self.sources.display[pos] = node_name(
&self.translate(&route.description),
&self.sources.description[pos],
);
self.sources.dropdown_sort();
break;
}
}
}
}
pub fn translate(&self, description: &str) -> String {
description
.replace("High Definition", "HD")
.replace("DisplayPort", "DP")
.replace("Controller", "")
}
}
fn node_name(route: &str, node: &str) -> Arc<str> {
if route.is_empty() {
node.to_owned()
} else {
[route, " - ", node].concat()
}
.into()
}

View file

@ -5,7 +5,7 @@ use cosmic::iced::core::Point;
use cosmic::iced::core::{ use cosmic::iced::core::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget, Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
event::{self, Event}, event::Event,
layout, mouse, overlay, renderer, touch, layout, mouse, overlay, renderer, touch,
widget::{Operation, Tree, tree}, widget::{Operation, Tree, tree},
}; };
@ -307,6 +307,19 @@ fn update<Message: Clone, Theme, Renderer>(
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
state: &mut State, state: &mut State,
) { ) {
// Handle wheel events before the bounds check. The panel's nested
// compositor routes axis events to this surface only when the pointer
// is over the applet, but the cursor coordinates it sends may be in
// panel-local space rather than surface-local space. This makes
// cursor.is_over() unreliable for scroll events.
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
if let Some(message) = widget.on_mouse_wheel.as_ref() {
shell.publish((message)(*delta));
shell.capture_event();
return;
}
}
if !cursor.is_over(layout.bounds()) { if !cursor.is_over(layout.bounds()) {
if !state.is_out_of_bounds { if !state.is_out_of_bounds {
if widget if widget
@ -416,12 +429,4 @@ fn update<Message: Clone, Theme, Renderer>(
} }
} }
} }
if let Some(message) = widget.on_mouse_wheel.as_ref() {
if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event {
shell.publish((message)(*delta));
shell.capture_event();
return;
}
}
} }

View file

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

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

@ -1,13 +1,13 @@
battery = Akkumulátor battery = Akkumulátor
battery-desc = Csökkentett energiafogyasztás és teljesítmény battery-desc = Csökkentett energiafogyasztás és teljesítmény.
balanced = Kiegyensúlyozott balanced = Kiegyensúlyozott
balanced-desc = Normál teljesítmény és akkumulátorhasználat balanced-desc = Normál teljesítmény és akkumulátorhasználat.
performance = Nagy teljesítmény performance = Nagy teljesítmény
performance-desc = Nagy teljesítmény és energiafogyasztás performance-desc = Nagy teljesítmény és energiafogyasztás.
max-charge = Az akkumulátor élettartamának növelése érdekében állítsa a maximális töltési szintet 80%-ra max-charge = Az akkumulátor élettartamának növelése érdekében állítsd a maximális töltési szintet 80%-ra
seconds = másodperc seconds = mp
minutes = perc minutes = p
hours = óra hours = ó
until-empty = a lemerülésig until-empty = a lemerülésig
power-settings = Energia- és akkumulátorbeállítások… power-settings = Energia- és akkumulátorbeállítások…
dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát dgpu-running = A dedikált GPU aktív, ami csökkentheti az akkumulátor élettartamát

View file

@ -0,0 +1,14 @@
battery = Betarî
seconds = ç
minutes = x
hours = d
until-empty = Heya vala dibe
power-settings = Sazkariyên hêz û betariyê...
battery-desc = Bikaranîna hêzê kêmkirî û performans.
balanced = Hevseng
balanced-desc = Performansa standard û bikaranîna betariyê.
performance = Performansa bilind
performance-desc = Performansa bilind û bikaranîna hêzê.
max-charge = Jiyana betariyê xwe dirêj bike bi riya danîna sînorê herî mezin yê tijîkrinê %80
dgpu-running = YKG a veqetandî çalak e û dikare jiyana betariyê kêm bike
dgpu-applications = Sepanên ku YKG a veqetandî ya { $gpu_name } bi kar tînin

View file

@ -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 = 降低效能與耗電量。 battery-desc = 降低效能與耗電量。
balanced = 平衡 balanced = 平衡
balanced-desc = 標準效能與耗電量。 balanced-desc = 標準效能與耗電量。

View file

@ -40,7 +40,7 @@ use cosmic_settings_upower_subscription::{
}; };
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::{path::PathBuf, sync::LazyLock, time::Duration}; use std::{path::PathBuf, time::Duration};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
// XXX improve // XXX improve
@ -169,9 +169,24 @@ impl CosmicBatteryApplet {
self.charging_limit = Some(limit); self.charging_limit = Some(limit);
self.update_battery(self.battery_percent, self.on_battery); self.update_battery(self.battery_percent, self.on_battery);
} }
fn panel_icon_name(&self) -> &str {
if self.no_battery {
if self.screen_brightness.is_some() {
self.display_icon_name.as_str()
} else if self.kbd_brightness.is_some() {
"keyboard-brightness-symbolic"
} else {
"plugged-into-power-symbolic"
}
} else {
self.icon_name.as_str()
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message { enum Message {
TogglePopup, TogglePopup,
CloseRequested(window::Id), CloseRequested(window::Id),
@ -368,6 +383,10 @@ impl cosmic::Application for CosmicBatteryApplet {
let _ = tx.send(()); let _ = tx.send(());
} }
let mut tasks = vec![get_popup(popup_settings)]; let mut tasks = vec![get_popup(popup_settings)];
if let Some(tx) = &self.settings_daemon_sender {
let _ = tx.send(settings_daemon::Request::GetDisplayBrightness);
let _ = tx.send(settings_daemon::Request::GetMaxDisplayBrightness);
}
// Try again every time a popup is opened // Try again every time a popup is opened
if self.charging_limit.is_none() { if self.charging_limit.is_none() {
tasks.push(Task::perform(get_charging_limit(), |limit| { tasks.push(Task::perform(get_charging_limit(), |limit| {
@ -507,7 +526,7 @@ impl cosmic::Application for CosmicBatteryApplet {
Task::none() Task::none()
} }
fn view(&self) -> Element<Message> { fn view(&self) -> Element<'_, Message> {
let is_horizontal = match self.core.applet.anchor { let is_horizontal = match self.core.applet.anchor {
PanelAnchor::Top | PanelAnchor::Bottom => true, PanelAnchor::Top | PanelAnchor::Bottom => true,
PanelAnchor::Left | PanelAnchor::Right => false, PanelAnchor::Left | PanelAnchor::Right => false,
@ -517,19 +536,9 @@ impl cosmic::Application for CosmicBatteryApplet {
let applet_padding = self.core.applet.suggested_padding(true); let applet_padding = self.core.applet.suggested_padding(true);
let mut children = vec![ let mut children = vec![
icon::from_name(if self.no_battery { icon::from_name(self.panel_icon_name())
if self.screen_brightness.is_some() { .size(suggested_size.0)
self.display_icon_name.as_str() .into(),
} else if self.kbd_brightness.is_some() {
"keyboard-brightness-symbolic"
} else {
self.icon_name.as_str()
}
} else {
self.icon_name.as_str()
})
.size(suggested_size.0)
.into(),
]; ];
if self.config.show_percentage && !self.no_battery { if self.config.show_percentage && !self.no_battery {

View file

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

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

View file

@ -1,7 +1,7 @@
bluetooth = Bluetooth bluetooth = Bluetooth
other-devices = Egyéb Bluetooth-eszközök other-devices = Egyéb Bluetooth-eszközök
settings = Bluetooth-beállítások… settings = Bluetooth-beállítások…
connected = Csatlakoztatva connected = Kapcsolódva
confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal confirm-pin = Ellenőrizd, hogy a következő PIN-kód megegyezik-e a(z) { $deviceName } eszközön megjelenő PIN-kóddal
confirm = Megerősítés confirm = Megerősítés
cancel = Mégse cancel = Mégse

View file

@ -0,0 +1,12 @@
cancel = Têk bibe
bluetooth = Bluetooth
connected = Hat girêdan
other-devices = Amûrên Bluetooth ên din
settings = Sazkariyên Bluetooth...
confirm-pin = Tika ye piştrast bike ku PIN a jêrîn bi ya ku li ser { $deviceName } tê xuyakirin re li hev tê
confirm = Bipejirîne
check-device = Piştrast bike ku { $deviceName } çalak e, di hundir rêjeyê de ye, û amade ye ji bo cotkirinê.
try-again = Dîsa hewl bide
discoverable = Dîtbar
pairable = Cotbar
unsuccessful = Cotkirin têkçûyî ye

View file

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

View file

@ -21,10 +21,10 @@ use cosmic::{
widget::{Column, column, container, row}, widget::{Column, column, container, row},
}, },
theme, theme,
widget::{button, divider, icon, scrollable, text}, widget::{button, divider, icon, indeterminate_circular, scrollable, text},
}; };
use futures::FutureExt; use futures::FutureExt;
use std::{collections::HashMap, sync::LazyLock, time::Duration}; use std::{collections::HashMap, time::Duration};
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use crate::{ use crate::{
@ -63,6 +63,7 @@ impl CosmicBluetoothApplet {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message { enum Message {
TogglePopup, TogglePopup,
CloseRequested(window::Id), CloseRequested(window::Id),
@ -145,7 +146,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
} }
Message::BluetoothEvent(e) => match e { Message::BluetoothEvent(e) => match e {
BluerEvent::RequestResponse { BluerEvent::RequestResponse {
req, req: _,
state, state,
err_msg, err_msg,
} => { } => {
@ -367,14 +368,12 @@ impl cosmic::Application for CosmicBluetoothApplet {
); );
} }
BluerDeviceStatus::Paired => {} BluerDeviceStatus::Paired => {}
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => { BluerDeviceStatus::Connecting
row = row.push( | BluerDeviceStatus::Disconnecting
icon::from_name("process-working-symbolic") | BluerDeviceStatus::Pairing => {
.size(24) row = row.push(indeterminate_circular().size(24.0));
.symbolic(true),
);
} }
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue, BluerDeviceStatus::Disconnected => continue,
} }
known_bluetooth.push( known_bluetooth.push(

View file

@ -149,8 +149,6 @@ pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
}, },
BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e), BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
_ => return,
}; };
_ = output.send(message).await; _ = output.send(message).await;
@ -199,6 +197,7 @@ pub enum BluerRequest {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum BluerEvent { pub enum BluerEvent {
RequestResponse { RequestResponse {
req: BluerRequest, req: BluerRequest,
@ -568,22 +567,21 @@ impl BluerSessionState {
tick(&mut interval).await; tick(&mut interval).await;
let new_status = adapter_clone.is_powered().await.unwrap_or_default(); let new_status = adapter_clone.is_powered().await.unwrap_or_default();
devices = build_device_list(devices, &adapter_clone).await; devices = build_device_list(devices, &adapter_clone).await;
if new_status != status { let power_changed = new_status != status;
status = new_status; status = new_status;
let state = BluerState { let state = BluerState {
devices: devices.clone(), devices: devices.clone(),
bluetooth_enabled: status, bluetooth_enabled: status,
}; };
if state.bluetooth_enabled { if power_changed && state.bluetooth_enabled {
for d in &state.devices { for d in &state.devices {
if d.paired_and_trusted() { if d.paired_and_trusted() {
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await; _ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
}
} }
} }
_ = wake_up_discover_tx.send(()).await; _ = wake_up_discover_tx.send(()).await;
let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await;
} }
let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await;
} }
}); });
} }
@ -674,6 +672,9 @@ impl BluerSessionState {
} }
} }
// Always refresh power state from the adapter
// to avoid stale cached is_powered values
is_powered = adapter_clone.is_powered().await.unwrap_or_default();
let _ = tx let _ = tx
.send(BluerSessionEvent::ChangesProcessed(BluerState { .send(BluerSessionEvent::ChangesProcessed(BluerState {
devices: devices.clone(), devices: devices.clone(),
@ -853,13 +854,7 @@ async fn bluer_state(adapter: &Adapter) -> BluerState {
#[inline(never)] #[inline(never)]
async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> { async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> {
let addrs: Vec<Address> = adapter let addrs = adapter.device_addresses().await.unwrap_or_default();
.device_addresses()
.await
.unwrap_or_default()
.into_iter()
.filter(|addr| !devices.iter().any(|d| d.address == *addr))
.collect();
devices.clear(); devices.clear();
if addrs.len() > devices.capacity() { if addrs.len() > devices.capacity() {

View file

@ -1,14 +1,14 @@
[package] [package]
name = "cosmic-applet-input-sources" name = "cosmic-applet-input-sources"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
[dependencies] [dependencies]
cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" } cosmic-comp-config.workspace = true
i18n-embed-fl.workspace = true i18n-embed-fl.workspace = true
i18n-embed.workspace = true i18n-embed.workspace = true
libcosmic.workspace = true cosmic.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
tokio.workspace = true tokio.workspace = true
tracing-log.workspace = true tracing-log.workspace = true

View file

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

View file

@ -0,0 +1,2 @@
show-keyboard-layout = Awaya kilîtdankê nîşan bide...
keyboard-settings = Sazkariyên kilîtdankê...

View file

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

View file

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

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-applet-minimize" name = "cosmic-applet-minimize"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
@ -9,7 +9,7 @@ anyhow.workspace = true
i18n-embed-fl.workspace = true i18n-embed-fl.workspace = true
i18n-embed.workspace = true i18n-embed.workspace = true
image = { version = "0.25.9", default-features = false } image = { version = "0.25.9", default-features = false }
libcosmic.workspace = true cosmic.workspace = true
memmap2 = "0.9.10" memmap2 = "0.9.10"
rust-embed.workspace = true rust-embed.workspace = true
rustix.workspace = true rustix.workspace = true

View file

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

View file

@ -121,6 +121,7 @@ struct AppData {
toplevel_info_state: ToplevelInfoState, toplevel_info_state: ToplevelInfoState,
toplevel_manager_state: ToplevelManagerState, toplevel_manager_state: ToplevelManagerState,
seat_state: SeatState, seat_state: SeatState,
captured_toplevels: std::collections::HashSet<ExtForeignToplevelHandleV1>,
} }
struct CaptureData { struct CaptureData {
@ -351,6 +352,36 @@ impl AppData {
} }
}); });
} }
fn handle_toplevel(&mut self, toplevel: &ExtForeignToplevelHandleV1, is_new: bool) {
let Some(info) = self.toplevel_info_state.info(toplevel) else {
return;
};
if info
.state
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
{
// Capture a thumbnail once, on the transition into minimized.
if self.captured_toplevels.insert(toplevel.clone()) {
self.send_image(toplevel.clone());
}
let update = if is_new {
ToplevelUpdate::Add(info.clone())
} else {
ToplevelUpdate::Update(info.clone())
};
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(update)));
} else {
self.remove_toplevel(toplevel);
}
}
fn remove_toplevel(&mut self, toplevel: &ExtForeignToplevelHandleV1) {
self.captured_toplevels.remove(toplevel);
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
ToplevelUpdate::Remove(toplevel.clone()),
)));
}
} }
impl ToplevelInfoHandler for AppData { impl ToplevelInfoHandler for AppData {
@ -364,23 +395,7 @@ impl ToplevelInfoHandler for AppData {
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
toplevel: &ExtForeignToplevelHandleV1, toplevel: &ExtForeignToplevelHandleV1,
) { ) {
if let Some(info) = self.toplevel_info_state.info(toplevel) { self.handle_toplevel(toplevel, true);
if info
.state
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
{
// spawn thread for sending the image
self.send_image(toplevel.clone());
let _ = futures::executor::block_on(
self.tx
.send(WaylandUpdate::Toplevel(ToplevelUpdate::Add(info.clone()))),
);
} else {
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
ToplevelUpdate::Remove(toplevel.clone()),
)));
}
}
} }
fn update_toplevel( fn update_toplevel(
@ -389,21 +404,7 @@ impl ToplevelInfoHandler for AppData {
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
toplevel: &ExtForeignToplevelHandleV1, toplevel: &ExtForeignToplevelHandleV1,
) { ) {
if let Some(info) = self.toplevel_info_state.info(toplevel) { self.handle_toplevel(toplevel, false);
if info
.state
.contains(&zcosmic_toplevel_handle_v1::State::Minimized)
{
self.send_image(toplevel.clone());
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
ToplevelUpdate::Update(info.clone()),
)));
} else {
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel(
ToplevelUpdate::Remove(toplevel.clone()),
)));
}
}
} }
fn toplevel_closed( fn toplevel_closed(
@ -412,9 +413,7 @@ impl ToplevelInfoHandler for AppData {
_qh: &QueueHandle<Self>, _qh: &QueueHandle<Self>,
toplevel: &ExtForeignToplevelHandleV1, toplevel: &ExtForeignToplevelHandleV1,
) { ) {
let _ = futures::executor::block_on(self.tx.send(WaylandUpdate::Toplevel( self.remove_toplevel(toplevel);
ToplevelUpdate::Remove(toplevel.clone()),
)));
} }
} }
@ -482,6 +481,7 @@ pub(crate) fn wayland_handler(
toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh), toplevel_manager_state: ToplevelManagerState::new(&registry_state, &qh),
queue_handle: qh, queue_handle: qh,
registry_state, registry_state,
captured_toplevels: std::collections::HashSet::new(),
}; };
loop { loop {

View file

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

View file

@ -0,0 +1,8 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy context="default">
<allow own_prefix="com.system76.CosmicSettings.Applet"/>
</policy>
</busconfig>

View file

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

View file

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

View file

@ -1,16 +1,16 @@
network = Hálózat network = Hálózat
airplane-mode = Repülőgép-üzemmód airplane-mode = Repülőgép-üzemmód
airplane-mode-on = A repülőgép-üzemmód be van kapcsolva airplane-mode-on = A repülőgép-üzemmód be van kapcsolva
turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil szélessáv engedélyezéséhez. turn-off-airplane-mode = Kapcsold ki a Wi-Fi, Bluetooth és mobil széles sáv engedélyezéséhez.
wifi = Wi-Fi wifi = Wi-Fi
identity = Azonosító identity = Azonosító
ipv4 = IPv4-cím ipv4 = IPv4-cím
ipv6 = IPv6-cím ipv6 = IPv6-cím
mac = MAC mac = MAC
megabits-per-second = Mbps megabits-per-second = Mbps
connected = Csatlakoztatva connected = Kapcsolódva
connecting = Csatlakozás… connecting = Kapcsolódás
connect = Csatlakozás connect = Kapcsolódás
cancel = Mégse cancel = Mégse
settings = Hálózati beállítások… settings = Hálózati beállítások…
visible-wireless-networks = Látható vezeték nélküli hálózatok visible-wireless-networks = Látható vezeték nélküli hálózatok

View file

@ -0,0 +1,24 @@
cancel = Têk bibe
connect = Girê bide
connected = Hat girêdan
network = Tor
airplane-mode = Awaya balafirê
airplane-mode-on = Awaya balafirê vekirî ye
turn-off-airplane-mode = Ji bo çalakirina Wi-Fi, Bluetooth û torên mobayîl, awaya balafirê bigire.
wifi = Wi-Fi
identity = Nasname
ipv4 = IPv4
ipv6 = IPv6
mac = MAC
megabits-per-second = Mbps
gigabits-per-second = Gbps
terabits-per-second = Tbps
connecting = Tê girêdan
settings = Sazkariyên torê...
visible-wireless-networks = Torên bêtêl ên xuya
vpn-connections = Girêdanên VPN
enter-password = Borînpeyvê yan jî kilîtê şîfrekirinê têxîne
router-wps-button = Tu dikarî bi pêlkirina bişkoka "WPS" ya li ser routerê jî girê bidî
unable-to-connect = Nikare bi torê ve were girêdan
check-wifi-connection = Piştrast bike ku Wi-Fi bi înternetê ve girêdayî ye û borînpeyv rast e
reset = Ji nû ve saz bike

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "cosmic-applet-notifications" name = "cosmic-applet-notifications"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
@ -8,7 +8,7 @@ license = "GPL-3.0-only"
cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" } cosmic-notifications-util = { git = "https://github.com/pop-os/cosmic-notifications" }
cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" } cosmic-notifications-config = { git = "https://github.com/pop-os/cosmic-notifications" }
anyhow.workspace = true anyhow.workspace = true
libcosmic.workspace = true cosmic.workspace = true
tokio.workspace = true tokio.workspace = true
# cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" } # cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" }
# cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" } # cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" }

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

@ -1,14 +1,14 @@
hours-ago = hours-ago =
{ $duration -> { $duration ->
[0] Most [0] Épp most
[one] 1 órával ezelőtt [one] 1 órája
*[other] { $duration } órával ezelőtt *[other] { $duration } órája
} }
minutes-ago = minutes-ago =
{ $duration -> { $duration ->
[0] Most [0] Épp most
[one] 1 perccel ezelőtt [one] 1 perce
*[other] { $duration } perccel ezelőtt *[other] { $duration } perce
} }
show-less = Kevesebb megjelenítése show-less = Kevesebb megjelenítése
show-more = { $more } további megjelenítése show-more = { $more } további megjelenítése

View file

@ -0,0 +1,19 @@
hours-ago =
{ $duration ->
[0] Niha
[one] 1 demjimêr berê
*[other] { $duration } demjimêr berê
}
minutes-ago =
{ $duration ->
[0] Niha
[one] 1 xulek berê
*[other] { $duration } xulek berê
}
show-less = Kêmtir nîşan bide
show-more = Bêtir { $more } nîşan bide
clear-group = Komê pak bike
clear-all = Hemû agahdariyan pak bike
do-not-disturb = Rawestandina balkêşiyê
notification-settings = Sazkariyên agahdariyê....
no-notifications = Agahdarî tune ne

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

@ -1,13 +1,13 @@
[package] [package]
name = "cosmic-applet-power" name = "cosmic-applet-power"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
[dependencies] [dependencies]
i18n-embed-fl.workspace = true i18n-embed-fl.workspace = true
i18n-embed.workspace = true i18n-embed.workspace = true
libcosmic.workspace = true cosmic.workspace = true
logind-zbus = "5.3.2" logind-zbus = "5.3.2"
rust-embed.workspace = true rust-embed.workspace = true
rustix.workspace = true rustix.workspace = true

View file

@ -0,0 +1,4 @@
cancel = Cancel·lar
confirm = Confirmar
restart = Reinicia
suspend = Suspèn

View file

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

View file

@ -33,4 +33,4 @@ confirm-body =
[lock-screen] zárolni fogja a képernyőt [lock-screen] zárolni fogja a képernyőt
[log-out] kijelentkezik [log-out] kijelentkezik
*[other] alkalmazni fogja a kiválasztott műveletet *[other] alkalmazni fogja a kiválasztott műveletet
} { $countdown } másodperc múlva } { $countdown } másodperc múlva.

View file

@ -0,0 +1,36 @@
cancel = Têk bibe
confirm = Bipejirîne
power = Hêz
restart = Ji nû ve bide destpêkirin
settings = Sazkarî...
shutdown = Vemrîne
suspend = Rawestîne
log-out = Derkeve
lock-screen = Dîmenderê kilît bike
lock-screen-shortcut = Super + Escape
log-out-shortcut = Super + Shift + Escape
confirm-button =
{ $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] Vemrîne
[log-out] { log-out }
*[other] { confirm }
}
confirm-title =
{ $action ->
[restart] { restart }
[suspend] { suspend }
[shutdown] { shutdown }
[log-out] Hemû sepanan bigire û derkeve
*[other] Çalakiyê hilbijartî bisepîne
} niha?
confirm-body =
Pergal wê xweber were { $action ->
[restart] jinûvedestpêkkirin
[suspend] rawestandin
[shutdown] vemirandin
[lock-screen] kilîtkirin
[log-out] derketin
*[other] çalakiyê hilbijartî bisepîne
}di { $countdown } çirke de.

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

View file

@ -1,12 +1,12 @@
[package] [package]
name = "cosmic-applet-status-area" name = "cosmic-applet-status-area"
version = "1.0.2" version = "1.3.0"
edition = "2024" edition = "2024"
license = "GPL-3.0-only" license = "GPL-3.0-only"
[dependencies] [dependencies]
futures.workspace = true futures.workspace = true
libcosmic.workspace = true cosmic.workspace = true
serde.workspace = true serde.workspace = true
rustc-hash.workspace = true rustc-hash.workspace = true
tokio.workspace = true tokio.workspace = true

View file

@ -9,11 +9,14 @@ use cosmic::{
}, },
cctk::sctk::reexports::calloop, cctk::sctk::reexports::calloop,
iced::{ iced::{
self, Length, Subscription, self,
Event::Mouse,
Length, Subscription, event,
mouse::{self, ScrollDelta},
platform_specific::shell::commands::popup::{destroy_popup, get_popup}, platform_specific::shell::commands::popup::{destroy_popup, get_popup},
theme::Style,
window, window,
}, },
scroll::{DiscreteScrollDelta, DiscreteScrollState},
surface, surface,
widget::{container, mouse_area}, widget::{container, mouse_area},
}; };
@ -25,6 +28,7 @@ use crate::{
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum Msg { pub enum Msg {
None, None,
Activate(usize), Activate(usize),
@ -33,7 +37,8 @@ pub enum Msg {
StatusMenu((usize, status_menu::Msg)), StatusMenu((usize, status_menu::Msg)),
StatusNotifier(status_notifier_watcher::Event), StatusNotifier(status_notifier_watcher::Event),
TogglePopup(usize), TogglePopup(usize),
Hovered(usize), Hovered(Option<usize>),
WheelScrolled(ScrollDelta),
Surface(surface::Action), Surface(surface::Action),
ToggleOverflow, ToggleOverflow,
HoveredOverflow, HoveredOverflow,
@ -49,6 +54,8 @@ pub(crate) struct App {
max_menu_id: usize, max_menu_id: usize,
popup: Option<window::Id>, popup: Option<window::Id>,
overflow_popup: Option<window::Id>, overflow_popup: Option<window::Id>,
hovered_menu: Option<usize>,
scroll_states: BTreeMap<usize, DiscreteScrollState>,
token_tx: Option<calloop::channel::Sender<TokenRequest>>, token_tx: Option<calloop::channel::Sender<TokenRequest>>,
} }
@ -103,9 +110,10 @@ impl App {
let overflow_index = self.overflow_index().unwrap_or(0); let overflow_index = self.overflow_index().unwrap_or(0);
let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| { let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| {
mouse_area( mouse_area(
menu_icon_button(&self.core.applet, &menu).on_press_down(Msg::TogglePopup(*id)), menu_icon_button(&self.core.applet, menu).on_press_down(Msg::TogglePopup(*id)),
) )
.on_enter(Msg::Hovered(*id)) .on_enter(Msg::Hovered(Some(*id)))
.on_exit(Msg::Hovered(None))
.into() .into()
}); });
@ -210,6 +218,7 @@ impl cosmic::Application for App {
{ {
let id = *id; let id = *id;
self.menus.remove(&id); self.menus.remove(&id);
self.scroll_states.remove(&id);
if self.open_menu == Some(id) { if self.open_menu == Some(id) {
self.open_menu = None; self.open_menu = None;
if let Some(popup_id) = self.popup { if let Some(popup_id) = self.popup {
@ -312,6 +321,10 @@ impl cosmic::Application for App {
} }
}, },
Msg::Hovered(id) => { Msg::Hovered(id) => {
self.hovered_menu = id;
let Some(id) = id else {
return Task::none();
};
let mut cmds = Vec::new(); let mut cmds = Vec::new();
if let Some(old_id) = self.open_menu.take() { if let Some(old_id) = self.open_menu.take() {
if old_id != id { if old_id != id {
@ -361,6 +374,19 @@ impl cosmic::Application for App {
cmds.push(get_popup(popup_settings)); cmds.push(get_popup(popup_settings));
Task::batch(cmds) Task::batch(cmds)
} }
Msg::WheelScrolled(delta) => {
let Some(id) = self.hovered_menu else {
return Task::none();
};
let discrete_delta = self.scroll_states.entry(id).or_default().update(delta);
let Some((delta, orientation)) = discrete_scroll_delta(discrete_delta) else {
return Task::none();
};
let Some(menu) = self.menus.get(&id) else {
return Task::none();
};
scroll(id, menu.item.item_proxy().clone(), delta, orientation)
}
Msg::Surface(a) => { Msg::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic( return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a), cosmic::app::Action::Surface(a),
@ -405,6 +431,7 @@ impl cosmic::Application for App {
} }
} }
Msg::HoveredOverflow => { Msg::HoveredOverflow => {
self.hovered_menu = None;
let mut cmds = Vec::new(); let mut cmds = Vec::new();
if self.overflow_popup.is_some() { if self.overflow_popup.is_some() {
// If we already have an overflow popup, do nothing // If we already have an overflow popup, do nothing
@ -460,6 +487,12 @@ impl cosmic::Application for App {
subscriptions.push(menu.subscription(is_open).with(*id).map(Msg::StatusMenu)); subscriptions.push(menu.subscription(is_open).with(*id).map(Msg::StatusMenu));
} }
subscriptions.push(activation_token_subscription(0).map(Msg::Token)); subscriptions.push(activation_token_subscription(0).map(Msg::Token));
subscriptions.push(event::listen_with(|e, status, _| match (e, status) {
(Mouse(mouse::Event::WheelScrolled { delta }), event::Status::Ignored) => {
Some(Msg::WheelScrolled(delta))
}
_ => None,
}));
iced::Subscription::batch(subscriptions) iced::Subscription::batch(subscriptions)
} }
@ -472,9 +505,10 @@ impl cosmic::Application for App {
.iter() .iter()
.take(overflow_index.unwrap_or(self.menus.len())) .take(overflow_index.unwrap_or(self.menus.len()))
.map(|(id, menu)| { .map(|(id, menu)| {
mouse_area(menu_icon_button(&self.core.applet, &menu).on_press(Msg::Activate(*id))) mouse_area(menu_icon_button(&self.core.applet, menu).on_press(Msg::Activate(*id)))
.on_right_press(Msg::TogglePopup(*id)) .on_right_press(Msg::TogglePopup(*id))
.on_enter(Msg::Hovered(*id)) .on_enter(Msg::Hovered(Some(*id)))
.on_exit(Msg::Hovered(None))
.into() .into()
}); });
@ -570,6 +604,33 @@ fn activate(
}) })
} }
fn scroll(
id: usize,
item_proxy: crate::subscriptions::status_notifier_item::StatusNotifierItemProxy<'static>,
delta: i32,
orientation: &'static str,
) -> Task<cosmic::Action<Msg>> {
Task::future(async move {
match item_proxy.scroll(delta, orientation).await {
Ok(_) => cosmic::action::app(Msg::None),
Err(err) => {
tracing::error!("Scroll failed for {}: {}", id, err);
cosmic::action::app(Msg::None)
}
}
})
}
fn discrete_scroll_delta(delta: DiscreteScrollDelta) -> Option<(i32, &'static str)> {
if delta.y != 0 {
Some((delta.y as i32, "vertical"))
} else if delta.x != 0 {
Some((delta.x as i32, "horizontal"))
} else {
None
}
}
fn menu_icon_button<'a>( fn menu_icon_button<'a>(
applet: &'a cosmic::applet::Context, applet: &'a cosmic::applet::Context,
menu: &'a status_menu::State, menu: &'a status_menu::State,
@ -577,7 +638,7 @@ fn menu_icon_button<'a>(
let icon = menu.icon_handle().clone(); let icon = menu.icon_handle().clone();
let theme = cosmic::theme::active(); let theme = cosmic::theme::active();
let theme = theme.cosmic(); let _theme = theme.cosmic();
let suggested = applet.suggested_size(true); let suggested = applet.suggested_size(true);
let padding = applet.suggested_padding(true).1; let padding = applet.suggested_padding(true).1;
@ -595,7 +656,7 @@ fn menu_icon_button<'a>(
.class(if symbolic { .class(if symbolic {
cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| { cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| {
cosmic::iced::widget::svg::Style { cosmic::iced::widget::svg::Style {
color: Some(theme.cosmic().background.on.into()), color: Some(theme.cosmic().background(false).on.into()),
} }
})) }))
} else { } else {
@ -614,3 +675,43 @@ fn menu_icon_button<'a>(
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
cosmic::applet::run::<App>(()) cosmic::applet::run::<App>(())
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discrete_scroll_prefers_vertical_delta() {
assert_eq!(
discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 4, y: -2 }),
Some((-2, "vertical"))
);
}
#[test]
fn discrete_scroll_uses_horizontal_delta_when_vertical_is_zero() {
assert_eq!(
discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 3, y: 0 }),
Some((3, "horizontal"))
);
}
#[test]
fn discrete_scroll_ignores_zero_delta() {
assert_eq!(
discrete_scroll_delta(cosmic::scroll::DiscreteScrollDelta { x: 0, y: 0 }),
None
);
}
#[test]
fn pixel_scroll_accumulates_before_emitting_discrete_delta() {
let mut state = cosmic::scroll::DiscreteScrollState::default();
let first = state.update(ScrollDelta::Pixels { x: 0.0, y: 12.0 });
assert_eq!(discrete_scroll_delta(first), None);
let second = state.update(ScrollDelta::Pixels { x: 0.0, y: 12.0 });
assert_eq!(discrete_scroll_delta(second), Some((1, "vertical")));
}
}

View file

@ -26,6 +26,7 @@ pub struct State {
expanded: Option<i32>, expanded: Option<i32>,
// TODO handle icon with multiple sizes? // TODO handle icon with multiple sizes?
icon_handle: icon::Handle, icon_handle: icon::Handle,
icon_theme_path: Option<PathBuf>,
click_event: Option<(i32, bool)>, click_event: Option<(i32, bool)>,
} }
@ -39,6 +40,7 @@ impl State {
icon_handle: icon::from_name("application-default") icon_handle: icon::from_name("application-default")
.prefer_svg(true) .prefer_svg(true)
.handle(), .handle(),
icon_theme_path: None,
click_event: None, click_event: None,
}, },
iced::Task::none(), iced::Task::none(),
@ -63,6 +65,7 @@ impl State {
} }
Msg::Icon(update) => { Msg::Icon(update) => {
let icon_name = update.name.unwrap_or_default(); let icon_name = update.name.unwrap_or_default();
self.icon_theme_path = update.theme_path;
// Use the icon pixmap if an icon was not defined by name. // Use the icon pixmap if an icon was not defined by name.
if icon_name.is_empty() { if icon_name.is_empty() {
@ -93,13 +96,18 @@ impl State {
self.icon_handle = if Path::new(&icon_name).exists() { self.icon_handle = if Path::new(&icon_name).exists() {
icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true) icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true)
} else { } else {
icon::from_name(icon_name) let mut builder = icon::from_name(icon_name).prefer_svg(true).fallback(Some(
.prefer_svg(true) IconFallback::Names(vec![
.fallback(Some(IconFallback::Names(vec![
"application-default".into(), "application-default".into(),
"application-x-executable".into(), "application-x-executable".into(),
]))) ]),
.handle() ));
if let Some(ref theme_path) = self.icon_theme_path {
builder = builder.with_extra_paths(vec![theme_path.clone()]);
}
builder.handle()
}; };
iced::Task::none() iced::Task::none()

View file

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

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use std::hash::Hash; use std::hash::Hash;
use std::path::PathBuf;
use cosmic::iced::{self, Subscription}; use cosmic::iced::{self, Subscription};
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
@ -27,7 +28,7 @@ pub struct Icon {
pub struct IconUpdate { pub struct IconUpdate {
pub name: Option<String>, pub name: Option<String>,
pub pixmap: Option<Vec<Icon>>, pub pixmap: Option<Vec<Icon>>,
// pub theme_path: Option<PathBuf>, pub theme_path: Option<PathBuf>,
} }
impl StatusNotifierItem { impl StatusNotifierItem {
@ -118,11 +119,11 @@ impl StatusNotifierItem {
async fn icon_events(item_proxy: StatusNotifierItemProxy<'static>) -> IconUpdate { async fn icon_events(item_proxy: StatusNotifierItemProxy<'static>) -> IconUpdate {
let icon_name = item_proxy.icon_name().await; let icon_name = item_proxy.icon_name().await;
let icon_pixmap = item_proxy.icon_pixmap().await; let icon_pixmap = item_proxy.icon_pixmap().await;
// let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from); let icon_theme_path = item_proxy.icon_theme_path().await.map(PathBuf::from);
IconUpdate { IconUpdate {
name: icon_name.ok(), name: icon_name.ok(),
pixmap: icon_pixmap.ok(), pixmap: icon_pixmap.ok(),
// theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()), theme_path: icon_theme_path.ok().filter(|x| !x.as_os_str().is_empty()),
} }
} }
@ -201,6 +202,8 @@ pub trait StatusNotifierItem {
fn activate(&self, x: i32, y: i32) -> zbus::Result<()>; fn activate(&self, x: i32, y: i32) -> zbus::Result<()>;
fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>; fn secondary_activate(&self, x: i32, y: i32) -> zbus::Result<()>;
fn scroll(&self, delta: i32, orientation: &str) -> zbus::Result<()>;
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View file

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

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

@ -3,11 +3,11 @@ tile-current = Jelenlegi munkaterület csempézése
shortcuts = Gyorsbillentyűk shortcuts = Gyorsbillentyűk
navigate-windows = Ablakok navigálása navigate-windows = Ablakok navigálása
move-window = Ablak mozgatása move-window = Ablak mozgatása
toggle-floating-window = Az ablakok lebegtetésének be- és kikapcsolása toggle-floating-window = Lebegő ablak be/ki
view-all-shortcuts = Az összes gyorsbillentyű megtekintése… view-all-shortcuts = Az összes gyorsbillentyű megtekintése…
active-hint = Aktív ablak kiemelése active-hint = Aktív ablak kiemelése
gaps = Hézagok gaps = Hézagok
floating-window-exceptions = Lebegő ablak kivételek… floating-window-exceptions = Lebegőablak-kivételek…
window-management-settings = Ablakkezelési beállítások… window-management-settings = Ablakkezelési beállítások…
all-workspaces = Összes munkaterület all-workspaces = Összes munkaterület
per-workspace = Munkaterületenként per-workspace = Munkaterületenként

View file

@ -0,0 +1,20 @@
tile-windows = Çarçoveyan xweber berhev bike
tile-current = Qada xebatê heyî berhev bike
shortcuts = Kurterê
navigate-windows = Di nav çarçoveyan de bigere
move-window = Çarçoveyê bilivîne
toggle-floating-window = Rewşê çarçoveyê herikbar biguhêrîne
view-all-shortcuts = Hemû kurteriyan nîşan bide...
active-hint = Pêşniyara çalak
gaps = Valahî
floating-window-exceptions = Awarteyên çarçoveyê herikbar...
window-management-settings = Sazkariyên rêveberiya çarçoveyê...
all-workspaces = Hemû qadên xebatê
per-workspace = Li gorî qada xebatê
super = Super
shift = Shift
arrow-keys = tîr
tiled = Berhevkirî
floating = Herikbar
autotile-behavior = Li ser qadên xebatê çarçoveyan berhev bike
new-workspace = Tevgera qada xebatê ya nû

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

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