Compare commits

...

40 commits

Author SHA1 Message Date
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
81 changed files with 3166 additions and 992 deletions

792
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -6,3 +6,14 @@ new-window = Nouvelle fenêtre
run = Exécuter
run-on = Lancer avec { $gpu }
run-on-default = (Défaut)
edit-launcher = Modifier le lanceur
launcher-name = Nom
launcher-command = Commande
launcher-icon = Icône
launcher-icon-theme = Thème
launcher-icon-search = Rechercher une icône
launcher-icon-catalog-loading = Chargement des icônes
launcher-icon-catalog-empty = Aucune icône
launcher-icons = icônes
save = Enregistrer
cancel = Annuler

View file

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

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
mod app;
mod icon_catalog;
mod launcher_edit;
mod localize;
mod wayland_handler;
mod wayland_subscription;

View file

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

View file

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

View file

@ -2,5 +2,5 @@ screen-reader = スクリーンリーダー
magnifier = 拡大鏡
settings = アクセシビリティ設定…
invert-colors = 色を反転
filter-colors = 色のフィルター
filter-colors = カラーフィルター
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

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

View file

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

View file

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

View file

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

@ -2,12 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only
mod localize;
mod model;
mod mouse_area;
use crate::localize::localize;
use config::{AudioAppletConfig, amplification_sink, amplification_source};
use cosmic::{
Element, Renderer, Task, Theme, app,
Apply, Element, Renderer, Task, Theme, app,
applet::{
column as applet_column,
cosmic_panel_config::PanelAnchor,
@ -18,18 +19,24 @@ use cosmic::{
cosmic_config::CosmicConfigEntry,
cosmic_theme::Spacing,
iced::{
self, Alignment, Length, Subscription,
self, Alignment, Length, Rectangle, Subscription,
futures::StreamExt,
widget::{self, column, row, slider},
window,
},
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 mpris_subscription::{MprisRequest, MprisUpdate};
use mpris2_zbus::player::PlaybackStatus;
use std::{cell::RefCell, rc::Rc, sync::Arc};
mod config;
mod mpris_subscription;
@ -50,8 +57,10 @@ pub struct Audio {
core: cosmic::app::Core,
/// Track the applet's popup window.
popup: Option<window::Id>,
/// The model from cosmic-settings for managing pipewire devices.
model: css::Model,
/// Varlink connection to `com.system76.CosmicSettings.Audio`.
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.
is_open: IsOpen,
/// 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>,
/// Used to request an activation token for opening cosmic-settings.
token_tx: Option<calloop::channel::Sender<TokenRequest>>,
rectangle_tracker: Option<RectangleTracker<u32>>,
rectangle: Option<iced::Rectangle>,
}
impl Audio {
fn output_icon_name(&self) -> &'static str {
let volume = self.model.sink_volume;
let mute = self.model.sink_mute;
let volume = self.model.active_sink.volume;
let mute = self.model.active_sink.mute;
if mute || volume == 0 {
"audio-volume-muted-symbolic"
} else if volume < 33 {
@ -88,8 +99,8 @@ impl Audio {
}
fn input_icon_name(&self) -> &'static str {
let volume = self.model.source_volume;
let mute = self.model.source_mute;
let volume = self.model.active_source.volume;
let mute = self.model.active_source.mute;
if mute || volume == 0 {
"microphone-sensitivity-muted-symbolic"
} else if volume < 33 {
@ -110,8 +121,10 @@ enum IsOpen {
Input,
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub enum Message {
/// Connection to `com.system76.CosmicSettings`.
Client(Arc<audio_client::Client>),
Ignore,
SetSinkVolume(u32),
SetSourceVolume(u32),
@ -129,8 +142,9 @@ pub enum Message {
MprisRequest(MprisRequest),
Token(TokenUpdate),
OpenSettings,
Subscription(css::Message),
Subscription(audio_client::Event),
Surface(surface::Action),
Rectangle(RectangleUpdate<u32>),
}
// TODO
@ -242,15 +256,9 @@ impl cosmic::Application for Audio {
const APP_ID: &'static str = "com.system76.CosmicAppletAudio";
fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
let mut model = css::Model::default();
model.unplugged_text = "Unplugged".into();
model.hd_audio_text = "HD Audio".into();
model.usb_audio_text = "USB Audio".into();
(
Self {
core,
model,
..Default::default()
},
Task::none(),
@ -271,6 +279,14 @@ impl cosmic::Application for Audio {
fn update(&mut self, message: Message) -> app::Task<Message> {
match message {
Message::Rectangle(u) => match u {
RectangleUpdate::Rectangle(r) => {
self.rectangle = Some(r.1);
}
RectangleUpdate::Init(tracker) => {
self.rectangle_tracker.replace(tracker);
}
},
Message::Ignore => {}
Message::TogglePopup => {
if let Some(p) = self.popup.take() {
@ -291,14 +307,21 @@ impl cosmic::Application for Audio {
(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(),
new_id,
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);
}
}
@ -318,52 +341,74 @@ impl cosmic::Application for Audio {
}
}
Message::Subscription(message) => {
return self
.model
.update(message)
.map(|message| Message::Subscription(message).into());
self.model.update(message);
}
Message::SetDefaultSink(pos) => {
return self
.model
.set_default_sink(pos)
.map(|message| Message::Subscription(message).into());
if let Some(&pos) = self.model.sinks.sorted_index.get(pos)
&& let Some(&node_id) = self.model.sinks.id.get(pos as usize)
&& let Some(client) = self.audio_client.as_mut()
{
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_default(node_id, true).await;
});
}
}
Message::SetDefaultSource(pos) => {
return self
.model
.set_default_source(pos)
.map(|message| Message::Subscription(message).into());
if let Some(&pos) = self.model.sources.sorted_index.get(pos)
&& let Some(&node_id) = self.model.sources.id.get(pos as usize)
&& let Some(client) = self.audio_client.as_mut()
{
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) => {
return self
.model
.set_sink_volume(volume)
.map(|message| Message::Subscription(message).into());
if let Some(ref mut client) = self.audio_client {
self.model.active_sink.volume = volume;
self.model.active_sink.volume_text = volume.to_string();
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_sink_volume(volume).await;
});
}
}
Message::SetSourceVolume(volume) => {
return self
.model
.set_source_volume(volume)
.map(|message| Message::Subscription(message).into());
if let Some(ref mut client) = self.audio_client {
self.model.active_source.volume = volume;
self.model.active_source.volume_text = volume.to_string();
futures::executor::block_on(async {
_ = client.borrow_mut().conn.set_source_volume(volume).await;
});
}
}
Message::ToggleMediaControlsInTopPanel(enabled) => {
self.config.show_media_controls_in_top_panel = enabled;
if let Ok(helper) =
cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
&& let Err(err) = self.config.write_entry(&helper)
{
if let Err(err) = self.config.write_entry(&helper) {
tracing::error!(?err, "Error writing config");
}
tracing::error!(?err, "Error writing config");
}
}
Message::CloseRequested(id) => {
@ -461,6 +506,12 @@ impl cosmic::Application for Audio {
cosmic::app::Action::Surface(a),
));
}
Message::Client(client) => {
if let Some(client) = Arc::into_inner(client) {
self.audio_client = Some(Rc::new(RefCell::new(client)));
self.model = model::Model::default();
}
}
}
Task::none()
@ -476,7 +527,50 @@ impl cosmic::Application for Audio {
}),
mpris_subscription::mpris_subscription(0).map(Message::Mpris),
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())
.on_press_down(Message::TogglePopup);
const WHEEL_STEP: f32 = 5.0; // 5% per wheel event
const WHEEL_STEP: f32 = 5.0; // 5% par cran logique
// Wayland axis_v120 envoie un cran physique en rafale de plusieurs
// ScrollDelta::Pixels (58 events ~1520px), pour ~120px par cran. On
// accumule ces sub-events dans un thread_local et on n'émet qu'au
// passage d'un seuil — sinon `signum()` faisait croire que chaque
// sub-event = un cran, et un seul cran physique faisait chuter le
// volume jusqu'à 0 ("coupe le son").
const PIXEL_THRESHOLD: f32 = 15.0; // px par cran logique
std::thread_local! {
static PIXEL_ACC: std::cell::Cell<f32> = const { std::cell::Cell::new(0.0) };
}
let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
let scroll_vector = match delta {
iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1
iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(), // -1/0/1
iced::mouse::ScrollDelta::Lines { y, .. } => {
PIXEL_ACC.with(|a| a.set(0.0));
y * WHEEL_STEP
}
iced::mouse::ScrollDelta::Pixels { y, .. } => {
PIXEL_ACC.with(|acc_cell| {
let acc = acc_cell.get() + y;
let steps = (acc / PIXEL_THRESHOLD).trunc();
acc_cell.set(acc - steps * PIXEL_THRESHOLD);
steps * WHEEL_STEP
})
}
};
if scroll_vector == 0.0 {
return Message::Ignore;
}
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);
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| {
// if we have a configure for width and height, we're in a overflow popup
c.width > 0. && c.height > 0.
}))
.then(|| self.playback_buttons());
self.core
.applet
.autosize_window(
if let Some(playback_buttons) = playback_buttons
&& !playback_buttons.is_empty()
{
match self.core.applet.anchor {
PanelAnchor::Left | PanelAnchor::Right => Element::from(
applet_column::Column::with_children(playback_buttons)
.push(btn)
.align_x(Alignment::Center)
.height(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
),
let mut ret = if let Some(playback_buttons) = playback_buttons
&& !playback_buttons.is_empty()
{
has_playback_buttons = true;
Element::from(match self.core.applet.anchor {
PanelAnchor::Left | PanelAnchor::Right => Element::from(
applet_column::Column::with_children(playback_buttons)
.push(btn)
.align_x(Alignment::Center)
.height(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
),
PanelAnchor::Top | PanelAnchor::Bottom => {
applet_row::Row::with_children(playback_buttons)
.push(btn)
.align_y(Alignment::Center)
.width(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
)
.into()
}
}
} else {
btn.into()
},
)
.into()
),
PanelAnchor::Top | PanelAnchor::Bottom => {
applet_row::Row::with_children(playback_buttons)
.push(btn)
.align_y(Alignment::Center)
.width(Length::Shrink)
// TODO configurable variable from the panel?
.spacing(
-(self.core.applet.suggested_padding(true).0 as f32)
* self.core.applet.padding_overlap,
)
.into()
}
})
} else {
btn.into()
};
if let Some(tracker) = self.rectangle_tracker.as_ref()
&& has_playback_buttons
{
ret = tracker.container(0, ret).into()
}
if !self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
// if we have a configure for width and height, we're in a overflow popup
c.width > 0. && c.height > 0.
}) {
ret = self.core.applet.autosize_window(ret).into();
}
ret
}
fn view_window(&self, _id: window::Id) -> Element<'_, Message> {
@ -551,19 +676,21 @@ impl cosmic::Application for Audio {
space_xxs, space_s, ..
} = theme::active().cosmic().spacing;
let sink = self
let sink: Option<&str> = self
.model
.active_sink()
.and_then(|pos| self.model.sinks().get(pos));
let source = self
.sinks
.active()
.map(|pos| self.model.sinks.sorted_display[pos].as_ref());
let source: Option<&str> = self
.model
.active_source()
.and_then(|pos| self.model.sources().get(pos));
.sources
.active()
.map(|pos| self.model.sources.sorted_display[pos].as_ref());
let mut audio_content = {
let output_slider = slider(
0..=self.max_sink_volume,
self.model.sink_volume,
self.model.active_sink.volume,
Message::SetSinkVolume,
)
.width(Length::FillPortion(5))
@ -571,7 +698,7 @@ impl cosmic::Application for Audio {
let input_slider = slider(
0..=self.max_source_volume,
self.model.source_volume,
self.model.active_source.volume,
Message::SetSourceVolume,
)
.width(Length::FillPortion(5))
@ -590,7 +717,7 @@ impl cosmic::Application for Audio {
.line_height(24)
.on_press(Message::ToggleSinkMute),
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))
.align_x(Alignment::End)
]
@ -609,7 +736,7 @@ impl cosmic::Application for Audio {
.line_height(24)
.on_press(Message::ToggleSourceMute),
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))
.align_x(Alignment::End)
]
@ -624,7 +751,7 @@ impl cosmic::Application for Audio {
Some(sink) => sink.to_owned(),
None => fl!("no-device"),
},
self.model.sinks(),
&self.model.sinks.sorted_display,
Message::OutputToggle,
Message::SetDefaultSink,
),
@ -635,7 +762,7 @@ impl cosmic::Application for Audio {
Some(source) => source.to_owned(),
None => fl!("no-device"),
},
self.model.sources(),
&self.model.sources.sorted_display,
Message::InputToggle,
Message::SetDefaultSource,
)
@ -691,19 +818,15 @@ impl cosmic::Application for Audio {
}
if let Some(play) = self.is_play() {
control_elements.push(
button::icon(
icon::from_name(if play { PLAY } else { PAUSE })
.size(32)
.symbolic(true),
)
.extra_small()
.class(cosmic::theme::Button::AppletIcon)
.on_press(if play {
Message::MprisRequest(MprisRequest::Play)
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true))
.extra_small()
.class(cosmic::theme::Button::AppletIcon)
.on_press(if play {
Message::MprisRequest(MprisRequest::Play)
} else {
Message::MprisRequest(MprisRequest::Pause)
})
.into(),
);
}
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,
title: String,
selected: String,
devices: &[String],
devices: &'a [Arc<str>],
toggle: Message,
mut change: impl FnMut(usize) -> Message + 'static,
) -> widget::Column<'static, Message, crate::Theme, Renderer> {
) -> widget::Column<'a, Message, crate::Theme, Renderer> {
if open {
devices.iter().cloned().enumerate().fold(
devices.iter().enumerate().fold(
column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
|col, (id, name)| {
move |col, (id, name)| {
col.push(
menu_button(text::body(name))
text::body(name.as_ref())
.apply(menu_button)
.on_press(change(id))
.width(Length::Fill)
.padding([8, 48]),
@ -785,9 +909,9 @@ fn revealer_head(
selected: String,
toggle: Message,
) -> cosmic::widget::Button<'static, Message> {
menu_button(column![
text::body(title).width(Length::Fill),
text::caption(selected),
])
.on_press(toggle)
cosmic::widget::column::with_capacity(2)
.push(text::body(title).width(Length::Fill))
.push(text::caption(selected))
.apply(menu_button)
.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::{
Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Widget,
event::{self, Event},
event::Event,
layout, mouse, overlay, renderer, touch,
widget::{Operation, Tree, tree},
};

View file

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

View file

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

View file

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

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

View file

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

View file

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

@ -21,10 +21,10 @@ use cosmic::{
widget::{Column, column, container, row},
},
theme,
widget::{button, divider, icon, scrollable, text},
widget::{button, divider, icon, indeterminate_circular, scrollable, text},
};
use futures::FutureExt;
use std::{collections::HashMap, sync::LazyLock, time::Duration};
use std::{collections::HashMap, time::Duration};
use tokio::sync::mpsc::Sender;
use crate::{
@ -63,6 +63,7 @@ impl CosmicBluetoothApplet {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum Message {
TogglePopup,
CloseRequested(window::Id),
@ -145,7 +146,7 @@ impl cosmic::Application for CosmicBluetoothApplet {
}
Message::BluetoothEvent(e) => match e {
BluerEvent::RequestResponse {
req,
req: _,
state,
err_msg,
} => {
@ -367,14 +368,12 @@ impl cosmic::Application for CosmicBluetoothApplet {
);
}
BluerDeviceStatus::Paired => {}
BluerDeviceStatus::Connecting | BluerDeviceStatus::Disconnecting => {
row = row.push(
icon::from_name("process-working-symbolic")
.size(24)
.symbolic(true),
);
BluerDeviceStatus::Connecting
| BluerDeviceStatus::Disconnecting
| BluerDeviceStatus::Pairing => {
row = row.push(indeterminate_circular().size(24.0));
}
BluerDeviceStatus::Disconnected | BluerDeviceStatus::Pairing => continue,
BluerDeviceStatus::Disconnected => continue,
}
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),
_ => return,
};
_ = output.send(message).await;
@ -199,6 +197,7 @@ pub enum BluerRequest {
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum BluerEvent {
RequestResponse {
req: BluerRequest,
@ -568,22 +567,21 @@ impl BluerSessionState {
tick(&mut interval).await;
let new_status = adapter_clone.is_powered().await.unwrap_or_default();
devices = build_device_list(devices, &adapter_clone).await;
if new_status != status {
status = new_status;
let state = BluerState {
devices: devices.clone(),
bluetooth_enabled: status,
};
if state.bluetooth_enabled {
for d in &state.devices {
if d.paired_and_trusted() {
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
}
let power_changed = new_status != status;
status = new_status;
let state = BluerState {
devices: devices.clone(),
bluetooth_enabled: status,
};
if power_changed && state.bluetooth_enabled {
for d in &state.devices {
if d.paired_and_trusted() {
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).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
.send(BluerSessionEvent::ChangesProcessed(BluerState {
devices: devices.clone(),
@ -853,13 +854,7 @@ async fn bluer_state(adapter: &Adapter) -> BluerState {
#[inline(never)]
async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> {
let addrs: Vec<Address> = adapter
.device_addresses()
.await
.unwrap_or_default()
.into_iter()
.filter(|addr| !devices.iter().any(|d| d.address == *addr))
.collect();
let addrs = adapter.device_addresses().await.unwrap_or_default();
devices.clear();
if addrs.len() > devices.capacity() {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,18 @@
[package]
name = "cosmic-applet-network"
version = "1.0.2"
version = "1.0.15"
edition = "2024"
license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
async-fn-stream = "0.3"
cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" }
cosmic-dbus-networkmanager = { path = "../../dbus-settings-bindings/networkmanager" }
futures.workspace = true
futures-util.workspace = true
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic = { workspace = true, features = [
cosmic = { workspace = true, features = [
"applet",
"applet-token",
"tokio",
@ -34,5 +34,4 @@ nmrs = "3.1.3"
[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"

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

@ -1,16 +1,16 @@
network = Hálózat
airplane-mode = Repülőgép-üzemmód
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
identity = Azonosító
ipv4 = IPv4-cím
ipv6 = IPv6-cím
mac = MAC
megabits-per-second = Mbps
connected = Csatlakoztatva
connecting = Csatlakozás…
connect = Csatlakozás
connected = Kapcsolódva
connecting = Kapcsolódás
connect = Kapcsolódás
cancel = Mégse
settings = Hálózati beállítások…
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

@ -35,7 +35,7 @@ use cosmic::{
widget::{
Id, button, column, container, divider,
icon::{self, from_name},
row, scrollable, secure_input, text, text_input, toggler,
indeterminate_circular, row, scrollable, secure_input, text, text_input, toggler,
},
};
use cosmic_dbus_networkmanager::interface::enums::{
@ -405,6 +405,7 @@ fn secret_agent_task(identifier: String) -> Task<NmAgentEvent> {
cosmic::Task::stream(async_fn_stream::fn_stream(move |emitter| async move {
let registration = SecretAgent::builder()
.with_identifier(identifier)
.with_object_path("/org/freedesktop/NetworkManager/SecretAgent")
.with_capabilities(SecretAgentCapabilities::VPN_HINTS)
.register()
.await;
@ -664,6 +665,13 @@ fn load_vpns(_conn: zbus::Connection) -> Task<crate::app::Message> {
let mut map: IndexMap<UUID, ConnectionSettings> = IndexMap::new();
for c in saved {
// Skip in-memory-only NM connections — assumed connections that NM
// auto-generated from externally-managed interfaces (e.g. one
// brought up by wg-quick@wg0.service) report unsaved=true and
// evaporate on deactivate, leaving the applet's toggle dead.
if c.unsaved {
continue;
}
let uuid: UUID = Arc::from(c.uuid.as_str());
let entry = match c.summary {
SettingsSummary::WireGuard { .. } => ConnectionSettings::Wireguard { id: c.id },
@ -726,9 +734,10 @@ impl cosmic::Application for CosmicNetworkApplet {
tasks.push(update_state(conn.clone()));
tasks.push(update_devices(conn.clone()));
tasks.push(load_vpns(conn));
let uuid = uuid::Uuid::new_v4().to_string().replace("-", "_");
let my_id = format!(
"com.system76.CosmicSettings.Applet.{}.NetworkManager.SecretAgent",
uuid::Uuid::new_v4()
"com.system76.CosmicSettings.Applet._{uuid}.NetworkManager.SecretAgent",
);
tasks.push(secret_agent_task(my_id).map(Message::SecretAgent));
}
@ -1507,12 +1516,7 @@ impl cosmic::Application for CosmicNetworkApplet {
];
match state {
ActiveConnectionState::Activating | ActiveConnectionState::Deactivating => {
btn_content.push(
icon::from_name("process-working-symbolic")
.size(24)
.symbolic(true)
.into(),
);
btn_content.push(indeterminate_circular().size(24.0).into());
}
ActiveConnectionState::Activated => btn_content.push(
text::body(fl!("connected"))
@ -1720,12 +1724,7 @@ impl cosmic::Application for CosmicNetworkApplet {
.into(),
);
btn_content.push(ssid.into());
btn_content.push(
icon::from_name("process-working-symbolic")
.size(24)
.symbolic(true)
.into(),
);
btn_content.push(indeterminate_circular().size(24.0).into());
} else if matches!(known.state, DeviceState::Unavailable) {
btn_content.push(
icon::from_name("network-wireless-disconnected-symbolic")
@ -1897,10 +1896,7 @@ impl cosmic::Application for CosmicNetworkApplet {
let connecting = padded_control(
row::with_children([
Element::from(id),
icon::from_name("process-working-symbolic")
.size(24)
.symbolic(true)
.into(),
indeterminate_circular().size(24.0).into(),
])
.spacing(8),
);

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applet-notifications"
version = "1.0.2"
version = "1.0.15"
edition = "2024"
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-config = { git = "https://github.com/pop-os/cosmic-notifications" }
anyhow.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
tokio.workspace = true
# cosmic-notifications-util = { path = "../../cosmic-notifications-daemon/cosmic-notifications-util" }
# cosmic-notifications-config = { path = "../../cosmic-notifications-daemon/cosmic-notifications-config" }

View file

@ -1,14 +1,14 @@
hours-ago =
{ $duration ->
[0] Most
[one] 1 órával ezelőtt
*[other] { $duration } órával ezelőtt
[0] Épp most
[one] 1 órája
*[other] { $duration } órája
}
minutes-ago =
{ $duration ->
[0] Most
[one] 1 perccel ezelőtt
*[other] { $duration } perccel ezelőtt
[0] Épp most
[one] 1 perce
*[other] { $duration } perce
}
show-less = Kevesebb 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

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

View file

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

View file

@ -33,4 +33,4 @@ confirm-body =
[lock-screen] zárolni fogja a képernyőt
[log-out] kijelentkezik
*[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

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

View file

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

View file

@ -9,11 +9,14 @@ use cosmic::{
},
cctk::sctk::reexports::calloop,
iced::{
self, Length, Subscription,
self,
Event::Mouse,
Length, Subscription, event,
mouse::{self, ScrollDelta},
platform_specific::shell::commands::popup::{destroy_popup, get_popup},
theme::Style,
window,
},
scroll::{DiscreteScrollDelta, DiscreteScrollState},
surface,
widget::{container, mouse_area},
};
@ -25,6 +28,7 @@ use crate::{
};
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum Msg {
None,
Activate(usize),
@ -33,7 +37,8 @@ pub enum Msg {
StatusMenu((usize, status_menu::Msg)),
StatusNotifier(status_notifier_watcher::Event),
TogglePopup(usize),
Hovered(usize),
Hovered(Option<usize>),
WheelScrolled(ScrollDelta),
Surface(surface::Action),
ToggleOverflow,
HoveredOverflow,
@ -49,6 +54,8 @@ pub(crate) struct App {
max_menu_id: usize,
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>>,
}
@ -103,9 +110,10 @@ impl App {
let overflow_index = self.overflow_index().unwrap_or(0);
let children = self.menus.iter().skip(overflow_index).map(|(id, menu)| {
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()
});
@ -210,6 +218,7 @@ impl cosmic::Application for App {
{
let id = *id;
self.menus.remove(&id);
self.scroll_states.remove(&id);
if self.open_menu == Some(id) {
self.open_menu = None;
if let Some(popup_id) = self.popup {
@ -312,6 +321,10 @@ impl cosmic::Application for App {
}
},
Msg::Hovered(id) => {
self.hovered_menu = id;
let Some(id) = id else {
return Task::none();
};
let mut cmds = Vec::new();
if let Some(old_id) = self.open_menu.take() {
if old_id != id {
@ -361,6 +374,19 @@ impl cosmic::Application for App {
cmds.push(get_popup(popup_settings));
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) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
@ -405,6 +431,7 @@ impl cosmic::Application for App {
}
}
Msg::HoveredOverflow => {
self.hovered_menu = None;
let mut cmds = Vec::new();
if self.overflow_popup.is_some() {
// 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(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)
}
@ -472,9 +505,10 @@ impl cosmic::Application for App {
.iter()
.take(overflow_index.unwrap_or(self.menus.len()))
.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_enter(Msg::Hovered(*id))
.on_enter(Msg::Hovered(Some(*id)))
.on_exit(Msg::Hovered(None))
.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>(
applet: &'a cosmic::applet::Context,
menu: &'a status_menu::State,
@ -577,7 +638,7 @@ fn menu_icon_button<'a>(
let icon = menu.icon_handle().clone();
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let _theme = theme.cosmic();
let suggested = applet.suggested_size(true);
let padding = applet.suggested_padding(true).1;
@ -595,7 +656,7 @@ fn menu_icon_button<'a>(
.class(if symbolic {
cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| {
cosmic::iced::widget::svg::Style {
color: Some(theme.cosmic().background.on.into()),
color: Some(theme.cosmic().background(false).on.into()),
}
}))
} else {
@ -614,3 +675,43 @@ fn menu_icon_button<'a>(
pub fn main() -> iced::Result {
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>,
// TODO handle icon with multiple sizes?
icon_handle: icon::Handle,
icon_theme_path: Option<PathBuf>,
click_event: Option<(i32, bool)>,
}
@ -39,6 +40,7 @@ impl State {
icon_handle: icon::from_name("application-default")
.prefer_svg(true)
.handle(),
icon_theme_path: None,
click_event: None,
},
iced::Task::none(),
@ -63,6 +65,7 @@ impl State {
}
Msg::Icon(update) => {
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.
if icon_name.is_empty() {
@ -93,13 +96,18 @@ impl State {
self.icon_handle = if Path::new(&icon_name).exists() {
icon::from_path(Path::new(&icon_name).to_path_buf()).symbolic(true)
} else {
icon::from_name(icon_name)
.prefer_svg(true)
.fallback(Some(IconFallback::Names(vec![
let mut builder = icon::from_name(icon_name).prefer_svg(true).fallback(Some(
IconFallback::Names(vec![
"application-default".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()

View file

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

View file

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

View file

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

View file

@ -3,11 +3,11 @@ tile-current = Jelenlegi munkaterület csempézése
shortcuts = Gyorsbillentyűk
navigate-windows = Ablakok navigálá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…
active-hint = Aktív ablak kiemelése
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…
all-workspaces = Összes munkaterület
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

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

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applet-time"
version = "1.0.2"
version = "1.0.15"
edition = "2024"
license = "GPL-3.0-only"
@ -9,7 +9,7 @@ cosmic-applets-config.workspace = true
jiff = "0.2"
i18n-embed-fl.workspace = true
i18n-embed.workspace = true
libcosmic.workspace = true
cosmic.workspace = true
rust-embed.workspace = true
tokio.workspace = true
tracing-log.workspace = true
@ -17,5 +17,5 @@ tracing-subscriber.workspace = true
tracing.workspace = true
icu = { version = "2.1.1", features = ["compiled_data"] }
zbus.workspace = true
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
timedate-zbus = { path = "../../dbus-settings-bindings/timedate" }
logind-zbus = "5.3.2"

View file

@ -0,0 +1 @@
datetime-settings = Sazkariyên dîrok, dem û salnameyê...

View file

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

View file

@ -1,12 +1,12 @@
[package]
name = "cosmic-applet-workspaces"
version = "1.0.2"
version = "1.0.15"
authors = ["Ashley Wulber <ashley@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"
[dependencies]
libcosmic.workspace = true
cosmic.workspace = true
cctk.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

View file

@ -0,0 +1 @@
cosmic-applet-workspaces = Qadên xebatê yên COSMIC

View file

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

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applets-config"
version = "1.0.2"
version = "1.0.15"
edition = "2024"
[dependencies]

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-applets"
version = "1.0.2"
version = "1.0.15"
edition = "2024"
license = "GPL-3.0-only"
@ -23,7 +23,7 @@ cosmic-applet-time = { path = "../cosmic-applet-time" }
cosmic-applet-workspaces = { path = "../cosmic-applet-workspaces" }
cosmic-applet-input-sources = { path = "../cosmic-applet-input-sources" }
cosmic-panel-button = { path = "../cosmic-panel-button" }
libcosmic.workspace = true
cosmic.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
tracing-log.workspace = true

View file

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

View file

@ -1,11 +1,11 @@
[package]
name = "cosmic-panel-button"
version = "1.0.2"
version = "1.0.15"
edition = "2024"
license = "GPL-3.0-only"
[dependencies]
libcosmic.workspace = true
cosmic.workspace = true
rustc-hash.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true

View file

@ -48,7 +48,7 @@ impl Button {
icon: cosmic::widget::icon::Handle,
) -> cosmic::widget::Button<'a, Message> {
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let _theme = theme.cosmic();
let suggested = self.core.applet.suggested_size(icon.symbolic);
let (major_padding, applet_padding_minor_axis) =
@ -66,7 +66,7 @@ impl Button {
.class(if symbolic {
cosmic::theme::Svg::Custom(std::rc::Rc::new(|theme| {
cosmic::iced::widget::svg::Style {
color: Some(theme.cosmic().background.on.into()),
color: Some(theme.cosmic().background(false).on.into()),
}
}))
} else {

6
debian/changelog vendored
View file

@ -1,3 +1,9 @@
cosmic-applets (1.0.15) noble; urgency=medium
* Update changelog
-- Michael Murphy <mmstick@pop-os> Thu, 28 May 2026 19:33:02 +0200
cosmic-applets (1.0.12) noble; urgency=medium
* Update changelog

View file

@ -40,7 +40,7 @@ cosmic-applet-power-comment = Képernyő zárolása, kijelentkezés, felfüggesz
cosmic-app-list-comment = Rögzített alkalmazások indítása és a megnyitott ablakok kezelése
cosmic-applet-status-area-comment = Alkalmazásjelzők, melyek menüt is megjeleníthetnek a panelen
cosmic-applet-status-area-keywords = COSMIC;kisalkalmazás;alkalmazás;app;jelző;értesítések;tálca;állapot;
cosmic-applet-tiling-comment = Az aktív ablak kiemelésének, valamint az aktuális és munkaterületenkénti automatikus csempézés kezelése
cosmic-applet-tiling-comment = Az aktív ablakkiemelés, valamint a jelenlegi és munkaterületenkénti automatikus csempézés kezelése
cosmic-applet-tiling-keywords = COSMIC;kisalkalmazás;csempézés;kiemelés;munkaterületek;
cosmic-applet-time-comment = Az aktuális idő megjelenítése a panelen, felugró naptárral
cosmic-applet-time-keywords = COSMIC;kisalkalmazás;dátum;idő;naptár;

View file

@ -0,0 +1 @@
cosmic-applet-a11y = アクセシビリティ

View file

@ -0,0 +1,51 @@
cosmic-applet-audio = Deng
cosmic-app-list = Destgeha sepanan
cosmic-applet-a11y = Gihîştin
cosmic-applet-bluetooth = Bluetooth
cosmic-applet-bluetooth-comment = Amûrên Bluetooth bi rê ve bibe
cosmic-applet-bluetooth-keywords = COSMIC;Sepanok;Bluetooth;
cosmic-applet-input-sources = Çavkaniyên ketanê
cosmic-applet-input-sources-comment = Di navbera çavkaniyên ketanê de biguhêrîne
cosmic-applet-input-sources-keywords = COSMIC;Sepanok;Ketan;Çavkanî;
cosmic-app-list-comment = Sepanên darxistî veke û çarçoveyên vekirî bi rê ve bibe
cosmic-app-list-keywords = COSMIC;Sepanok;Sepan;Destgeh;Lîste;Erk;Bend;
cosmic-applet-a11y-comment = Sazkariyên gihîştinê ji destgehê saz bike
cosmic-applet-a11y-keywords = COSMIC;Sepanok;Gihîştin;Dîmender;A11y;Xwîner;Dîmender;Mezinker;Bijberî;Reng;
cosmic-applet-audio-comment = Hilbijartina amûra dengê, kontrola dengê, û kontrolên mediyaya MPRIS
cosmic-applet-audio-keywords = COSMIC;Sepanok;Deng;Deng;MPRIS;
cosmic-applet-battery = Hêz & Betarî
cosmic-applet-battery-comment = Awayên hêzê û vebijêrkên parastina hêzê
cosmic-applet-battery-keywords = COSMIC;Sepanok;Hêz;Betarî;
cosmic-applet-minimize = Çarçoveyên biçûk
cosmic-applet-minimize-comment = Çarçoveyên biçûk bi rê ve bibe
cosmic-applet-minimize-keywords = COSMIC;Sepanok;Biçûkkirin;
cosmic-applet-network = Tor
cosmic-applet-network-comment = Girêdanên torê bi rê ve bibe
cosmic-applet-network-keywords = COSMIC;Sepanok;Tor;
cosmic-applet-notifications = Navenda Agahdariyan
cosmic-applet-notifications-comment = Agahdarî û rawestandina balkişiyê bi rê ve bibe
cosmic-applet-notifications-keywords = COSMIC;Sepanok;Agahdarî;
cosmic-applet-power = Danaşîna bikarhêner
cosmic-applet-power-comment = Dîmenderê kilît bike, derkeve, rawestîne, ji nû ve bide destpêkirin, û vemrîne
cosmic-applet-power-keywords = COSMIC;Sepanok;Bikarhêner;Danaşîn;Kilîtkirin;Derketin;Destpêkirin;Vemirandin;Rawestandin;
cosmic-applet-status-area = Desgeha agahdariyan
cosmic-applet-status-area-comment = Nîşanderên sepanan ên ku dibe ku menuyan bo destgehê derxînin
cosmic-applet-status-area-keywords = COSMIC;Sepanok;Sepan;Nîşander;Agahdarî;Destgeh;Rewş;
cosmic-applet-tiling = Berhevkirin
cosmic-applet-tiling-comment = Berhevkirina xweber a çalak, ya niha û ya her qada xebatê bi rê ve bibe
cosmic-applet-tiling-keywords = COSMIC;Sepanok;Berhevkirin;Nîşander;Navenda xebatê;
cosmic-applet-time = Dîrok, dem & Salname
cosmic-applet-time-comment = Demê niha li ser destgehê bi salnameyeke vebûyî nîşan bide
cosmic-applet-time-keywords = COSMIC;Sepanok;Dîrok;Dem;Salname;
cosmic-applet-workspaces = Qada xebatê jimarkirî
cosmic-applet-workspaces-comment = Di navbera qada xebatê jimarkirî de di destgehê de biguherîne
cosmic-applet-workspaces-keywords = COSMIC;Sepanok;Qada xebatê;
cosmic-panel-app-button = Bişkoka pirtûkxaneya sepanan
cosmic-panel-app-button-comment = Pirtûkxaneya sepanan ji bo vekirina sepanên sazkirî veke
cosmic-panel-app-button-keywords = COSMIC;Sepanok;Sepan;Pirtûkxane;Destgeh;
cosmic-panel-launcher-button = Bişkoka destpêker
cosmic-panel-launcher-button-comment = Destpêkerê ji bo lêgerîna sepanan û xebitandina fermanan veke
cosmic-panel-launcher-button-keywords = COSMIC;Sepanok;Destpêker;Rêveber;
cosmic-panel-workspaces-button = Bişkoka qadên xebatê
cosmic-panel-workspaces-button-comment = Pêşdîtina qadên xebatê veke ji bo bi rê ve bibî û biguhêrînî
cosmic-panel-workspaces-button-keywords = COSMIC;Sepan;Qada xebatê;Pêşdîtin;

View file

@ -46,3 +46,6 @@ cosmic-panel-launcher-button-keywords = COSMIC;애플릿;런처;실행기;
cosmic-panel-workspaces-button = 작업 공간 버튼
cosmic-panel-workspaces-button-comment = 작업 공간 관리 및 전환을 위해 작업 공간 개요 열기
cosmic-panel-workspaces-button-keywords = COSMIC;애플릿;작업 공간;개요;
cosmic-applet-workspaces-comment = 패널에서 번호가 지정된 작업 공간 간 전환
cosmic-applet-tiling-comment = 활성 창 강조, 현재 및 작업 공간별 자동 타일링 관리
cosmic-applet-workspaces = 번호가 지정된 작업 공간

View file

@ -20,3 +20,8 @@ cosmic-applet-workspaces-comment = Alternar entre áreas de trabalho numeradas n
cosmic-applet-workspaces-keywords = COSMIC;Miniaplicação;Applet;Área;Trabalho;
cosmic-panel-workspaces-button-comment = Abrir a visão geral das áreas de trabalho para gerir e alternar entre elas
cosmic-panel-workspaces-button-keywords = COSMIC;Miniaplicação;Applet;Área;Trabalho;Visão;Geral;
cosmic-applet-a11y-comment = Configurar definições de acessibilidade a partir do painel
cosmic-applet-battery = Energia e Bateria
cosmic-app-list-keywords = COSMIC;Miniaplicação;Applet;Aplicação;Bandeja;Lista;Tarefa;Barra;
cosmic-applet-a11y-keywords = COSMIC;Miniaplicação;Applet;Acessibilidade;A11y;Ecrã;Leitor;Lupa;Ampliador;Contraste;Cor;
cosmic-applet-audio-keywords = COSMIC;Miniaplicação;Applet;Som;Áudio;MPRIS;

View file

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

View file

@ -59,13 +59,16 @@ _install_status_notifier_watcher:
sed "s|@bindir@|{{ prefixdir }}|" cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service.in > cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service
install -Dm0644 cosmic-applet-status-area/data/com.system76.CosmicStatusNotifierWatcher.service {{ libdir }}/systemd/user/com.system76.CosmicStatusNotifierWatcher.service
_install_secret_agent_policy:
install -Dm0644 cosmic-applet-network/data/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf {{ sharedir }}/dbus-1/system.d/com.system76.CosmicSettings.Applet.NetworkManager.SecretAgent.conf
# Installs files into the system
install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') _install_metainfo _install_status_notifier_watcher
install: (_install_bin 'cosmic-applets') (_link_applet 'cosmic-panel-button') (_install_applet 'com.system76.CosmicAppList' 'cosmic-app-list') (_install_default_schema 'cosmic-app-list') (_install_applet 'com.system76.CosmicAppletA11y' 'cosmic-applet-a11y') (_install_applet 'com.system76.CosmicAppletAudio' 'cosmic-applet-audio') (_install_applet 'com.system76.CosmicAppletInputSources' 'cosmic-applet-input-sources') (_install_applet 'com.system76.CosmicAppletBattery' 'cosmic-applet-battery') (_install_applet 'com.system76.CosmicAppletBluetooth' 'cosmic-applet-bluetooth') (_install_applet 'com.system76.CosmicAppletMinimize' 'cosmic-applet-minimize') (_install_applet 'com.system76.CosmicAppletNetwork' 'cosmic-applet-network') (_install_applet 'com.system76.CosmicAppletNotifications' 'cosmic-applet-notifications') (_install_applet 'com.system76.CosmicAppletPower' 'cosmic-applet-power') (_install_applet 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area') (_install_applet 'com.system76.CosmicAppletTiling' 'cosmic-applet-tiling') (_install_applet 'com.system76.CosmicAppletTime' 'cosmic-applet-time') (_install_applet 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces') (_install_button 'com.system76.CosmicPanelAppButton' 'cosmic-panel-app-button') (_install_button 'com.system76.CosmicPanelLauncherButton' 'cosmic-panel-launcher-button') (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button') _install_metainfo _install_status_notifier_watcher _install_secret_agent_policy
# Vendor Cargo dependencies locally
vendor:
mkdir -p .cargo
cargo vendor | head -n -1 > .cargo/config
cargo vendor --locked | head -n -1 > .cargo/config
echo 'directory = "vendor"' >> .cargo/config
tar pcf vendor.tar vendor
rm -rf vendor

56
redeploy.sh Executable file
View file

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

View file

@ -1,2 +1,2 @@
[toolchain]
channel = "1.93"
channel = "1.93.0"