Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d96863b687 | ||
|
|
8156fc47ca | ||
|
|
9bc771bcca | ||
|
|
fe74a82a46 | ||
|
|
ac231eeb54 | ||
|
|
8cf03d4529 | ||
|
|
2a716dac9e | ||
|
|
719ef2f38e | ||
|
|
9ba224d776 | ||
|
|
8b59b78d9f | ||
|
|
e6b4d3c1a1 | ||
|
|
70d8d32ee2 | ||
|
|
c37ca3baee | ||
|
|
bd5d030d91 |
+2
-2
@@ -14,10 +14,10 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
# Generated from the colour scheme; machine state, not configuration.
|
# Generated from the color scheme; machine state, not configuration.
|
||||||
/config/dot/kitty/current-theme.conf
|
/config/dot/kitty/current-theme.conf
|
||||||
|
|
||||||
# Generated from the colour scheme; machine state, not configuration.
|
# Generated from the color scheme; machine state, not configuration.
|
||||||
/config/dot/gtk-3.0/settings.ini
|
/config/dot/gtk-3.0/settings.ini
|
||||||
/config/dot/gtk-4.0/settings.ini
|
/config/dot/gtk-4.0/settings.ini
|
||||||
/config/dot/tmux/current-theme.conf
|
/config/dot/tmux/current-theme.conf
|
||||||
|
|||||||
@@ -152,11 +152,14 @@ case "$cmd" in
|
|||||||
|
|
||||||
# 2.4) Run flatpak updates (user then system)
|
# 2.4) Run flatpak updates (user then system)
|
||||||
flatpak update -y
|
flatpak update -y
|
||||||
sudo flatpak update
|
sudo flatpak update -y
|
||||||
|
|
||||||
# 2.5) Optional firmware via fwupd
|
# 2.5) Optional firmware via fwupd
|
||||||
if $firmware; then
|
if $firmware; then
|
||||||
sudo fwupdmgr refresh
|
# fwupdmgr exits non-zero when metadata is already current -- that is
|
||||||
|
# not an error, but under 'set -e' it would abort the script before
|
||||||
|
# 'fwupdmgr update' ever runs.
|
||||||
|
sudo fwupdmgr refresh || true
|
||||||
sudo fwupdmgr update
|
sudo fwupdmgr update
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# btop ships no Tokyo Night light variant at all, and the nearest stock light
|
# btop ships no Tokyo Night light variant at all, and the nearest stock light
|
||||||
# theme (flat-remix-light) is a different palette that happens to have a similar
|
# theme (flat-remix-light) is a different palette that happens to have a similar
|
||||||
# background. Same colours as kitty/themes/tokyonight-day.conf so the terminal
|
# background. Same colors as kitty/themes/tokyonight-day.conf so the terminal
|
||||||
# and what runs inside it cannot disagree.
|
# and what runs inside it cannot disagree.
|
||||||
#
|
#
|
||||||
# Gradients keep the same low -> middle -> high meaning as the dark theme, using
|
# Gradients keep the same low -> middle -> high meaning as the dark theme, using
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# GENERATED FILE -- edit settings.ini.template instead.
|
# GENERATED FILE -- edit settings.ini.template instead.
|
||||||
#
|
#
|
||||||
# The theme name and dark preference below follow Panama's colour
|
# The theme name and dark preference below follow Panama's color
|
||||||
# scheme, so this file is regenerated on every switch and is not
|
# scheme, so this file is regenerated on every switch and is not
|
||||||
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
||||||
# XSETTINGS and this file is ignored; under Hyprland there is no
|
# XSETTINGS and this file is ignored; under Hyprland there is no
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# GENERATED FILE -- edit settings.ini.template instead.
|
# GENERATED FILE -- edit settings.ini.template instead.
|
||||||
#
|
#
|
||||||
# The theme name and dark preference below follow Panama's colour
|
# The theme name and dark preference below follow Panama's color
|
||||||
# scheme, so this file is regenerated on every switch and is not
|
# scheme, so this file is regenerated on every switch and is not
|
||||||
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
||||||
# XSETTINGS and this file is ignored; under Hyprland there is no
|
# XSETTINGS and this file is ignored; under Hyprland there is no
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
|||||||
| Hidden applications | GNOME App Hider list in Vicinae provider config | Live |
|
| Hidden applications | GNOME App Hider list in Vicinae provider config | Live |
|
||||||
| Tiling and dynamic workspaces | Hyprland Lua config, Forge keymap preserved | Live |
|
| Tiling and dynamic workspaces | Hyprland Lua config, Forge keymap preserved | Live |
|
||||||
| Overview | Continuum filmstrip, large workspace preview, cross-workspace search | Live |
|
| Overview | Continuum filmstrip, large workspace preview, cross-workspace search | Live |
|
||||||
| Minimise | `SUPER+SHIFT+X` sends to scratchpad; `SUPER+X` restores | Live |
|
| Minimize | `SUPER+SHIFT+X` sends to scratchpad; `SUPER+X` restores | Live |
|
||||||
| Dock | Quickshell dock with GNOME favourites and intellihide | Live |
|
| Dock | Quickshell dock with GNOME favorites and intellihide | Live |
|
||||||
| Panel | Workspaces, CPU/RAM/GPU, clock, weather, quiet event capsule, media, clipboard, tray, status | Live |
|
| Panel | Workspaces, CPU/RAM/GPU, clock, weather, quiet event capsule, media, clipboard, tray, status | Live |
|
||||||
| Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live |
|
| Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live |
|
||||||
| Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live |
|
| Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live |
|
||||||
| Date menu | Two-column Daybook with Agenda, Ongoing activity, Notifications, month calendar, weather, and media | Live |
|
| Date menu | Two-column Daybook with Agenda, Ongoing activity, Notifications, month calendar, weather, and media | Live |
|
||||||
| Ongoing activity | Focus, Caffeine, recording, screen sharing, camera, and microphone state with direct controls | Live |
|
| Ongoing activity | Focus, Caffeine, recording, screen sharing, camera, and microphone state with direct controls | Live |
|
||||||
| Calendar agenda | Existing GNOME, Google, iCloud, and Nextcloud sources through Evolution Data Server | Live |
|
| Calendar agenda | Existing GNOME, Google, iCloud, and Nextcloud sources through Evolution Data Server | Live |
|
||||||
| Control Center | Wi-Fi, Bluetooth, audio input/output, Caffeine, Night Light, Focus, Home Assistant favourites, and KDE Connect phone continuity | Live |
|
| Control Center | Wi-Fi, Bluetooth, audio input/output, Caffeine, Night Light, Focus, Home Assistant favorites, and KDE Connect phone continuity | Live |
|
||||||
| Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live |
|
| Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live |
|
||||||
| Brightness | Appears only when a backlight device exists | Live, not applicable on this monitor |
|
| Brightness | Appears only when a backlight device exists | Live, not applicable on this monitor |
|
||||||
| AppIndicator / tray | Native StatusNotifierItem host and themed menus | Live |
|
| AppIndicator / tray | Native StatusNotifierItem host and themed menus | Live |
|
||||||
@@ -36,7 +36,7 @@ Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
|||||||
| Removable media | udiskie plus udisks notifications | Live |
|
| Removable media | udiskie plus udisks notifications | Live |
|
||||||
| Autostart apps | Nextcloud, Bitwarden, and RustDesk system service/tray | Live |
|
| Autostart apps | Nextcloud, Bitwarden, and RustDesk system service/tray | Live |
|
||||||
| Printer administration | CUPS with the `system-config-printer` graphical interface | Live |
|
| Printer administration | CUPS with the `system-config-printer` graphical interface | Live |
|
||||||
| System settings | The Settings app for display policy, appearance, desktop, sound, focus, shortcuts, and services; labelled GNOME hardware/account handoffs | Live |
|
| System settings | The Settings app for display policy, appearance, desktop, sound, focus, shortcuts, and services; labeled GNOME hardware/account handoffs | Live |
|
||||||
| System health and recovery | Settings → System Health, `Panama: Check System Health` in Vicinae, a degraded-only bar indicator, redacted reports, and bounded Panama-owned repairs | Live |
|
| System health and recovery | Settings → System Health, `Panama: Check System Health` in Vicinae, a degraded-only bar indicator, redacted reports, and bounded Panama-owned repairs | Live |
|
||||||
|
|
||||||
## System health and recovery
|
## System health and recovery
|
||||||
@@ -59,7 +59,7 @@ They never install packages, invoke `sudo`, delete user data, or rewrite
|
|||||||
arbitrary configuration.
|
arbitrary configuration.
|
||||||
|
|
||||||
Generic Fedora configuration remains with the system tools that own it. The
|
Generic Fedora configuration remains with the system tools that own it. The
|
||||||
final System Health card hands network settings, users, sharing, colour
|
final System Health card hands network settings, users, sharing, color
|
||||||
profiles, and digital wellbeing to their exact GNOME Settings panels rather
|
profiles, and digital wellbeing to their exact GNOME Settings panels rather
|
||||||
than presenting inert Hyprland controls.
|
than presenting inert Hyprland controls.
|
||||||
|
|
||||||
@@ -77,9 +77,9 @@ than presenting inert Hyprland controls.
|
|||||||
| Caffeine | Replaced by a real logind inhibitor in quick settings |
|
| Caffeine | Replaced by a real logind inhibitor in quick settings |
|
||||||
| Blur My Shell / Openbar / User Theme | Replaced by the Prism shell and compositor blur |
|
| Blur My Shell / Openbar / User Theme | Replaced by the Prism shell and compositor blur |
|
||||||
| Bluetooth Quick Connect | Replaced by the full Bluetooth picker |
|
| Bluetooth Quick Connect | Replaced by the full Bluetooth picker |
|
||||||
| Wi-Fi QR | Deliberately omitted; it is not useful enough to justify another credential-reading surface |
|
| Wi-Fi QR | Replaced by an on-demand QR-code sharing flow in Control Center's Wi-Fi panel; the code is generated only while shown and written to tmpfs, never persisted |
|
||||||
| GSConnect | Replaced by capability-aware KDE Connect phone continuity in Control Center; the paired iPhone exposes file, clipboard, and Ring actions when reachable |
|
| GSConnect | Replaced by capability-aware KDE Connect phone continuity in Control Center; the paired iPhone exposes file, clipboard, and Ring actions when reachable |
|
||||||
| Home Assistant | Replaced by secure favourites in Control Center; explicit private environment values take precedence over the existing GNOME extension and Secret Service setup |
|
| Home Assistant | Replaced by secure favorites in Control Center; explicit private environment values take precedence over the existing GNOME extension and Secret Service setup |
|
||||||
| Custom Hot Corners Extended | No action was configured, so there is no behavior to port |
|
| Custom Hot Corners Extended | No action was configured, so there is no behavior to port |
|
||||||
| App Hider | Hidden app list is now applied in Vicinae |
|
| App Hider | Hidden app list is now applied in Vicinae |
|
||||||
| Allow Locked Remote Desktop | Mutter-specific patch; RustDesk is independent, but lock-screen access needs a real remote test |
|
| Allow Locked Remote Desktop | Mutter-specific patch; RustDesk is independent, but lock-screen access needs a real remote test |
|
||||||
@@ -111,4 +111,4 @@ menu for a minority, which is worse than omitting it. Relevant references:
|
|||||||
- <https://github.com/hyprwm/Hyprland/discussions/1358>
|
- <https://github.com/hyprwm/Hyprland/discussions/1358>
|
||||||
|
|
||||||
A future focused-window menu could expose compositor actions such as float,
|
A future focused-window menu could expose compositor actions such as float,
|
||||||
move, fullscreen, and close. It should not be labelled as an application menu.
|
move, fullscreen, and close. It should not be labeled as an application menu.
|
||||||
|
|||||||
+14
-14
@@ -30,7 +30,7 @@ Don't "fix" them.
|
|||||||
| `prefs.lua` | Reads the settings file the Settings app writes. See below |
|
| `prefs.lua` | Reads the settings file the Settings app writes. See below |
|
||||||
| `env.lua` | Environment. Note the uwsm caveat below |
|
| `env.lua` | Environment. Note the uwsm caveat below |
|
||||||
| `monitors.lua` | DP-2 geometry, scaling, and the HDR decision |
|
| `monitors.lua` | DP-2 geometry, scaling, and the HDR decision |
|
||||||
| `looks.lua` | Colours, blur, glow, shadows, animations, VRR, scanout |
|
| `looks.lua` | Colors, blur, glow, shadows, animations, VRR, scanout |
|
||||||
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
||||||
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
||||||
| `keybinds.lua` | The full keymap |
|
| `keybinds.lua` | The full keymap |
|
||||||
@@ -87,7 +87,7 @@ persistence, validation, reset, and the live write are all derived from that
|
|||||||
entry.
|
entry.
|
||||||
|
|
||||||
`prefs.lua` never raises. A missing, empty, truncated, malformed, or
|
`prefs.lua` never raises. A missing, empty, truncated, malformed, or
|
||||||
wrong-typed settings file costs you your customisations and nothing else;
|
wrong-typed settings file costs you your customizations and nothing else;
|
||||||
`tests/hypr/prefs-fallback-contract.sh` pins that, including that Hyprland still
|
`tests/hypr/prefs-fallback-contract.sh` pins that, including that Hyprland still
|
||||||
accepts the config in each of those states.
|
accepts the config in each of those states.
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ four places, and nowhere else —
|
|||||||
dock, popovers), running blue on the left to orchid on the right and fading
|
dock, popovers), running blue on the left to orchid on the right and fading
|
||||||
out before the corners. See `quickshell/widgets/PrismEdge.qml`.
|
out before the corners. See `quickshell/widgets/PrismEdge.qml`.
|
||||||
2. The focused window's border — `general.col.active_border`, blue→orchid at
|
2. The focused window's border — `general.col.active_border`, blue→orchid at
|
||||||
115°. Unfocused windows get no colour at all, because the gradient only
|
115°. Unfocused windows get no color at all, because the gradient only
|
||||||
means something if one window on screen is wearing it.
|
means something if one window on screen is wearing it.
|
||||||
3. The active workspace pill in the bar.
|
3. The active workspace pill in the bar.
|
||||||
4. Slider fills.
|
4. Slider fills.
|
||||||
@@ -179,9 +179,9 @@ which is the opposite of the intent.
|
|||||||
|
|
||||||
The Nerd Font is still used, but only to draw **icon glyphs** — it is the
|
The Nerd Font is still used, but only to draw **icon glyphs** — it is the
|
||||||
pragmatic alternative to freedesktop symbolic icons, which ship with a
|
pragmatic alternative to freedesktop symbolic icons, which ship with a
|
||||||
hardcoded `#2e3436` fill that Qt (unlike GTK) will not recolour. Where a
|
hardcoded `#2e3436` fill that Qt (unlike GTK) will not recolor. Where a
|
||||||
themed freedesktop icon is wanted instead, `quickshell/widgets/ThemedIcon.qml`
|
themed freedesktop icon is wanted instead, `quickshell/widgets/ThemedIcon.qml`
|
||||||
paints a palette colour through the icon's alpha.
|
paints a palette color through the icon's alpha.
|
||||||
|
|
||||||
Anything whose digits change in place — the clock, the vitals percentages, the
|
Anything whose digits change in place — the clock, the vitals percentages, the
|
||||||
recording timer, the selection readout — sets `font.features:
|
recording timer, the selection readout — sets `font.features:
|
||||||
@@ -247,7 +247,7 @@ The mental model is unchanged from Forge:
|
|||||||
| `SUPER + SHIFT + H/J/K/L` | Move window |
|
| `SUPER + SHIFT + H/J/K/L` | Move window |
|
||||||
| `SUPER + CTRL + H/J/K/L` | Swap window |
|
| `SUPER + CTRL + H/J/K/L` | Swap window |
|
||||||
| `SUPER + SHIFT + Y/O` · `B/M` | Wider · narrower |
|
| `SUPER + SHIFT + Y/O` · `B/M` | Wider · narrower |
|
||||||
| `SUPER + SHIFT + I/U` · `P/N` | Taller · shorter |
|
| `SUPER + SHIFT + I/U` · `,/N` | Taller · shorter |
|
||||||
| `SUPER + [` / `]` / `=` | Shrink / expand / reset split |
|
| `SUPER + [` / `]` / `=` | Shrink / expand / reset split |
|
||||||
| `SUPER + Q` | Close |
|
| `SUPER + Q` | Close |
|
||||||
| `SUPER + U` | Fullscreen |
|
| `SUPER + U` | Fullscreen |
|
||||||
@@ -256,7 +256,7 @@ The mental model is unchanged from Forge:
|
|||||||
| `SUPER + CTRL + G` / `Z` / `V` | Toggle split / preselect right / preselect down |
|
| `SUPER + CTRL + G` / `Z` / `V` | Toggle split / preselect right / preselect down |
|
||||||
| `SUPER + Tab` / `SHIFT + Tab` | Cycle windows |
|
| `SUPER + Tab` / `SHIFT + Tab` | Cycle windows |
|
||||||
| `SUPER + SHIFT + grave` | Last window |
|
| `SUPER + SHIFT + grave` | Last window |
|
||||||
| `SUPER + X` / `SUPER + SHIFT + X` | Restore scratchpad / minimise to scratchpad |
|
| `SUPER + X` / `SUPER + SHIFT + X` | Restore scratchpad / minimize to scratchpad |
|
||||||
|
|
||||||
### Workspaces (dynamic, like GNOME)
|
### Workspaces (dynamic, like GNOME)
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
@@ -270,19 +270,19 @@ The mental model is unchanged from Forge:
|
|||||||
### Launcher and shell
|
### Launcher and shell
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `SUPER + A` / `R` / `Space` | Launcher (vicinae) — all three, pick your favourite |
|
| `SUPER + A` / `R` / `Space` | Launcher (vicinae) — all three, pick your favorite |
|
||||||
| `SUPER + SHIFT + R` | Fallback launcher (wofi) if the shell is broken |
|
| `SUPER + SHIFT + R` | Fallback launcher (wofi) if the shell is broken |
|
||||||
| `SUPER + V` | Clipboard history |
|
| `SUPER + V` | Clipboard history |
|
||||||
| `SUPER + .` | Emoji picker |
|
| `SUPER + .` | Emoji picker |
|
||||||
| `SUPER + S` | Quick settings |
|
| `SUPER + S` | Quick settings |
|
||||||
| `SUPER + I` | Settings |
|
| `SUPER + I` | Settings |
|
||||||
| `SUPER + SHIFT + F` | Start or reveal focus session |
|
| `SUPER + SHIFT + F` | Start or reveal focus session |
|
||||||
| `SUPER + B` | Notification centre |
|
| `SUPER + B` | Notification center |
|
||||||
| `SUPER + grave` | Workspace overview |
|
| `SUPER + grave` | Workspace overview |
|
||||||
| `Print` | Screenshot / record picker |
|
| `Print` | Screenshot / record picker |
|
||||||
| `SHIFT` / `ALT + Print` | Screenshot screen / window immediately |
|
| `SHIFT` / `ALT + Print` | Screenshot screen / window immediately |
|
||||||
| `SUPER + SHIFT + S` | Screen Intelligence — read text and codes from a selection |
|
| `SUPER + SHIFT + S` | Screen Intelligence — read text and codes from a selection |
|
||||||
| `SUPER + SHIFT + P` | Colour picker |
|
| `SUPER + SHIFT + P` | Color picker |
|
||||||
| `CTRL + ALT + L` | Lock (SUPER+L is "focus right") |
|
| `CTRL + ALT + L` | Lock (SUPER+L is "focus right") |
|
||||||
| `CTRL + ALT + Delete` | Power menu |
|
| `CTRL + ALT + Delete` | Power menu |
|
||||||
|
|
||||||
@@ -325,8 +325,8 @@ Caffeine, Night Light, Focus, audio input/output, user, settings, and power in
|
|||||||
one place. Home and Phone continue the same surface rather than opening extra
|
one place. Home and Phone continue the same surface rather than opening extra
|
||||||
dashboard windows.
|
dashboard windows.
|
||||||
|
|
||||||
Home shows the first four selected favourites at rest and every selected light
|
Home shows the first four selected favorites at rest and every selected light
|
||||||
when expanded. Use **Settings → Home & Phone** to choose favourites,
|
when expanded. Use **Settings → Home & Phone** to choose favorites,
|
||||||
set Panama-only aliases, and arrange their order. Dragging a brightness control
|
set Panama-only aliases, and arrange their order. Dragging a brightness control
|
||||||
only previews the value; releasing it sends one brightness request. A normal
|
only previews the value; releasing it sends one brightness request. A normal
|
||||||
power toggle leaves Home Assistant responsible for restoring its previous
|
power toggle leaves Home Assistant responsible for restoring its previous
|
||||||
@@ -334,7 +334,7 @@ level.
|
|||||||
|
|
||||||
Credentials stay private in the gitignored `config/bash/env` file, with the
|
Credentials stay private in the gitignored `config/bash/env` file, with the
|
||||||
existing GNOME extension and Secret Service setup retained as a compatibility
|
existing GNOME extension and Secret Service setup retained as a compatibility
|
||||||
fallback. Favourites, aliases, and order live in Quickshell state. No shell
|
fallback. Favorites, aliases, and order live in Quickshell state. No shell
|
||||||
restart is required after changing credentials; close and reopen Control Center
|
restart is required after changing credentials; close and reopen Control Center
|
||||||
to refresh. If Home Assistant is offline, the last known values stay visible
|
to refresh. If Home Assistant is offline, the last known values stay visible
|
||||||
with a stale-state label and Retry action.
|
with a stale-state label and Retry action.
|
||||||
@@ -361,7 +361,7 @@ These are the places a 1:1 port was impossible, and what was done instead:
|
|||||||
|
|
||||||
- **Per-edge resize.** Forge resized one named edge; Hyprland resizes along an
|
- **Per-edge resize.** Forge resized one named edge; Hyprland resizes along an
|
||||||
axis and lets the layout pick the edge. The eight Forge keys collapse to four
|
axis and lets the layout pick the edge. The eight Forge keys collapse to four
|
||||||
behaviours, keeping the horizontal/vertical and grow/shrink pairing.
|
behaviors, keeping the horizontal/vertical and grow/shrink pairing.
|
||||||
- **Overview on `SUPER + grave`, not a bare SUPER tap.** Tap-detection on a
|
- **Overview on `SUPER + grave`, not a bare SUPER tap.** Tap-detection on a
|
||||||
modifier misfires when you're quick with SUPER combos.
|
modifier misfires when you're quick with SUPER combos.
|
||||||
- **Lock on `CTRL + ALT + L`.** `SUPER + L` is "focus right" in this keymap.
|
- **Lock on `CTRL + ALT + L`.** `SUPER + L` is "focus right" in this keymap.
|
||||||
|
|||||||
@@ -23,8 +23,16 @@ hl.on("hyprland.start", function()
|
|||||||
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=Hyprland")
|
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=Hyprland")
|
||||||
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||||
|
|
||||||
-- Units: polkit prompts, wallpaper, launcher daemon, idle/lock.
|
-- Units: polkit prompts, wallpaper, launcher daemon, idle/lock. All four
|
||||||
hl.exec_cmd("systemctl --user start hyprpolkitagent.service hyprpaper.service vicinae.service hypridle.service")
|
-- carry `ConditionEnvironment=WAYLAND_DISPLAY`, and hl.exec_cmd fires
|
||||||
|
-- commands without waiting for them to finish, so the dbus-update call
|
||||||
|
-- above racing this one is not safe to assume complete -- a lost race
|
||||||
|
-- leaves the Condition unmet and the unit silently never starts (exit 0,
|
||||||
|
-- no error). hypridle is the only listener for the logind Lock signal,
|
||||||
|
-- so that failure mode is "lock-session goes to nobody". Re-import
|
||||||
|
-- synchronously in the same shell invocation first so the Condition
|
||||||
|
-- always sees it, regardless of how the dbus-update call above scheduled.
|
||||||
|
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user start hyprpolkitagent.service hyprpaper.service vicinae.service hypridle.service")
|
||||||
|
|
||||||
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
||||||
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
||||||
@@ -39,18 +47,22 @@ hl.on("hyprland.start", function()
|
|||||||
-- Keyring unlock, for Nextcloud and Bitwarden credential storage.
|
-- Keyring unlock, for Nextcloud and Bitwarden credential storage.
|
||||||
hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=secrets,ssh,pkcs11")
|
hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=secrets,ssh,pkcs11")
|
||||||
|
|
||||||
-- Tray applications carried over from ~/.config/autostart. These need the
|
-- Nextcloud and Bitwarden are NOT started here. Both ship a
|
||||||
-- Quickshell tray (an SNI host) to be up, hence starting after it.
|
-- ~/.config/autostart/*.desktop entry, and systemd's own
|
||||||
|
-- systemd-xdg-autostart-generator turns every such entry into a
|
||||||
|
-- `PartOf=graphical-session.target` unit (`[email protected]`,
|
||||||
|
-- `[email protected]`) that fires once the uwsm
|
||||||
|
-- session brings up graphical-session.target -- confirmed live via
|
||||||
|
-- `systemctl --user list-units 'app-*@autostart.service'`. An explicit
|
||||||
|
-- second launch here used to duplicate that: for Bitwarden specifically,
|
||||||
|
-- each `flatpak run` gets its own sandbox instance, so the two starts
|
||||||
|
-- didn't just race, they left two competing processes fighting over the
|
||||||
|
-- app's single-instance lock, with neither reliably owning a usable
|
||||||
|
-- window. Trust the generator instead of re-launching.
|
||||||
--
|
--
|
||||||
-- Commands are copied verbatim from the GNOME .desktop files rather than
|
-- RustDesk is deliberately absent too: it ships an enabled *system*
|
||||||
-- guessed: Bitwarden is a flatpak with no `bitwarden` binary on PATH, and
|
-- service (`rustdesk --service`) that spawns --server and --tray for the
|
||||||
-- its autostart entry launches a specific script with --autostart.
|
-- session on its own. Starting it here as well would give you two trays.
|
||||||
--
|
|
||||||
-- RustDesk is deliberately absent: it ships an enabled *system* service
|
|
||||||
-- (`rustdesk --service`) that spawns --server and --tray for the session on
|
|
||||||
-- its own. Starting it here as well would give you two trays.
|
|
||||||
hl.exec_cmd("nextcloud --background")
|
|
||||||
hl.exec_cmd("flatpak run --command=bitwarden.sh com.bitwarden.desktop --autostart")
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
hl.on("hyprland.shutdown", function()
|
hl.on("hyprland.shutdown", function()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- Environment
|
-- Environment
|
||||||
--
|
--
|
||||||
-- hl.env() sets variables before the display server initialises, which is what
|
-- hl.env() sets variables before the display server initializes, which is what
|
||||||
-- makes it valid for AQ_* and cursor variables.
|
-- makes it valid for AQ_* and cursor variables.
|
||||||
--
|
--
|
||||||
-- NOTE for uwsm sessions: if you log in via "Hyprland (uwsm-managed)", uwsm
|
-- NOTE for uwsm sessions: if you log in via "Hyprland (uwsm-managed)", uwsm
|
||||||
@@ -58,7 +58,7 @@ hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
|||||||
|
|
||||||
-- "gtk3" makes Qt derive its palette from the live GTK3 theme via the qgtk3
|
-- "gtk3" makes Qt derive its palette from the live GTK3 theme via the qgtk3
|
||||||
-- platform plugin, so Qt apps match adw-gtk3-dark exactly rather than
|
-- platform plugin, so Qt apps match adw-gtk3-dark exactly rather than
|
||||||
-- approximating it -- and it gives them GTK file/font/colour dialogs. It also
|
-- approximating it -- and it gives them GTK file/font/color dialogs. It also
|
||||||
-- covers Qt5 and Qt6 with one value.
|
-- covers Qt5 and Qt6 with one value.
|
||||||
--
|
--
|
||||||
-- Note this is a single value, NOT a fallback list: Qt splits on ":" and uses
|
-- Note this is a single value, NOT a fallback list: Qt splits on ":" and uses
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# hypridle — idle behaviour
|
# hypridle — idle behavior
|
||||||
#
|
#
|
||||||
# Mirrors the GNOME power profile this replaces, exactly:
|
# Mirrors the GNOME power profile this replaces, exactly:
|
||||||
# org.gnome.desktop.session idle-delay = 300 (screen off at 5min)
|
# org.gnome.desktop.session idle-delay = 300 (screen off at 5min)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
-- Shared preferences first: looks/input/monitors read their defaults through it.
|
-- Shared preferences first: looks/input/monitors read their defaults through it.
|
||||||
-- It never raises, so a missing or malformed settings file costs customisations
|
-- It never raises, so a missing or malformed settings file costs customizations
|
||||||
-- and nothing else.
|
-- and nothing else.
|
||||||
require("prefs")
|
require("prefs")
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
#
|
#
|
||||||
# GENERATED FILE. Edit hyprlock.conf.template and re-run
|
# GENERATED FILE. Edit hyprlock.conf.template and re-run
|
||||||
# quickshell/scripts/panama-theme-apps; editing this copy is overwritten on the
|
# quickshell/scripts/panama-theme-apps; editing this copy is overwritten on the
|
||||||
# next colour scheme change.
|
# next color scheme change.
|
||||||
#
|
#
|
||||||
# The colours here follow the desktop's light/dark setting. They used to be
|
# The colors here follow the desktop's light/dark setting. They used to be
|
||||||
# hardcoded Tokyo Night Moon, which meant the one screen you see most often
|
# hardcoded Tokyo Night Moon, which meant the one screen you see most often
|
||||||
# stayed dark when everything else went light.
|
# stayed dark when everything else went light.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# hyprland-guiutils hyprland-dialog / hyprland-run
|
# hyprland-guiutils hyprland-dialog / hyprland-run
|
||||||
# hyprpolkitagent authentication prompts (via hyprland-qt-support)
|
# hyprpolkitagent authentication prompts (via hyprland-qt-support)
|
||||||
#
|
#
|
||||||
# Colours are Tokyo Night Moon, matching quickshell/config/Theme.qml and
|
# Colors are Tokyo Night Moon, matching quickshell/config/Theme.qml and
|
||||||
# hypr/looks.lua. Format is hyprlang; path is fixed at
|
# hypr/looks.lua. Format is hyprlang; path is fixed at
|
||||||
# $XDG_CONFIG_HOME/hypr/hyprtoolkit.conf (confirmed in libhyprtoolkit).
|
# $XDG_CONFIG_HOME/hypr/hyprtoolkit.conf (confirmed in libhyprtoolkit).
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ hl.config({
|
|||||||
repeat_rate = prefs.get("keyRepeatRate", 33),
|
repeat_rate = prefs.get("keyRepeatRate", 33),
|
||||||
|
|
||||||
-- 1 = FOLLOW. The window under the pointer takes focus. This comment
|
-- 1 = FOLLOW. The window under the pointer takes focus. This comment
|
||||||
-- previously claimed 1 was "click to focus, GNOME's behaviour", which
|
-- previously claimed 1 was "click to focus, GNOME's behavior", which
|
||||||
-- is the opposite of what Hyprland does -- `hyprctl descriptions` gives
|
-- is the opposite of what Hyprland does -- `hyprctl descriptions` gives
|
||||||
-- map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}],
|
-- map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}],
|
||||||
-- so click-to-focus is 0. Changing the shipped value is a behaviour
|
-- so click-to-focus is 0. Changing the shipped value is a behavior
|
||||||
-- decision rather than a correction, so the value is left alone and
|
-- decision rather than a correction, so the value is left alone and
|
||||||
-- only the description is fixed; Settings exposes all four.
|
-- only the description is fixed; Settings exposes all four.
|
||||||
follow_mouse = prefs.getInt("followMouse", 1),
|
follow_mouse = prefs.getInt("followMouse", 1),
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd(qs("focus", "reveal")), { descriptio
|
|||||||
-- which Hyprland has no equivalent for.
|
-- which Hyprland has no equivalent for.
|
||||||
bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" })
|
bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" })
|
||||||
|
|
||||||
-- Notification centre.
|
-- Notification center.
|
||||||
bind(mod .. " + B", hl.dsp.exec_cmd(qs("notifications", "toggle")), { description = "Notifications" })
|
bind(mod .. " + B", hl.dsp.exec_cmd(qs("notifications", "toggle")), { description = "Notifications" })
|
||||||
|
|
||||||
-- Screenshot / screen record. One key, then pick screen / window / region and
|
-- Screenshot / screen record. One key, then pick screen / window / region and
|
||||||
@@ -133,8 +133,8 @@ bind("ALT + Print", hl.dsp.exec_cmd(qs("capture", "windowNow")), { description =
|
|||||||
bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd(qs("screen-intelligence", "open")),
|
bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd(qs("screen-intelligence", "open")),
|
||||||
{ description = "Screen Intelligence" })
|
{ description = "Screen Intelligence" })
|
||||||
|
|
||||||
-- Colour picker: copies the hex under the cursor to the clipboard.
|
-- Color picker: copies the hex under the cursor to the clipboard.
|
||||||
bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Colour picker" })
|
bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Color picker" })
|
||||||
|
|
||||||
-- ── Window management ───────────────────────────────────────────────────────
|
-- ── Window management ───────────────────────────────────────────────────────
|
||||||
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||||
@@ -182,7 +182,7 @@ bind(mod .. " + CTRL + L", hl.dsp.window.swap({ direction = "r" }), { descriptio
|
|||||||
--
|
--
|
||||||
-- Forge resized one named edge at a time. Hyprland resizes the active window
|
-- Forge resized one named edge at a time. Hyprland resizes the active window
|
||||||
-- along an axis and lets the layout decide which edge actually moves, so the
|
-- along an axis and lets the layout decide which edge actually moves, so the
|
||||||
-- eight Forge keys collapse onto four behaviours. The pairing is kept
|
-- eight Forge keys collapse onto four behaviors. The pairing is kept
|
||||||
-- consistent with the original: Y/B/O/M are horizontal, I/P/U/N are vertical,
|
-- consistent with the original: Y/B/O/M are horizontal, I/P/U/N are vertical,
|
||||||
-- and "increase" always grows while "decrease" always shrinks.
|
-- and "increase" always grows while "decrease" always shrinks.
|
||||||
local step = 60
|
local step = 60
|
||||||
@@ -192,7 +192,10 @@ bind(mod .. " + SHIFT + B", hl.dsp.window.resize({ x = -step, y = 0, relative =
|
|||||||
bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||||
bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||||
bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||||
bind(mod .. " + SHIFT + P", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
-- SUPER+SHIFT+P was double-bound with the color picker above; moved to
|
||||||
|
-- Comma, which continues the bottom-row cluster (B/M/N) this axis already
|
||||||
|
-- uses rather than landing on an arbitrary free key.
|
||||||
|
bind(mod .. " + SHIFT + Comma", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||||
bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||||
|
|
||||||
-- Window cycling (GNOME: cycle-windows on SUPER+Tab), now with an overlay
|
-- Window cycling (GNOME: cycle-windows on SUPER+Tab), now with an overlay
|
||||||
@@ -223,6 +226,17 @@ bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true, description
|
|||||||
-- Plain relative selectors ("+1" / "-1") reproduce GNOME's dynamic workspaces:
|
-- Plain relative selectors ("+1" / "-1") reproduce GNOME's dynamic workspaces:
|
||||||
-- moving right past the last workspace creates a new one, and moving left from
|
-- moving right past the last workspace creates a new one, and moving left from
|
||||||
-- the first clamps instead of wrapping.
|
-- the first clamps instead of wrapping.
|
||||||
|
|
||||||
|
-- Behavior for the relative/cyclic binds below. These are Hyprland's own
|
||||||
|
-- `binds:` options -- not part of general/dwindle -- and have no other home
|
||||||
|
-- in the config, so they are read here rather than in looks.lua.
|
||||||
|
hl.config({
|
||||||
|
binds = {
|
||||||
|
workspace_back_and_forth = prefs.get("workspaceBackAndForth", false),
|
||||||
|
allow_workspace_cycles = prefs.get("allowWorkspaceCycles", false),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
bind("ALT + H", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
bind("ALT + H", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||||
bind("ALT + L", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
bind("ALT + L", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||||
bind("ALT + SHIFT + H", hl.dsp.window.move({ workspace = "-1" }), { description = "Move window to workspace left" })
|
bind("ALT + SHIFT + H", hl.dsp.window.move({ workspace = "-1" }), { description = "Move window to workspace left" })
|
||||||
@@ -245,18 +259,18 @@ end
|
|||||||
bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||||
bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||||
|
|
||||||
-- Minimise, as far as Hyprland has one.
|
-- Minimize, as far as Hyprland has one.
|
||||||
--
|
--
|
||||||
-- Hyprland has no minimise: it receives the request (the binary has
|
-- Hyprland has no minimize: it receives the request (the binary has
|
||||||
-- setSetMinimized handlers for xdg, XWayland and foreign-toplevel) but exposes
|
-- setSetMinimized handlers for xdg, XWayland and foreign-toplevel) but exposes
|
||||||
-- no dispatcher, no config option and not even an event to hook, so titlebar
|
-- no dispatcher, no config option and not even an event to hook, so titlebar
|
||||||
-- minimise buttons are inert and cannot be made to work. A tiling WM has no
|
-- minimize buttons are inert and cannot be made to work. A tiling WM has no
|
||||||
-- iconified state and no taskbar to restore from.
|
-- iconified state and no taskbar to restore from.
|
||||||
--
|
--
|
||||||
-- The scratchpad is the honest equivalent: the window goes away, and the same
|
-- The scratchpad is the honest equivalent: the window goes away, and the same
|
||||||
-- key brings it back. Bound to X to match the muscle memory it replaces.
|
-- key brings it back. Bound to X to match the muscle memory it replaces.
|
||||||
bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimised)" })
|
bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimized)" })
|
||||||
bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimise to scratchpad" })
|
bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimize to scratchpad" })
|
||||||
|
|
||||||
-- ── Session ─────────────────────────────────────────────────────────────────
|
-- ── Session ─────────────────────────────────────────────────────────────────
|
||||||
-- GNOME's lock was SUPER+L, which is "focus right" here, so lock moves to
|
-- GNOME's lock was SUPER+L, which is "focus right" here, so lock moves to
|
||||||
|
|||||||
+39
-11
@@ -1,8 +1,8 @@
|
|||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- Look and feel -- Tokyo Night Moon
|
-- Look and feel -- Tokyo Night Moon
|
||||||
--
|
--
|
||||||
-- Colours here must stay in sync with quickshell/config/Theme.qml.
|
-- Colors here must stay in sync with quickshell/config/Theme.qml.
|
||||||
-- accent #82aaff borders / focus
|
-- accent user-selectable, see `accents` below -- blue (#82aaff) ships
|
||||||
-- bg #222436 base
|
-- bg #222436 base
|
||||||
--
|
--
|
||||||
-- Performance note: every animation below is event-driven. Nothing uses the
|
-- Performance note: every animation below is event-driven. Nothing uses the
|
||||||
@@ -14,6 +14,30 @@
|
|||||||
|
|
||||||
local prefs = require("prefs")
|
local prefs = require("prefs")
|
||||||
|
|
||||||
|
-- Mirrors config/Theme.qml's `accents` map. Lua has no import path into a QML
|
||||||
|
-- singleton, so the hex pairs are restated here -- just the four hex strings
|
||||||
|
-- each named accent needs, not the labels, which stay UI-only.
|
||||||
|
local accents = {
|
||||||
|
blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
|
||||||
|
orchid = { dark = "c099ff", darkSecondary = "fca7ea", light = "7847bd", lightSecondary = "9854f1" },
|
||||||
|
teal = { dark = "86e1fc", darkSecondary = "82aaff", light = "007197", lightSecondary = "2e7de9" },
|
||||||
|
green = { dark = "c3e88d", darkSecondary = "86e1fc", light = "587539", lightSecondary = "007197" },
|
||||||
|
amber = { dark = "ffc777", darkSecondary = "ff966c", light = "8c6c3e", lightSecondary = "b15c00" },
|
||||||
|
orange = { dark = "ff966c", darkSecondary = "ff757f", light = "b15c00", lightSecondary = "c64343" },
|
||||||
|
rose = { dark = "ff757f", darkSecondary = "c099ff", light = "f52a65", lightSecondary = "9854f1" },
|
||||||
|
slate = { dark = "828bb8", darkSecondary = "82aaff", light = "6172b0", lightSecondary = "2e7de9" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- The accent pair a fresh session or `hyprctl reload` starts from.
|
||||||
|
-- services/ColorScheme.qml overwrites this live, from the same Theme.accents
|
||||||
|
-- data, once the shell settles (~1.2s after startup -- see its `settle`
|
||||||
|
-- Timer). This table exists only so the compositor is never observably blue
|
||||||
|
-- for a non-blue accent during the gap before that first live apply.
|
||||||
|
local accentScheme = prefs.get("colorScheme", "dark")
|
||||||
|
local accentPair = accents[prefs.get("accentName", "blue")] or accents.blue
|
||||||
|
local accentStart = accentScheme == "light" and accentPair.light or accentPair.dark
|
||||||
|
local accentEnd = accentScheme == "light" and accentPair.lightSecondary or accentPair.darkSecondary
|
||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
general = {
|
general = {
|
||||||
gaps_in = prefs.get("gapsIn", 5),
|
gaps_in = prefs.get("gapsIn", 5),
|
||||||
@@ -22,12 +46,14 @@ hl.config({
|
|||||||
border_size = prefs.get("borderSize", 2),
|
border_size = prefs.get("borderSize", 2),
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
-- The focused accent role: blue leads, orchid follows, on a
|
-- The focused accent role, on a diagonal so the pair is visible
|
||||||
-- diagonal so the pair is visible on both a tall and a wide
|
-- on both a tall and a wide window. Driven by the chosen
|
||||||
-- window. ColorScheme never writes this role; a future accent
|
-- accentName (see the `accents` table above); services/
|
||||||
-- picker can own it without fighting light/dark mode.
|
-- ColorScheme.qml applies the same values live, and restates them
|
||||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
-- from Theme.accent/Theme.accentSecondary on every scheme change
|
||||||
-- The neutral inactive role follows the colour scheme because a
|
-- too, since each accent carries a separate pair per scheme.
|
||||||
|
active_border = { colors = { "rgba(" .. accentStart .. "ee)", "rgba(" .. accentEnd .. "ee)" }, angle = 115 },
|
||||||
|
-- The neutral inactive role follows the color scheme because a
|
||||||
-- dark neutral disappears against a light desktop.
|
-- dark neutral disappears against a light desktop.
|
||||||
-- services/ColorScheme.qml applies the same values live; this is
|
-- services/ColorScheme.qml applies the same values live; this is
|
||||||
-- the value a fresh session starts from.
|
-- the value a fresh session starts from.
|
||||||
@@ -104,11 +130,13 @@ hl.config({
|
|||||||
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
||||||
-- border is the signature, and a strong halo would compete with it.
|
-- border is the signature, and a strong halo would compete with it.
|
||||||
-- This is just enough to lift the focused window off the wallpaper.
|
-- This is just enough to lift the focused window off the wallpaper.
|
||||||
|
-- Derived from the same accent as active_border above, not hardcoded,
|
||||||
|
-- so the halo never disagrees with the border it surrounds.
|
||||||
glow = {
|
glow = {
|
||||||
enabled = prefs.get("glowEnabled", true),
|
enabled = prefs.get("glowEnabled", true),
|
||||||
range = prefs.get("glowRange", 8),
|
range = prefs.get("glowRange", 8),
|
||||||
render_power = 2,
|
render_power = 2,
|
||||||
color = "rgba(82aaff33)",
|
color = "rgba(" .. accentStart .. "33)",
|
||||||
color_inactive = "rgba(00000000)",
|
color_inactive = "rgba(00000000)",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -165,12 +193,12 @@ hl.config({
|
|||||||
-- Let a crashed lock screen be recovered rather than stranding you.
|
-- Let a crashed lock screen be recovered rather than stranding you.
|
||||||
allow_session_lock_restore = true,
|
allow_session_lock_restore = true,
|
||||||
|
|
||||||
-- Don't let apps steal focus by shouting; matches GNOME's behaviour.
|
-- Don't let apps steal focus by shouting; matches GNOME's behavior.
|
||||||
focus_on_activate = false,
|
focus_on_activate = false,
|
||||||
},
|
},
|
||||||
|
|
||||||
render = {
|
render = {
|
||||||
-- Colour management is stable and on by default in 0.56.
|
-- Color management is stable and on by default in 0.56.
|
||||||
cm_enabled = true,
|
cm_enabled = true,
|
||||||
|
|
||||||
-- 1 = automatically flip the monitor into HDR for fullscreen content
|
-- 1 = automatically flip the monitor into HDR for fullscreen content
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ local prefs = require("prefs")
|
|||||||
-- x = 0, y = 0, primary = true,
|
-- x = 0, y = 0, primary = true,
|
||||||
-- } }
|
-- } }
|
||||||
--
|
--
|
||||||
-- Only mode, scale, and transform are read. Colour management and bit depth
|
-- Only mode, scale, and transform are read. Color management and bit depth
|
||||||
-- stay here, because those are the settings with a documented reason attached
|
-- stay here, because those are the settings with a documented reason attached
|
||||||
-- (see the header) rather than preferences, and a settings page has no way to
|
-- (see the header) rather than preferences, and a settings page has no way to
|
||||||
-- explain the screencopy tradeoff at the moment you would be changing it.
|
-- explain the screencopy tradeoff at the moment you would be changing it.
|
||||||
@@ -147,7 +147,7 @@ hl.monitor({
|
|||||||
})
|
})
|
||||||
|
|
||||||
-- Other connected outputs use the same validated per-output store. They keep
|
-- Other connected outputs use the same validated per-output store. They keep
|
||||||
-- automatic placement and the compositor's normal colour policy; DP-2 alone
|
-- automatic placement and the compositor's normal color policy; DP-2 alone
|
||||||
-- carries the panel-specific 10-bit policy documented above.
|
-- carries the panel-specific 10-bit policy documented above.
|
||||||
for output, _ in pairs(displays) do
|
for output, _ in pairs(displays) do
|
||||||
if output ~= "DP-2" then
|
if output ~= "DP-2" then
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
-- cannot drift apart.
|
-- cannot drift apart.
|
||||||
--
|
--
|
||||||
-- Nothing here may raise. A missing, empty, truncated, or actively malformed
|
-- Nothing here may raise. A missing, empty, truncated, or actively malformed
|
||||||
-- file must cost the user nothing worse than their customisations; it must
|
-- file must cost the user nothing worse than their customizations; it must
|
||||||
-- never cost them a working compositor. Every failure path returns the caller's
|
-- never cost them a working compositor. Every failure path returns the caller's
|
||||||
-- fallback.
|
-- fallback.
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ hl.window_rule({
|
|||||||
float = true,
|
float = true,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Settings-style utility windows and pickers: float and centre, like GNOME did.
|
-- Settings-style utility windows and pickers: float and center, like GNOME did.
|
||||||
hl.window_rule({
|
hl.window_rule({
|
||||||
match = { class = "^(pavucontrol|org\\.pulseaudio\\.pavucontrol|nm-connection-editor|blueman-manager|org\\.gnome\\.Settings)$" },
|
match = { class = "^(pavucontrol|org\\.pulseaudio\\.pavucontrol|nm-connection-editor|blueman-manager|org\\.gnome\\.Settings)$" },
|
||||||
float = true,
|
float = true,
|
||||||
@@ -150,7 +150,7 @@ hl.layer_rule({
|
|||||||
ignore_alpha = 0.3,
|
ignore_alpha = 0.3,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Popovers: calendar, quick settings, notification centre, tray menus.
|
-- Popovers: calendar, quick settings, notification center, tray menus.
|
||||||
hl.layer_rule({
|
hl.layer_rule({
|
||||||
name = "qs-popover",
|
name = "qs-popover",
|
||||||
match = { namespace = "^qs-popover" },
|
match = { namespace = "^qs-popover" },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Tokyo Night Moon — the dark theme Panama ships.
|
# Tokyo Night Moon — the dark theme Panama ships.
|
||||||
#
|
#
|
||||||
# Extracted from kitty.conf so the two schemes can be swapped. kitty.conf
|
# Extracted from kitty.conf so the two schemes can be swapped. kitty.conf
|
||||||
# includes current-theme.conf, which Panama generates from the colour scheme
|
# includes current-theme.conf, which Panama generates from the color scheme
|
||||||
# setting; that generated file is gitignored because it is machine state.
|
# setting; that generated file is gitignored because it is machine state.
|
||||||
#
|
#
|
||||||
# Live changes go through `kitty @ set-colors`, which is why kitty.conf enables
|
# Live changes go through `kitty @ set-colors`, which is why kitty.conf enables
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
-- Hyprland config read.
|
-- Hyprland config read.
|
||||||
--
|
--
|
||||||
-- Nothing here may raise. A missing or malformed settings file must cost the
|
-- Nothing here may raise. A missing or malformed settings file must cost the
|
||||||
-- user their colour scheme preference and nothing else; editing text is more
|
-- user their color scheme preference and nothing else; editing text is more
|
||||||
-- important than matching the desktop.
|
-- important than matching the desktop.
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
-- Tokyo Night, following the desktop's colour scheme.
|
-- Tokyo Night, following the desktop's color scheme.
|
||||||
--
|
--
|
||||||
-- Moon when Panama is dark, Day when it is light. Same theme family either way,
|
-- Moon when Panama is dark, Day when it is light. Same theme family either way,
|
||||||
-- so the editor keeps the identity the rest of the desktop has rather than
|
-- so the editor keeps the identity the rest of the desktop has rather than
|
||||||
-- becoming a different-looking application when the scheme flips.
|
-- becoming a different-looking application when the scheme flips.
|
||||||
--
|
--
|
||||||
-- The readability fixes below are deliberately dark-only. They were written
|
-- The readability fixes below are deliberately dark-only. They were written
|
||||||
-- against Moon's palette -- a pale comment colour, a mid-grey gutter -- and
|
-- against Moon's palette -- a pale comment color, a mid-gray gutter -- and
|
||||||
-- applying them to Day would put light grey text on a light background, which
|
-- applying them to Day would put light gray text on a light background, which
|
||||||
-- is exactly the legibility problem they exist to solve, inverted.
|
-- is exactly the legibility problem they exist to solve, inverted.
|
||||||
|
|
||||||
local panama = require("config.panama")
|
local panama = require("config.panama")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ console_title_template = '{{ .Shell }} in {{ .Folder }}'
|
|||||||
[[blocks.segments]]
|
[[blocks.segments]]
|
||||||
type = 'git'
|
type = 'git'
|
||||||
style = 'plain'
|
style = 'plain'
|
||||||
foreground = 'p:grey'
|
foreground = 'p:gray'
|
||||||
background = 'transparent'
|
background = 'transparent'
|
||||||
template = ' {{ .HEAD }}{{ if or (.Working.Changed) (.Staging.Changed) }}*{{ end }} <cyan>{{ if gt .Behind 0 }}⇣{{ end }}{{ if gt .Ahead 0 }}⇡{{ end }}</>'
|
template = ' {{ .HEAD }}{{ if or (.Working.Changed) (.Staging.Changed) }}*{{ end }} <cyan>{{ if gt .Behind 0 }}⇣{{ end }}{{ if gt .Ahead 0 }}⇡{{ end }}</>'
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Singleton {
|
|||||||
const raw = (parsed && typeof parsed === "object") ? parsed : {};
|
const raw = (parsed && typeof parsed === "object") ? parsed : {};
|
||||||
|
|
||||||
// Upgrade before anything reads a value. A stored key the current
|
// Upgrade before anything reads a value. A stored key the current
|
||||||
// schema no longer recognises is carried through untouched and silently
|
// schema no longer recognizes is carried through untouched and silently
|
||||||
// stops taking effect, so the conversion has to happen here rather than
|
// stops taking effect, so the conversion has to happen here rather than
|
||||||
// being noticed later by whoever owns that setting.
|
// being noticed later by whoever owns that setting.
|
||||||
const result = Migrations.apply(raw);
|
const result = Migrations.apply(raw);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Singleton {
|
|||||||
printErrors: false
|
printErrors: false
|
||||||
atomicWrites: true
|
atomicWrites: true
|
||||||
onSaved: root.saveError = ""
|
onSaved: root.saveError = ""
|
||||||
onSaveFailed: error => root.saveError = "Could not save Home favourites."
|
onSaveFailed: error => root.saveError = "Could not save Home favorites."
|
||||||
|
|
||||||
JsonAdapter {
|
JsonAdapter {
|
||||||
id: values
|
id: values
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ pragma Singleton
|
|||||||
// The schema is the single source of truth for what a setting IS, but it cannot
|
// The schema is the single source of truth for what a setting IS, but it cannot
|
||||||
// describe what a setting USED to be. Renaming a key, changing its units, or
|
// describe what a setting USED to be. Renaming a key, changing its units, or
|
||||||
// splitting one setting into two all leave a stored value that the new schema
|
// splitting one setting into two all leave a stored value that the new schema
|
||||||
// does not recognise -- and an unrecognised key is silently carried through
|
// does not recognize -- and an unrecognized key is silently carried through
|
||||||
// untouched, so the user's choice simply stops taking effect with nothing to
|
// untouched, so the user's choice simply stops taking effect with nothing to
|
||||||
// say why. That is the failure this exists to prevent.
|
// say why. That is the failure this exists to prevent.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ Singleton {
|
|||||||
unit: "px",
|
unit: "px",
|
||||||
group: "windows",
|
group: "windows",
|
||||||
label: "Inner gaps",
|
label: "Inner gaps",
|
||||||
detail: "Space between neighbouring tiled windows",
|
detail: "Space between neighboring tiled windows",
|
||||||
hypr: { path: ["general", "gaps_in"], option: "general:gaps_in", readAs: "css" }
|
hypr: { path: ["general", "gaps_in"], option: "general:gaps_in", readAs: "css" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -278,7 +278,7 @@ Singleton {
|
|||||||
// ── Master layout ───────────────────────────────────────────────────
|
// ── Master layout ───────────────────────────────────────────────────
|
||||||
// Only meaningful when the tiling layout is Master and stack. Offering
|
// Only meaningful when the tiling layout is Master and stack. Offering
|
||||||
// that layout with none of its options was an omission: it is the one
|
// that layout with none of its options was an omission: it is the one
|
||||||
// layout whose whole behaviour is in these settings.
|
// layout whose whole behavior is in these settings.
|
||||||
{
|
{
|
||||||
key: "masterFactor", type: "real", def: 0.55, min: 0.1, max: 0.9, step: 0.05,
|
key: "masterFactor", type: "real", def: 0.55, min: 0.1, max: 0.9, step: 0.05,
|
||||||
group: "master",
|
group: "master",
|
||||||
@@ -295,7 +295,7 @@ Singleton {
|
|||||||
{ value: "right", label: "Right" },
|
{ value: "right", label: "Right" },
|
||||||
{ value: "top", label: "Top" },
|
{ value: "top", label: "Top" },
|
||||||
{ value: "bottom", label: "Bottom" },
|
{ value: "bottom", label: "Bottom" },
|
||||||
{ value: "center", label: "Centre" }
|
{ value: "center", label: "Center" }
|
||||||
],
|
],
|
||||||
hypr: { path: ["master", "orientation"], option: "master:orientation", readAs: "str" }
|
hypr: { path: ["master", "orientation"], option: "master:orientation", readAs: "str" }
|
||||||
},
|
},
|
||||||
@@ -429,7 +429,7 @@ Singleton {
|
|||||||
{
|
{
|
||||||
key: "keyboardVariant", type: "string", def: "", group: "input",
|
key: "keyboardVariant", type: "string", def: "", group: "input",
|
||||||
// Same shape as the layout list and for the same reason: this is
|
// Same shape as the layout list and for the same reason: this is
|
||||||
// serialised into an hl.config string.
|
// serialized into an hl.config string.
|
||||||
pattern: "^$|^[a-z0-9_]{1,24}(,[a-z0-9_]{1,24})*$",
|
pattern: "^$|^[a-z0-9_]{1,24}(,[a-z0-9_]{1,24})*$",
|
||||||
label: "Layout variant",
|
label: "Layout variant",
|
||||||
detail: "XKB variant, such as dvorak or colemak. Empty for the standard layout",
|
detail: "XKB variant, such as dvorak or colemak. Empty for the standard layout",
|
||||||
@@ -475,7 +475,7 @@ Singleton {
|
|||||||
// The compositor publishes the authoritative mapping itself --
|
// The compositor publishes the authoritative mapping itself --
|
||||||
// `hyprctl descriptions` gives
|
// `hyprctl descriptions` gives
|
||||||
// map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}]
|
// map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}]
|
||||||
// -- so a desktop labelled "Click to focus" was in fact following
|
// -- so a desktop labeled "Click to focus" was in fact following
|
||||||
// the pointer, and the way to actually get click-to-focus was to
|
// the pointer, and the way to actually get click-to-focus was to
|
||||||
// choose "Never". Value 3 was missing entirely.
|
// choose "Never". Value 3 was missing entirely.
|
||||||
//
|
//
|
||||||
@@ -852,14 +852,14 @@ Singleton {
|
|||||||
detail: "A time earlier than the start simply means the next morning"
|
detail: "A time earlier than the start simply means the next morning"
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Colour scheme ───────────────────────────────────────────────────
|
// ── Color scheme ───────────────────────────────────────────────────
|
||||||
// Light is Tokyo Night Day, the official light variant, rather than a
|
// Light is Tokyo Night Day, the official light variant, rather than a
|
||||||
// palette invented to merely not be dark. Both share the same hues at
|
// palette invented to merely not be dark. Both share the same hues at
|
||||||
// different lightness, which is what keeps the Prism identity intact
|
// different lightness, which is what keeps the Prism identity intact
|
||||||
// across the switch.
|
// across the switch.
|
||||||
//
|
//
|
||||||
// services/ColorScheme.qml pushes the choice to GTK and to the
|
// services/ColorScheme.qml pushes the choice to GTK and to the
|
||||||
// compositor's border colours, because an application toolbar or a
|
// compositor's border colors, because an application toolbar or a
|
||||||
// window border still wearing the other scheme is more jarring than
|
// window border still wearing the other scheme is more jarring than
|
||||||
// either scheme on its own.
|
// either scheme on its own.
|
||||||
{
|
{
|
||||||
@@ -871,6 +871,26 @@ Singleton {
|
|||||||
{ value: "light", label: "Light" }
|
{ value: "light", label: "Light" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "accentName", type: "enum", def: "blue", group: "appearance",
|
||||||
|
label: "Accent color",
|
||||||
|
detail: "Drives the focused window border, the bar hairline, and every active state",
|
||||||
|
// NAMED accents, not a free color. Each name carries a curated
|
||||||
|
// pair per scheme, because one hex cannot serve both: a color
|
||||||
|
// legible on the dark ground is usually illegible on the light one.
|
||||||
|
// The palette and its measured contrast live in config/Theme.qml,
|
||||||
|
// which is also what stops this list drifting from what is drawn.
|
||||||
|
options: [
|
||||||
|
{ value: "blue", label: "Prism blue" },
|
||||||
|
{ value: "orchid", label: "Orchid" },
|
||||||
|
{ value: "teal", label: "Teal" },
|
||||||
|
{ value: "green", label: "Green" },
|
||||||
|
{ value: "amber", label: "Amber" },
|
||||||
|
{ value: "orange", label: "Orange" },
|
||||||
|
{ value: "rose", label: "Rose" },
|
||||||
|
{ value: "slate", label: "Slate" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// ── Application themes ─────────────────────────────────────────────
|
// ── Application themes ─────────────────────────────────────────────
|
||||||
// ColorScheme owns GTK's light/dark theme. These are the two theme
|
// ColorScheme owns GTK's light/dark theme. These are the two theme
|
||||||
@@ -978,7 +998,7 @@ Singleton {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── Application titlebars ──────────────────────────────────────────
|
// ── Application titlebars ──────────────────────────────────────────
|
||||||
// These affect applications that honour GNOME's window preferences.
|
// These affect applications that honor GNOME's window preferences.
|
||||||
// Hyprland itself has no server-side titlebar buttons, so minimize is
|
// Hyprland itself has no server-side titlebar buttons, so minimize is
|
||||||
// deliberately absent rather than presented as a switch that lies.
|
// deliberately absent rather than presented as a switch that lies.
|
||||||
{
|
{
|
||||||
@@ -1041,7 +1061,7 @@ Singleton {
|
|||||||
// ── Which GPU the vitals readout tracks ─────────────────────────────
|
// ── Which GPU the vitals readout tracks ─────────────────────────────
|
||||||
// A sysfs path rather than a card number, because the number is neither
|
// A sysfs path rather than a card number, because the number is neither
|
||||||
// stable across machines nor meaningful. Constrained to the one shape
|
// stable across machines nor meaningful. Constrained to the one shape
|
||||||
// that can be read for utilisation; VitalsWidget hides itself when the
|
// that can be read for utilization; VitalsWidget hides itself when the
|
||||||
// path is unreadable, so a stale value degrades to no readout rather
|
// path is unreadable, so a stale value degrades to no readout rather
|
||||||
// than a wrong one.
|
// than a wrong one.
|
||||||
{
|
{
|
||||||
@@ -1057,7 +1077,7 @@ Singleton {
|
|||||||
// Coordinates rather than a place name, because that is what Open-Meteo
|
// Coordinates rather than a place name, because that is what Open-Meteo
|
||||||
// takes and it needs no API key. weatherLocation is only the label shown
|
// takes and it needs no API key. weatherLocation is only the label shown
|
||||||
// in the UI; it is never sent anywhere, so it can say whatever makes the
|
// in the UI; it is never sent anywhere, so it can say whatever makes the
|
||||||
// reading recognisable.
|
// reading recognizable.
|
||||||
{
|
{
|
||||||
key: "weatherLatitude", type: "real", def: 27.7375, min: -90, max: 90, step: 0.0001,
|
key: "weatherLatitude", type: "real", def: 27.7375, min: -90, max: 90, step: 0.0001,
|
||||||
group: "weather", internal: true,
|
group: "weather", internal: true,
|
||||||
@@ -1192,7 +1212,7 @@ Singleton {
|
|||||||
|
|
||||||
// ── Display configuration ───────────────────────────────────────────
|
// ── Display configuration ───────────────────────────────────────────
|
||||||
// { "<output>": { mode, scale, transform, x, y, primary } }, applied by
|
// { "<output>": { mode, scale, transform, x, y, primary } }, applied by
|
||||||
// hypr/monitors.lua on top of the shipped values. Colour management and
|
// hypr/monitors.lua on top of the shipped values. Color management and
|
||||||
// bit depth are deliberately not here: those carry a documented
|
// bit depth are deliberately not here: those carry a documented
|
||||||
// screencopy tradeoff that a settings page cannot explain at the moment
|
// screencopy tradeoff that a settings page cannot explain at the moment
|
||||||
// you would be changing it.
|
// you would be changing it.
|
||||||
@@ -1305,7 +1325,7 @@ Singleton {
|
|||||||
case "string": {
|
case "string": {
|
||||||
const text = typeof value === "string" ? value : String(value);
|
const text = typeof value === "string" ? value : String(value);
|
||||||
// A constrained string is rejected rather than sanitised. Several
|
// A constrained string is rejected rather than sanitised. Several
|
||||||
// of these are serialised into an hl.config payload, and quietly
|
// of these are serialized into an hl.config payload, and quietly
|
||||||
// stripping characters would turn a typo into a different setting
|
// stripping characters would turn a typo into a different setting
|
||||||
// instead of an error the user can see.
|
// instead of an error the user can see.
|
||||||
if (entry.pattern && !new RegExp(entry.pattern).test(text))
|
if (entry.pattern && !new RegExp(entry.pattern).test(text))
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Singleton {
|
|||||||
readonly property int focusDurationMinutes: DesktopPreferences.get("focusDurationMinutes")
|
readonly property int focusDurationMinutes: DesktopPreferences.get("focusDurationMinutes")
|
||||||
|
|
||||||
// ── Dock ────────────────────────────────────────────────────────────────
|
// ── Dock ────────────────────────────────────────────────────────────────
|
||||||
// Pinned apps, in order, taken from the GNOME dash favourites.
|
// Pinned apps, in order, taken from the GNOME dash favorites.
|
||||||
readonly property var dockPinned: DesktopPreferences.get("dockPinned")
|
readonly property var dockPinned: DesktopPreferences.get("dockPinned")
|
||||||
|
|
||||||
// Dash-to-Dock was set to intellihide against all windows: the dock hides
|
// Dash-to-Dock was set to intellihide against all windows: the dock hides
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Tokyo Night Moon — the single source of truth for every colour, radius,
|
// Tokyo Night Moon — the single source of truth for every color, radius,
|
||||||
// duration and font in the shell. Nothing below this file should hardcode a
|
// duration and font in the shell. Nothing below this file should hardcode a
|
||||||
// colour; if you find yourself writing "#" outside this file, add a token here.
|
// color; if you find yourself writing "#" outside this file, add a token here.
|
||||||
//
|
//
|
||||||
// The bar keeps GNOME's 36px information density but sits directly on the
|
// The bar keeps GNOME's 36px information density but sits directly on the
|
||||||
// desktop, edge to edge. The dock retains the former glass vocabulary: bottom,
|
// desktop, edge to edge. The dock retains the former glass vocabulary: bottom,
|
||||||
@@ -18,7 +18,7 @@ import QtQuick
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// ── Colour scheme ───────────────────────────────────────────────────────
|
// ── Color scheme ───────────────────────────────────────────────────────
|
||||||
// Tokyo Night ships an official light variant (Day), so light mode is that
|
// Tokyo Night ships an official light variant (Day), so light mode is that
|
||||||
// rather than a palette invented to merely not be dark. The two share the
|
// rather than a palette invented to merely not be dark. The two share the
|
||||||
// same hues at different lightness, which is what lets the Prism identity
|
// same hues at different lightness, which is what lets the Prism identity
|
||||||
@@ -42,13 +42,41 @@ Singleton {
|
|||||||
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
||||||
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
||||||
|
|
||||||
// The pair. `accent` is the primary and carries every state meaning
|
// ── The accent ──────────────────────────────────────────────────────────
|
||||||
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
//
|
||||||
// theme — it never appears alone, only as the far end of a gradient. That
|
// `accent` is the primary and carries every state meaning (focused, active,
|
||||||
// restraint is the whole point: the two colours meeting is the signature,
|
// on). `accentSecondary` never appears alone, only as the far end of a
|
||||||
// so the pink stops being special the moment it's used as a flat fill.
|
// gradient. That restraint is the whole point: the two colors meeting is
|
||||||
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
|
// the signature, so the second color stops being special the moment it is
|
||||||
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
|
// used as a flat fill.
|
||||||
|
//
|
||||||
|
// NAMED accents rather than a free color. Each name carries a curated
|
||||||
|
// triple per scheme, because an arbitrary hex cannot work in both: a color
|
||||||
|
// legible on the Moon background is usually illegible on the Day one, and a
|
||||||
|
// picker that lets someone choose an unreadable desktop is not a feature.
|
||||||
|
// Every pair below measures at least 3:1 against the ground it sits on.
|
||||||
|
// This is also GNOME's model, which is the parity being chased.
|
||||||
|
//
|
||||||
|
// Blue is the shipped Prism -- blue leading, orchid following -- and stays
|
||||||
|
// the default.
|
||||||
|
readonly property var accents: ({
|
||||||
|
"blue": { dark: "#82aaff", darkSecondary: "#b172b0", light: "#2e7de9", lightSecondary: "#9854f1", label: "Prism blue" },
|
||||||
|
"orchid": { dark: "#c099ff", darkSecondary: "#fca7ea", light: "#7847bd", lightSecondary: "#9854f1", label: "Orchid" },
|
||||||
|
"teal": { dark: "#86e1fc", darkSecondary: "#82aaff", light: "#007197", lightSecondary: "#2e7de9", label: "Teal" },
|
||||||
|
"green": { dark: "#c3e88d", darkSecondary: "#86e1fc", light: "#587539", lightSecondary: "#007197", label: "Green" },
|
||||||
|
"amber": { dark: "#ffc777", darkSecondary: "#ff966c", light: "#8c6c3e", lightSecondary: "#b15c00", label: "Amber" },
|
||||||
|
"orange": { dark: "#ff966c", darkSecondary: "#ff757f", light: "#b15c00", lightSecondary: "#c64343", label: "Orange" },
|
||||||
|
"rose": { dark: "#ff757f", darkSecondary: "#c099ff", light: "#f52a65", lightSecondary: "#9854f1", label: "Rose" },
|
||||||
|
"slate": { dark: "#828bb8", darkSecondary: "#82aaff", light: "#6172b0", lightSecondary: "#2e7de9", label: "Slate" }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Falls back to blue for an unknown name, so a settings file written by a
|
||||||
|
// newer Panama -- or edited by hand -- degrades to the shipped identity
|
||||||
|
// rather than to an undefined color.
|
||||||
|
readonly property var accentPair: root.accents[DesktopPreferences.get("accentName")] ?? root.accents["blue"]
|
||||||
|
|
||||||
|
readonly property color accent: root.dark ? root.accentPair.dark : root.accentPair.light
|
||||||
|
readonly property color accentSecondary: root.dark ? root.accentPair.darkSecondary : root.accentPair.lightSecondary
|
||||||
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
||||||
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
||||||
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
||||||
@@ -108,7 +136,7 @@ Singleton {
|
|||||||
|
|
||||||
// Nerd Font, used ONLY to draw icon glyphs — never for text. It is the
|
// Nerd Font, used ONLY to draw icon glyphs — never for text. It is the
|
||||||
// pragmatic alternative to freedesktop symbolic icons, which ship with a
|
// pragmatic alternative to freedesktop symbolic icons, which ship with a
|
||||||
// hardcoded dark fill Qt will not recolour (see widgets/ThemedIcon.qml).
|
// hardcoded dark fill Qt will not recolor (see widgets/ThemedIcon.qml).
|
||||||
readonly property string fontMono: DesktopPreferences.get("iconFont")
|
readonly property string fontMono: DesktopPreferences.get("iconFont")
|
||||||
|
|
||||||
// Apply to any text whose digits change in place — clocks, percentages,
|
// Apply to any text whose digits change in place — clocks, percentages,
|
||||||
@@ -152,7 +180,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Blend two tokens — used for hover/pressed states so we never invent a
|
// Blend two tokens — used for hover/pressed states so we never invent a
|
||||||
// colour that isn't derived from the palette.
|
// color that isn't derived from the palette.
|
||||||
function mix(a: color, b: color, ratio: real): color {
|
function mix(a: color, b: color, ratio: real): color {
|
||||||
return Qt.rgba(a.r + (b.r - a.r) * ratio, a.g + (b.g - a.g) * ratio, a.b + (b.b - a.b) * ratio, a.a + (b.a - a.a) * ratio);
|
return Qt.rgba(a.r + (b.r - a.r) * ratio, a.g + (b.g - a.g) * ratio, a.b + (b.b - a.b) * ratio, a.a + (b.a - a.a) * ratio);
|
||||||
}
|
}
|
||||||
@@ -163,7 +191,7 @@ Singleton {
|
|||||||
// the active workspace pill and slider fills.
|
// the active workspace pill and slider fills.
|
||||||
//
|
//
|
||||||
// Where the gradient runs left-to-right the blue leads, because the eye
|
// Where the gradient runs left-to-right the blue leads, because the eye
|
||||||
// reads left first and blue is the colour that carries meaning.
|
// reads left first and blue is the color that carries meaning.
|
||||||
readonly property real prismStart: 0.22
|
readonly property real prismStart: 0.22
|
||||||
readonly property real prismEnd: 0.78
|
readonly property real prismEnd: 0.78
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshIdentityFixture(): string {
|
function refreshIdentityFixture(): string {
|
||||||
const modes = Displays.normaliseModes([
|
const modes = Displays.normalizeModes([
|
||||||
"[email protected]",
|
"[email protected]",
|
||||||
"[email protected]"
|
"[email protected]"
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//
|
//
|
||||||
// Fixture steps rather than the real list: the real one is empty until the
|
// Fixture steps rather than the real list: the real one is empty until the
|
||||||
// first breaking schema change, and a mechanism that has never been run against
|
// first breaking schema change, and a mechanism that has never been run against
|
||||||
// a failing step is not one to discover the behaviour of during an upgrade.
|
// a failing step is not one to discover the behavior of during an upgrade.
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|||||||
@@ -25,10 +25,18 @@ PanelWindow {
|
|||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
// Deliberately does not read Capture.recordingSeconds (or anything else
|
||||||
|
// that ticks once a second): this array is a plain JS array, not an
|
||||||
|
// identity-preserving model, so any dependency that changes every second
|
||||||
|
// would make the whole thing re-derive every second, and the Repeater
|
||||||
|
// below would destroy and recreate every row -- including one the user
|
||||||
|
// might be hovering or about to click. The set of activities should only
|
||||||
|
// change when an activity actually starts or stops. Elapsed time is
|
||||||
|
// rendered by each row's own Text binding instead, further down.
|
||||||
readonly property var activities: {
|
readonly property var activities: {
|
||||||
const result = [];
|
const result = [];
|
||||||
if (PrivacyState.recordingActive)
|
if (PrivacyState.recordingActive)
|
||||||
result.push({ kind: "recording", glyph: "\u{F044A}", label: "Screen recording", detail: "Panama · " + root.elapsed(), tone: "danger", stoppable: true });
|
result.push({ kind: "recording", glyph: "\u{F044A}", label: "Screen recording", detail: "Panama", tone: "danger", stoppable: true });
|
||||||
if (PrivacyState.screenSharingActive)
|
if (PrivacyState.screenSharingActive)
|
||||||
result.push({ kind: "screen", glyph: "\u{F0379}", label: "Screen sharing", detail: PrivacyState.screenSharingApp || "Managed by the application", tone: "warn", stoppable: false });
|
result.push({ kind: "screen", glyph: "\u{F0379}", label: "Screen sharing", detail: PrivacyState.screenSharingApp || "Managed by the application", tone: "warn", stoppable: false });
|
||||||
if (PrivacyState.cameraActive)
|
if (PrivacyState.cameraActive)
|
||||||
@@ -38,11 +46,12 @@ PanelWindow {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function elapsed(): string {
|
// Pure formatter, no ticking property read here -- callers decide what
|
||||||
const total = Capture.recordingSeconds;
|
// seconds value to pass, and only they take on the per-second dependency.
|
||||||
const seconds = String(total % 60).padStart(2, "0");
|
function formatElapsed(totalSeconds: int): string {
|
||||||
const minutes = Math.floor(total / 60) % 60;
|
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
||||||
const hours = Math.floor(total / 3600);
|
const minutes = Math.floor(totalSeconds / 60) % 60;
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +173,11 @@ PanelWindow {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: activityRow.modelData.detail
|
// Only this Text re-evaluates every second while
|
||||||
|
// recording -- Capture.recordingSeconds is read
|
||||||
|
// here, not in the parent `activities` array, so
|
||||||
|
// the row itself is never torn down for a tick.
|
||||||
|
text: activityRow.modelData.kind === "recording" ? activityRow.modelData.detail + " · " + root.formatElapsed(Capture.recordingSeconds) : activityRow.modelData.detail
|
||||||
color: Theme.fgDim
|
color: Theme.fgDim
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
// Bar {}
|
// Bar {}
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// Layout mirrors GNOME's: workspaces + vitals left, clock + weather centre,
|
// Layout mirrors GNOME's: workspaces + vitals left, clock + weather center,
|
||||||
// media + tray + status cluster right.
|
// media + tray + status cluster right.
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -83,7 +83,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Centre ──────────────────────────────────────────────────────────
|
// ── Center ──────────────────────────────────────────────────────────
|
||||||
Row {
|
Row {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -1,222 +0,0 @@
|
|||||||
// The calendar that drops out of the clock — GNOME's date menu, minus the
|
|
||||||
// notification list (that lives in its own panel). Month grid, today marked
|
|
||||||
// with the accent, arrows to page through months, current weather at the foot.
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import QtQuick
|
|
||||||
import qs.config
|
|
||||||
import qs.services
|
|
||||||
import qs.widgets
|
|
||||||
|
|
||||||
Popover {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
// Popover's container is a plain Item, which does not derive an implicit
|
|
||||||
// size from its children, so the window has to be sized from the body.
|
|
||||||
implicitWidth: body.implicitWidth + contentPadding * 2
|
|
||||||
implicitHeight: body.implicitHeight + contentPadding * 2
|
|
||||||
|
|
||||||
readonly property int cellSize: 34
|
|
||||||
readonly property int cellHeight: 30
|
|
||||||
|
|
||||||
// Hours precision is enough: the only thing that has to change on its own
|
|
||||||
// is which cell counts as "today", and that only moves at midnight.
|
|
||||||
SystemClock {
|
|
||||||
id: clock
|
|
||||||
precision: SystemClock.Hours
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property int todayYear: clock.date.getFullYear()
|
|
||||||
readonly property int todayMonth: clock.date.getMonth()
|
|
||||||
readonly property int todayDay: clock.date.getDate()
|
|
||||||
|
|
||||||
// The month currently on screen. Reset to today every time the popover
|
|
||||||
// opens, so it never comes back showing wherever you paged off to.
|
|
||||||
property int viewYear: root.todayYear
|
|
||||||
property int viewMonth: root.todayMonth
|
|
||||||
|
|
||||||
onVisibleChanged: if (visible)
|
|
||||||
root.showToday()
|
|
||||||
|
|
||||||
function showToday(): void {
|
|
||||||
root.viewYear = root.todayYear;
|
|
||||||
root.viewMonth = root.todayMonth;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stepMonth(delta: int): void {
|
|
||||||
const d = new Date(root.viewYear, root.viewMonth + delta, 1);
|
|
||||||
root.viewYear = d.getFullYear();
|
|
||||||
root.viewMonth = d.getMonth();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Six weeks of cells, so the grid height never changes as you page through
|
|
||||||
// months. Days from the neighbouring months fill the edges, dimmed.
|
|
||||||
readonly property var cells: {
|
|
||||||
const first = new Date(root.viewYear, root.viewMonth, 1);
|
|
||||||
const offset = first.getDay(); // 0 = Sunday, matching the header row
|
|
||||||
const inThisMonth = new Date(root.viewYear, root.viewMonth + 1, 0).getDate();
|
|
||||||
const inPrevMonth = new Date(root.viewYear, root.viewMonth, 0).getDate();
|
|
||||||
|
|
||||||
const out = [];
|
|
||||||
for (let i = 0; i < 42; i++) {
|
|
||||||
const n = i - offset + 1;
|
|
||||||
if (n < 1)
|
|
||||||
out.push({
|
|
||||||
day: inPrevMonth + n,
|
|
||||||
current: false
|
|
||||||
});
|
|
||||||
else if (n > inThisMonth)
|
|
||||||
out.push({
|
|
||||||
day: n - inThisMonth,
|
|
||||||
current: false
|
|
||||||
});
|
|
||||||
else
|
|
||||||
out.push({
|
|
||||||
day: n,
|
|
||||||
current: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: body
|
|
||||||
spacing: Theme.itemSpacing
|
|
||||||
|
|
||||||
// ── Month header ────────────────────────────────────────────────────
|
|
||||||
Item {
|
|
||||||
width: root.cellSize * 7
|
|
||||||
height: 28
|
|
||||||
|
|
||||||
CalendarArrow {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
glyph: "\u{F0141}" // md-chevron_left
|
|
||||||
onActivated: root.stepMonth(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: Qt.formatDateTime(new Date(root.viewYear, root.viewMonth, 1), "MMMM yyyy")
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
color: Theme.fg
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: root.showToday()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarArrow {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
glyph: "\u{F0142}" // md-chevron_right
|
|
||||||
onActivated: root.stepMonth(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Weekday header ──────────────────────────────────────────────────
|
|
||||||
Row {
|
|
||||||
Repeater {
|
|
||||||
model: ["S", "M", "T", "W", "T", "F", "S"]
|
|
||||||
|
|
||||||
delegate: Text {
|
|
||||||
required property string modelData
|
|
||||||
|
|
||||||
width: root.cellSize
|
|
||||||
height: 20
|
|
||||||
text: modelData
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.fgMuted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Day grid ────────────────────────────────────────────────────────
|
|
||||||
Grid {
|
|
||||||
columns: 7
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.cells
|
|
||||||
|
|
||||||
delegate: Item {
|
|
||||||
id: cell
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.viewMonth === root.todayMonth && root.viewYear === root.todayYear
|
|
||||||
|
|
||||||
width: root.cellSize
|
|
||||||
height: root.cellHeight
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: root.cellHeight - 2
|
|
||||||
height: root.cellHeight - 2
|
|
||||||
radius: width / 2
|
|
||||||
border.width: 0
|
|
||||||
visible: cell.isToday
|
|
||||||
color: Theme.accent
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: cell.modelData.day
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
font.weight: cell.isToday ? Font.DemiBold : Font.Normal
|
|
||||||
|
|
||||||
color: {
|
|
||||||
if (cell.isToday)
|
|
||||||
return Theme.bgDark;
|
|
||||||
return cell.modelData.current ? Theme.fg : Theme.fgMuted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Weather ─────────────────────────────────────────────────────────
|
|
||||||
Rectangle {
|
|
||||||
width: root.cellSize * 7
|
|
||||||
height: 1
|
|
||||||
color: Theme.alpha(Theme.fg, 0.1)
|
|
||||||
visible: Weather.available
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.itemSpacing
|
|
||||||
visible: Weather.available
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: Weather.icon
|
|
||||||
font.family: Theme.fontMono
|
|
||||||
font.pixelSize: Theme.fontSizeTitle
|
|
||||||
color: Theme.accentAlt
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
Text {
|
|
||||||
text: Math.round(Weather.temperature) + Weather.unitSuffix
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
color: Theme.fg
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: Weather.description
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.fgDim
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// The centre clock, kept in the same place and the same format GNOME had it:
|
// The center clock, kept in the same place and the same format GNOME had it:
|
||||||
// weekday, date, 12-hour time with seconds. Clicking it opens the calendar,
|
// weekday, date, 12-hour time with seconds. Clicking it opens the calendar,
|
||||||
// exactly like GNOME's date menu.
|
// exactly like GNOME's date menu.
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Pill {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: Qt.formatDateTime(clock.date, root.format)
|
text: Qt.formatDateTime(clock.date, root.format)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
// Seconds tick once a second in the centre of the bar; without tabular
|
// Seconds tick once a second in the center of the bar; without tabular
|
||||||
// figures the whole clock shifts sideways on every digit change.
|
// figures the whole clock shifts sideways on every digit change.
|
||||||
font.features: Theme.tabularFigures
|
font.features: Theme.tabularFigures
|
||||||
font.pixelSize: Theme.fontSize
|
font.pixelSize: Theme.fontSize
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Pill {
|
|||||||
if (!root.wifiNetwork)
|
if (!root.wifiNetwork)
|
||||||
return "\u{F092D}"; // md-wifi_strength_off
|
return "\u{F092D}"; // md-wifi_strength_off
|
||||||
|
|
||||||
// NetworkManager reports strength 0-100; some backends normalise to
|
// NetworkManager reports strength 0-100; some backends normalize to
|
||||||
// 0-1, so accept either rather than showing one bar forever.
|
// 0-1, so accept either rather than showing one bar forever.
|
||||||
const raw = root.wifiNetwork.signalStrength ?? 0;
|
const raw = root.wifiNetwork.signalStrength ?? 0;
|
||||||
const strength = raw <= 1 ? raw * 100 : raw;
|
const strength = raw <= 1 ? raw * 100 : raw;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// One glyph + percentage pair inside VitalsWidget. Split out so the three
|
// One glyph + percentage pair inside VitalsWidget. Split out so the three
|
||||||
// fields cannot drift apart in spacing, width or colour thresholds.
|
// fields cannot drift apart in spacing, width or color thresholds.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.config
|
import qs.config
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Vitals extension was configured with (_processor_usage_, _memory_usage_,
|
// Vitals extension was configured with (_processor_usage_, _memory_usage_,
|
||||||
// _gpu#1_usage_).
|
// _gpu#1_usage_).
|
||||||
//
|
//
|
||||||
// Values are colour-coded rather than graphed: at bar size a number you can
|
// Values are color-coded rather than graphed: at bar size a number you can
|
||||||
// glance at beats a sparkline you have to squint at.
|
// glance at beats a sparkline you have to squint at.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Item {
|
|||||||
|
|
||||||
implicitWidth: row.implicitWidth + 12
|
implicitWidth: row.implicitWidth + 12
|
||||||
// Full bar height makes (0, 0) part of the wheel target. The dots stay
|
// Full bar height makes (0, 0) part of the wheel target. The dots stay
|
||||||
// optically centred by the row and active-pill geometry below.
|
// optically centered by the row and active-pill geometry below.
|
||||||
implicitHeight: Theme.barHeight
|
implicitHeight: Theme.barHeight
|
||||||
|
|
||||||
// Scroll over the padding either side of the row, not just over a dot.
|
// Scroll over the padding either side of the row, not just over a dot.
|
||||||
@@ -59,7 +59,7 @@ Item {
|
|||||||
border.width: 0 // QTBUG-137166: rounded rects can lose their corners
|
border.width: 0 // QTBUG-137166: rounded rects can lose their corners
|
||||||
|
|
||||||
// The prism, as a fill rather than a hairline. This is the one element
|
// The prism, as a fill rather than a hairline. This is the one element
|
||||||
// in the bar that is always coloured, so it is where the pair belongs.
|
// in the bar that is always colored, so it is where the pair belongs.
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
orientation: Gradient.Horizontal
|
orientation: Gradient.Horizontal
|
||||||
GradientStop {
|
GradientStop {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// The floating control panel at the bottom of the capture overlay.
|
// The floating control panel at the bottom of the capture overlay.
|
||||||
//
|
//
|
||||||
// Layout is a deliberate copy of GNOME 42's screenshot UI: the area segmented
|
// Layout is a deliberate copy of GNOME 42's screenshot UI: the area segmented
|
||||||
// control on top, and below it the shutter dead-centre with "Show Pointer" on
|
// control on top, and below it the shutter dead-center with "Show Pointer" on
|
||||||
// the left and the screenshot/screencast switch on the right.
|
// the left and the screenshot/screencast switch on the right.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -13,7 +13,7 @@ Rectangle {
|
|||||||
|
|
||||||
signal captureRequested
|
signal captureRequested
|
||||||
|
|
||||||
// Set by the overlay so the shutter can grey out when Window mode has
|
// Set by the overlay so the shutter can gray out when Window mode has
|
||||||
// nothing pickable (no Hyprland, or an empty workspace).
|
// nothing pickable (no Hyprland, or an empty workspace).
|
||||||
property bool captureEnabled: true
|
property bool captureEnabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// One segment of the Screen / Window / Selection control.
|
// One segment of the Screen / Window / Selection control.
|
||||||
//
|
//
|
||||||
// Icons are Nerd Font glyphs rather than freedesktop icons on purpose: the
|
// Icons are Nerd Font glyphs rather than freedesktop icons on purpose: the
|
||||||
// Adwaita "symbolic" SVGs are flat black and Qt, unlike GTK, will not recolour
|
// Adwaita "symbolic" SVGs are flat black and Qt, unlike GTK, will not recolor
|
||||||
// them, so they'd be invisible on this dark overlay.
|
// them, so they'd be invisible on this dark overlay.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
// The "you are being recorded" pill.
|
|
||||||
//
|
|
||||||
// GNOME puts a red dot and a timer in the top bar while a screencast runs and
|
|
||||||
// clicking it stops the recording. The bar is another module's window, so this
|
|
||||||
// is its own tiny layer surface parked just below it.
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import QtQuick
|
|
||||||
import qs.config
|
|
||||||
import qs.services
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: win
|
|
||||||
|
|
||||||
visible: Capture.recording
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
// Top only: with neither left nor right anchored, layer-shell centres the
|
|
||||||
// surface horizontally.
|
|
||||||
anchors.top: true
|
|
||||||
margins.top: Theme.barGap * 2 + Theme.barHeight + Theme.barGap
|
|
||||||
|
|
||||||
implicitWidth: pill.implicitWidth
|
|
||||||
implicitHeight: pill.implicitHeight
|
|
||||||
exclusiveZone: 0
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "qs-popover-recording"
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
function elapsed(): string {
|
|
||||||
const t = Capture.recordingSeconds;
|
|
||||||
const s = ("0" + (t % 60)).slice(-2);
|
|
||||||
const m = Math.floor(t / 60) % 60;
|
|
||||||
const h = Math.floor(t / 3600);
|
|
||||||
return h > 0 ? h + ":" + ("0" + m).slice(-2) + ":" + s : m + ":" + s;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: pill
|
|
||||||
implicitWidth: row.implicitWidth + 12
|
|
||||||
implicitHeight: 34
|
|
||||||
radius: Theme.pillRadius
|
|
||||||
border.width: 0
|
|
||||||
color: Theme.redDeep
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: row
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
// Static dot, not a blinking one: nothing in this shell repaints
|
|
||||||
// while idle.
|
|
||||||
Rectangle {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 10
|
|
||||||
height: 10
|
|
||||||
radius: 5
|
|
||||||
border.width: 0
|
|
||||||
color: Theme.fg
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: win.elapsed()
|
|
||||||
color: Theme.fg
|
|
||||||
// Tabular figures: the timer must not shuffle as it counts.
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.features: Theme.tabularFigures
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: stop
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 26
|
|
||||||
height: 26
|
|
||||||
radius: width / 2
|
|
||||||
border.width: 0
|
|
||||||
color: stopMouse.containsMouse ? Theme.alpha(Theme.fg, 0.28) : Theme.alpha(Theme.fg, 0.14)
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.durFast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: ""
|
|
||||||
color: Theme.fg
|
|
||||||
font.family: Theme.fontMono
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: stopMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
// SIGINT, so wf-recorder finalises the container.
|
|
||||||
onClicked: Capture.stopRecording()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Selection mode: a GNOME-style rubber band over the frozen screen.
|
// Selection mode: a GNOME-style rubber band over the frozen screen.
|
||||||
//
|
//
|
||||||
// Behaviours copied from GNOME 42: the overlay opens with a sensible centred
|
// Behaviors copied from GNOME 42: the overlay opens with a sensible centered
|
||||||
// selection already in place (so the shutter is immediately useful), dragging
|
// selection already in place (so the shutter is immediately useful), dragging
|
||||||
// inside moves it, dragging a corner resizes from the opposite corner, and
|
// inside moves it, dragging a corner resizes from the opposite corner, and
|
||||||
// dragging anywhere else starts a fresh band.
|
// dragging anywhere else starts a fresh band.
|
||||||
@@ -52,7 +52,7 @@ Item {
|
|||||||
root.resetSelection();
|
root.resetSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centred, roughly the middle half of the screen.
|
// Centered, roughly the middle half of the screen.
|
||||||
function resetSelection(): void {
|
function resetSelection(): void {
|
||||||
if (root.width <= 0 || root.height <= 0)
|
if (root.width <= 0 || root.height <= 0)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// The month grid from GNOME's date menu.
|
// The month grid from GNOME's date menu.
|
||||||
//
|
//
|
||||||
// Six week rows always, so the grid height never changes as you page through
|
// Six week rows always, so the grid height never changes as you page through
|
||||||
// months and the panel below it never jumps. Days belonging to the neighbouring
|
// months and the panel below it never jumps. Days belonging to the neighboring
|
||||||
// months fill the edges, dimmed. Today wears the accent.
|
// months fill the edges, dimmed. Today wears the accent.
|
||||||
//
|
//
|
||||||
// Days become real navigation targets now that Evolution Data Server backs the
|
// Days become real navigation targets now that Evolution Data Server backs the
|
||||||
// panel. Up to three source-coloured markers show whether a date has events.
|
// panel. Up to three source-colored markers show whether a date has events.
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -134,7 +134,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centred on the row rather than between the pill and the arrows,
|
// Centered on the row rather than between the pill and the arrows,
|
||||||
// so it does not shift sideways when the pill appears.
|
// so it does not shift sideways when the pill appears.
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// GNOME's date/time menu: calendar, ongoing activity and messages in one panel,
|
// GNOME's date/time menu: calendar, ongoing activity and messages in one panel,
|
||||||
// dropping from the top centre because that is where GNOME put it and the
|
// dropping from the top center because that is where GNOME put it and the
|
||||||
// muscle memory is the whole point.
|
// muscle memory is the whole point.
|
||||||
//
|
//
|
||||||
// A layer-shell surface rather than a PopupWindow because the inline reply
|
// A layer-shell surface rather than a PopupWindow because the inline reply
|
||||||
// fields have to be typeable, and because the clock it hangs from is owned by a
|
// fields have to be typeable, and because the clock it hangs from is owned by a
|
||||||
// different module.
|
// different module.
|
||||||
//
|
//
|
||||||
// Layer-shell centres a surface on whichever axis it is not anchored to, so
|
// Layer-shell centers a surface on whichever axis it is not anchored to, so
|
||||||
// anchoring only `top` is what puts it in the middle.
|
// anchoring only `top` is what puts it in the middle.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -87,7 +87,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grows out of the top edge it is anchored to, rather than out of its
|
// Grows out of the top edge it is anchored to, rather than out of its
|
||||||
// own centre — the panel should look like it came from the bar.
|
// own center — the panel should look like it came from the bar.
|
||||||
transform: Scale {
|
transform: Scale {
|
||||||
id: openScale
|
id: openScale
|
||||||
origin.x: surface.width / 2
|
origin.x: surface.width / 2
|
||||||
|
|||||||
@@ -48,12 +48,22 @@ PanelWindow {
|
|||||||
implicitHeight: tooltipSpace + body.implicitHeight + bottomMargin
|
implicitHeight: tooltipSpace + body.implicitHeight + bottomMargin
|
||||||
|
|
||||||
// ── Intellihide ─────────────────────────────────────────────────────────
|
// ── Intellihide ─────────────────────────────────────────────────────────
|
||||||
|
// This instance's own monitor, the same lookup Workspaces.qml uses to
|
||||||
|
// scope a per-screen bar to its own screen. Falls back to null when this
|
||||||
|
// Dock is created standalone (no `screen` set) rather than via Variants.
|
||||||
|
readonly property HyprlandMonitor monitor: root.screen ? Hyprland.monitorFor(root.screen) : null
|
||||||
|
|
||||||
// Hyprland does not expose live toplevel geometry, so exact overlap cannot
|
// Hyprland does not expose live toplevel geometry, so exact overlap cannot
|
||||||
// be computed. "Is anything on this workspace at all" is the robust proxy,
|
// be computed. "Is anything on this workspace at all" is the robust proxy,
|
||||||
// and it is what Dash-to-Dock's all-windows intellihide felt like in
|
// and it is what Dash-to-Dock's all-windows intellihide felt like in
|
||||||
// practice: an empty workspace keeps the dock out.
|
// practice: an empty workspace keeps the dock out.
|
||||||
|
//
|
||||||
|
// Deliberately this instance's own monitor's active workspace, not the
|
||||||
|
// globally-focused one -- with one Dock per screen, keying off the global
|
||||||
|
// focus would make focusing an empty workspace on monitor A hide the dock
|
||||||
|
// on monitor B even though B's own workspace is still busy.
|
||||||
readonly property bool workspaceOccupied: {
|
readonly property bool workspaceOccupied: {
|
||||||
const ws = Hyprland.focusedWorkspace;
|
const ws = root.monitor ? root.monitor.activeWorkspace : Hyprland.focusedWorkspace;
|
||||||
return !!ws && ws.toplevels.values.length > 0;
|
return !!ws && ws.toplevels.values.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,15 +93,31 @@ PanelWindow {
|
|||||||
onTriggered: root.revealed = false
|
onTriggered: root.revealed = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other modules (the bar, the capture overlay) read this.
|
// Other modules (the bar, the capture overlay) read this. It is one
|
||||||
onRevealedChanged: ShellState.dockRevealed = revealed
|
// shared flag but there is one Dock per monitor, so only the instance on
|
||||||
|
// the currently-focused monitor is allowed to write it -- otherwise
|
||||||
|
// whichever instance last changed reveal state would stomp the others,
|
||||||
|
// and a reader would see an arbitrary monitor's value. This scopes the
|
||||||
|
// flag to mean "is the dock revealed on the monitor the user is on",
|
||||||
|
// which is what a capture overlay or the bar actually care about.
|
||||||
|
// (A true per-monitor flag would need ShellState.dockRevealed itself to
|
||||||
|
// become keyed by screen, which is out of scope here -- see the report.)
|
||||||
|
readonly property bool isFocusedMonitorInstance: root.monitor === null || root.monitor === Hyprland.focusedMonitor
|
||||||
|
|
||||||
|
onRevealedChanged: root._syncShellState()
|
||||||
|
onIsFocusedMonitorInstanceChanged: root._syncShellState()
|
||||||
|
|
||||||
|
function _syncShellState(): void {
|
||||||
|
if (root.isFocusedMonitorInstance)
|
||||||
|
ShellState.dockRevealed = root.revealed;
|
||||||
|
}
|
||||||
|
|
||||||
// wantRevealed's first evaluation emits no change signal when it lands on
|
// wantRevealed's first evaluation emits no change signal when it lands on
|
||||||
// false (the default), so the initial state has to be taken explicitly —
|
// false (the default), so the initial state has to be taken explicitly —
|
||||||
// otherwise a shell started on a busy workspace would leave the dock up.
|
// otherwise a shell started on a busy workspace would leave the dock up.
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
revealed = wantRevealed;
|
revealed = wantRevealed;
|
||||||
ShellState.dockRevealed = revealed;
|
root._syncShellState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Input region ────────────────────────────────────────────────────────
|
// ── Input region ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -172,14 +172,14 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centred on the hovered item, clamped inside the dock. Reads the item's
|
// Centered on the hovered item, clamped inside the dock. Reads the item's
|
||||||
// x directly (rather than mapToItem) so the binding re-evaluates when
|
// x directly (rather than mapToItem) so the binding re-evaluates when
|
||||||
// the row reflows.
|
// the row reflows.
|
||||||
x: {
|
x: {
|
||||||
if (!root.hoveredItem)
|
if (!root.hoveredItem)
|
||||||
return 0;
|
return 0;
|
||||||
const centre = row.x + root.hoveredItem.x + root.hoveredItem.width / 2;
|
const center = row.x + root.hoveredItem.x + root.hoveredItem.width / 2;
|
||||||
return Math.max(4, Math.min(root.width - width - 4, centre - width / 2));
|
return Math.max(4, Math.min(root.width - width - 4, center - width / 2));
|
||||||
}
|
}
|
||||||
y: -height - 8
|
y: -height - 8
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// One dock icon: the app, its running-window dots, hover scale and click
|
// One dock icon: the app, its running-window dots, hover scale and click
|
||||||
// behaviour. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
|
// behavior. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
|
||||||
// underneath, grow-on-hover, click to focus / cycle / launch.
|
// underneath, grow-on-hover, click to focus / cycle / launch.
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -72,7 +72,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Last resort for an app with no resolvable icon: an initial, which is
|
// Last resort for an app with no resolvable icon: an initial, which is
|
||||||
// still recognisable, unlike the theme's broken-icon placeholder.
|
// still recognizable, unlike the theme's broken-icon placeholder.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: root.iconSource === ""
|
visible: root.iconSource === ""
|
||||||
@@ -155,7 +155,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Quickshell reports the address without the 0x prefix Hyprland's
|
// Quickshell reports the address without the 0x prefix Hyprland's
|
||||||
// window selector expects; normalise so either form works.
|
// window selector expects; normalize so either form works.
|
||||||
if (next.address) {
|
if (next.address) {
|
||||||
const addr = next.address.startsWith("0x") ? next.address : "0x" + next.address;
|
const addr = next.address.startsWith("0x") ? next.address : "0x" + next.address;
|
||||||
Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addr}" })`);
|
Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addr}" })`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// One notification, rendered GNOME-style: app icon, app name, summary, body,
|
// One notification, rendered GNOME-style: app icon, app name, summary, body,
|
||||||
// optional image, action buttons and an inline reply box.
|
// optional image, action buttons and an inline reply box.
|
||||||
//
|
//
|
||||||
// Shared by the toasts and the notification centre so a notification looks the
|
// Shared by the toasts and the notification center so a notification looks the
|
||||||
// same wherever you meet it.
|
// same wherever you meet it.
|
||||||
//
|
//
|
||||||
// IconButton/ThemedIcon come from the quicksettings module — they belong in
|
// IconButton/ThemedIcon come from the quicksettings module — they belong in
|
||||||
@@ -21,7 +21,7 @@ Rectangle {
|
|||||||
|
|
||||||
required property var notification
|
required property var notification
|
||||||
|
|
||||||
// The centre renders a denser card than a toast banner.
|
// The center renders a denser card than a toast banner.
|
||||||
property bool compact: false
|
property bool compact: false
|
||||||
|
|
||||||
signal dismissed
|
signal dismissed
|
||||||
@@ -43,7 +43,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Critical notifications get a red edge rather than a colour wash, so the
|
// Critical notifications get a red edge rather than a color wash, so the
|
||||||
// text contrast never changes.
|
// text contrast never changes.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
// GNOME's message tray: everything that has arrived, grouped by app.
|
|
||||||
//
|
|
||||||
// Drops from the top centre because that is where GNOME's tray lives and the
|
|
||||||
// muscle memory is the whole point. Layer-shell centres a surface on whichever
|
|
||||||
// axis it is not anchored to, so anchoring only `top` does it.
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Hyprland
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.config
|
|
||||||
import qs.services
|
|
||||||
import qs.modules.quicksettings
|
|
||||||
import qs.widgets
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
visible: ShellState.notificationsOpen
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
anchors.top: true
|
|
||||||
margins.top: Theme.barHeight + Theme.barGap * 2
|
|
||||||
exclusiveZone: 0
|
|
||||||
|
|
||||||
implicitWidth: 440
|
|
||||||
implicitHeight: surface.implicitHeight
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "qs-popover-notifications"
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (root.visible)
|
|
||||||
Notifs.markAllRead();
|
|
||||||
}
|
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
|
||||||
windows: [root]
|
|
||||||
active: root.visible
|
|
||||||
onCleared: ShellState.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: surface
|
|
||||||
anchors.fill: parent
|
|
||||||
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
|
|
||||||
radius: Theme.popoverRadius
|
|
||||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
|
||||||
border.width: 1
|
|
||||||
border.color: Theme.alpha(Theme.fg, 0.08)
|
|
||||||
|
|
||||||
// The prism edge — see widgets/PrismEdge.qml. Sits just inside the 1px
|
|
||||||
// border so the two don't fight for the same row of pixels.
|
|
||||||
PrismEdge {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 1
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
inset: parent.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
Keys.onEscapePressed: ShellState.close()
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: content
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.margins: Theme.popoverPadding
|
|
||||||
spacing: Theme.itemSpacing
|
|
||||||
|
|
||||||
// ── Header ──────────────────────────────────────────────────
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Notifications"
|
|
||||||
color: Theme.fg
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
size: 30
|
|
||||||
iconSize: 16
|
|
||||||
icon: "notifications-disabled-symbolic"
|
|
||||||
tint: Notifs.doNotDisturb ? Theme.accent : Theme.fgDim
|
|
||||||
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: clearLabel.implicitWidth + 20
|
|
||||||
height: 28
|
|
||||||
radius: Theme.pillRadius
|
|
||||||
border.width: 0
|
|
||||||
visible: Notifs.hasNotifications
|
|
||||||
color: clearMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08)
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.durFast
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: clearLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "Clear all"
|
|
||||||
color: Theme.fg
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clearMouse
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: Notifs.dismissAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Empty state ─────────────────────────────────────────────
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 120
|
|
||||||
visible: !Notifs.hasNotifications
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: Notifs.doNotDisturb ? "Do Not Disturb is on" : "No notifications"
|
|
||||||
color: Theme.fgMuted
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Grouped history ─────────────────────────────────────────
|
|
||||||
ScrollColumn {
|
|
||||||
width: parent.width
|
|
||||||
maxHeight: 620
|
|
||||||
spacing: Theme.itemSpacing
|
|
||||||
visible: Notifs.hasNotifications
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Notifs.groups
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: group
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: 26
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 4
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: group.modelData.app
|
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
size: 24
|
|
||||||
iconSize: 13
|
|
||||||
tint: Theme.fgDim
|
|
||||||
icon: "edit-clear-all-symbolic"
|
|
||||||
iconFallback: "window-close-symbolic"
|
|
||||||
onClicked: Notifs.dismissApp(group.modelData.app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: group.modelData.items
|
|
||||||
|
|
||||||
NotificationCard {
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
width: group.width
|
|
||||||
compact: true
|
|
||||||
notification: modelData
|
|
||||||
onDismissed: Notifs.dismiss(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// A single banner: a notification card that slides in and times itself out.
|
// A single banner: a notification card that slides in and times itself out.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
@@ -14,28 +13,25 @@ Item {
|
|||||||
// into a keyboard focus request on the layer surface.
|
// into a keyboard focus request on the layer surface.
|
||||||
signal replyFocusChanged(bool focused)
|
signal replyFocusChanged(bool focused)
|
||||||
|
|
||||||
|
// Mirrors the signal above so the dismiss timer below can read it.
|
||||||
|
property bool replyFocused: false
|
||||||
|
|
||||||
implicitHeight: card.implicitHeight
|
implicitHeight: card.implicitHeight
|
||||||
|
|
||||||
// Critical notifications stay until dismissed (the setting is 0). An app
|
// Critical notifications stay until dismissed (the setting is 0). An app
|
||||||
// asking for 0 means "never expire" per the freedesktop spec; -1 means
|
// asking for 0 means "never expire" per the freedesktop spec; -1 means
|
||||||
// "server decides", which is our default.
|
// "server decides", which is our default. Shared with Notifs.qml, which
|
||||||
readonly property int timeoutMs: {
|
// gives a DND-hidden transient notification the same lifetime.
|
||||||
if (root.notification.urgency === NotificationUrgency.Critical)
|
readonly property int timeoutMs: Notifs.notificationTimeoutMs(root.notification)
|
||||||
return Settings.notificationTimeoutCriticalMs;
|
|
||||||
if (root.notification.expireTimeout === 0)
|
|
||||||
return 0;
|
|
||||||
if (root.notification.expireTimeout > 0)
|
|
||||||
return Math.round(root.notification.expireTimeout * 1000);
|
|
||||||
return Settings.notificationTimeoutMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
id: hover
|
id: hover
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
// Hovering holds the banner open; the countdown restarts on leave.
|
// Hovering, or actively typing a reply, holds the banner open; the
|
||||||
running: root.timeoutMs > 0 && !hover.hovered
|
// countdown restarts (from the top, same as hover) once both let go.
|
||||||
|
running: root.timeoutMs > 0 && !hover.hovered && !root.replyFocused
|
||||||
interval: root.timeoutMs
|
interval: root.timeoutMs
|
||||||
onTriggered: Notifs.dropPopup(root.notification)
|
onTriggered: Notifs.dropPopup(root.notification)
|
||||||
}
|
}
|
||||||
@@ -45,7 +41,10 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
notification: root.notification
|
notification: root.notification
|
||||||
onDismissed: Notifs.dropPopup(root.notification)
|
onDismissed: Notifs.dropPopup(root.notification)
|
||||||
onReplyFocusChanged: focused => root.replyFocusChanged(focused)
|
onReplyFocusChanged: focused => {
|
||||||
|
root.replyFocused = focused;
|
||||||
|
root.replyFocusChanged(focused);
|
||||||
|
}
|
||||||
|
|
||||||
// Slide in from the right edge. Runs once, on creation.
|
// Slide in from the right edge. Runs once, on creation.
|
||||||
NumberAnimation on x {
|
NumberAnimation on x {
|
||||||
|
|||||||
@@ -45,7 +45,17 @@ PanelWindow {
|
|||||||
spacing: Theme.itemSpacing
|
spacing: Theme.itemSpacing
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Notifs.popups.slice(0, Settings.maxVisibleToasts)
|
// Notifs.popups.slice() is a fresh array on every change (a new
|
||||||
|
// arrival, a dismissal, a sibling toast timing out). Handing that
|
||||||
|
// straight to Repeater would reset the model and rebuild every
|
||||||
|
// delegate each time, blowing away whichever toast has a reply
|
||||||
|
// field mid-typing. ScriptModel diffs by object identity
|
||||||
|
// (Notification instances are unique QObjects), so only genuinely
|
||||||
|
// added/removed notifications add/remove delegates — unrelated
|
||||||
|
// toasts, and their slide-in animations, are untouched.
|
||||||
|
model: ScriptModel {
|
||||||
|
values: Notifs.popups.slice(0, Settings.maxVisibleToasts)
|
||||||
|
}
|
||||||
|
|
||||||
Toast {
|
Toast {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// One workspace in the overview: a proportional card holding live thumbnails
|
// One workspace in the overview: a proportional card holding live thumbnails
|
||||||
// of its windows, labelled underneath. The focused workspace is outlined in the
|
// of its windows, labeled underneath. The focused workspace is outlined in the
|
||||||
// accent colour, as GNOME outlines the current workspace.
|
// accent color, as GNOME outlines the current workspace.
|
||||||
//
|
//
|
||||||
// A card with a null `workspace` is the trailing "new workspace" affordance
|
// A card with a null `workspace` is the trailing "new workspace" affordance
|
||||||
// that dynamic-workspace setups (GNOME's, and Hyprland's) always show.
|
// that dynamic-workspace setups (GNOME's, and Hyprland's) always show.
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ Item {
|
|||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
// BlueZ reports a plain freedesktop name ("audio-headphones");
|
// BlueZ reports a plain freedesktop name ("audio-headphones");
|
||||||
// the symbolic variant is the one that can be recoloured.
|
// the symbolic variant is the one that can be recolored.
|
||||||
icon: deviceRow.modelData.icon !== "" ? deviceRow.modelData.icon + "-symbolic" : "bluetooth-symbolic"
|
icon: deviceRow.modelData.icon !== "" ? deviceRow.modelData.icon + "-symbolic" : "bluetooth-symbolic"
|
||||||
iconFallback: "bluetooth-symbolic"
|
iconFallback: "bluetooth-symbolic"
|
||||||
label: deviceRow.modelData.name || deviceRow.modelData.address
|
label: deviceRow.modelData.name || deviceRow.modelData.address
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Item {
|
|||||||
property real backlightValue: 0
|
property real backlightValue: 0
|
||||||
|
|
||||||
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
|
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
|
||||||
readonly property bool labelled: root.rowCount > 1
|
readonly property bool labeled: root.rowCount > 1
|
||||||
|
|
||||||
visible: root.rowCount > 0
|
visible: root.rowCount > 0
|
||||||
implicitHeight: rows.implicitHeight
|
implicitHeight: rows.implicitHeight
|
||||||
@@ -89,7 +89,7 @@ Item {
|
|||||||
// Hyprland already knows what each output is called, so the
|
// Hyprland already knows what each output is called, so the
|
||||||
// name comes from there rather than from a second source that
|
// name comes from there rather than from a second source that
|
||||||
// could disagree with the Displays page. The connector is the
|
// could disagree with the Displays page. The connector is the
|
||||||
// fallback, so a display is never an unlabelled slider.
|
// fallback, so a display is never an unlabeled slider.
|
||||||
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
||||||
value: modelData.value / 100
|
value: modelData.value / 100
|
||||||
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
|
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
|
||||||
@@ -113,7 +113,7 @@ Item {
|
|||||||
anchors.leftMargin: 6
|
anchors.leftMargin: 6
|
||||||
anchors.rightMargin: 6
|
anchors.rightMargin: 6
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
visible: root.labelled
|
visible: root.labeled
|
||||||
height: visible ? implicitHeight + 2 : 0
|
height: visible ? implicitHeight + 2 : 0
|
||||||
text: row.label
|
text: row.label
|
||||||
color: Theme.fgDim
|
color: Theme.fgDim
|
||||||
@@ -124,7 +124,7 @@ Item {
|
|||||||
|
|
||||||
// The slider and its glyph share one strip so the two stay aligned
|
// The slider and its glyph share one strip so the two stay aligned
|
||||||
// whether or not a caption sits above them. Anchoring the glyph to
|
// whether or not a caption sits above them. Anchoring the glyph to
|
||||||
// both a caption and a centre line instead would conflict, and an
|
// both a caption and a center line instead would conflict, and an
|
||||||
// anchor set to undefined is not released.
|
// anchor set to undefined is not released.
|
||||||
Item {
|
Item {
|
||||||
id: control
|
id: control
|
||||||
@@ -134,7 +134,7 @@ Item {
|
|||||||
height: 32
|
height: 32
|
||||||
|
|
||||||
// ValueSlider draws its own leading icon, but symbolic icons need
|
// ValueSlider draws its own leading icon, but symbolic icons need
|
||||||
// recolouring to be visible — see ThemedIcon.
|
// recoloring to be visible — see ThemedIcon.
|
||||||
ThemedIcon {
|
ThemedIcon {
|
||||||
id: glyph
|
id: glyph
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// The three system power profiles, as an expandable list.
|
||||||
|
//
|
||||||
|
// A row per profile rather than a cycling button: there are three, and cycling
|
||||||
|
// through them means passing through one you did not want on a machine where
|
||||||
|
// the change is immediate and audible. The same shape the audio device lists
|
||||||
|
// use, so the panel reads consistently.
|
||||||
|
//
|
||||||
|
// The daemon owns the profile -- it survives a shell restart and anything else
|
||||||
|
// on the system can change it -- so this reads back rather than assuming, the
|
||||||
|
// same as monitor brightness.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: PowerProfiles.profiles
|
||||||
|
|
||||||
|
RowButton {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: root.width
|
||||||
|
icon: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "power-saver": return "power-profile-power-saver-symbolic";
|
||||||
|
case "performance": return "power-profile-performance-symbolic";
|
||||||
|
default: return "power-profile-balanced-symbolic";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconFallback: "preferences-system-power-symbolic"
|
||||||
|
label: PowerProfiles.label(modelData)
|
||||||
|
sublabel: PowerProfiles.detail(modelData)
|
||||||
|
selected: modelData === PowerProfiles.active
|
||||||
|
dimmed: PowerProfiles.busy
|
||||||
|
onClicked: PowerProfiles.set(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only when it is true. A machine that cannot deliver the profile it is set
|
||||||
|
// to is the one case where the label alone is misleading.
|
||||||
|
Text {
|
||||||
|
visible: PowerProfiles.degraded !== ""
|
||||||
|
width: root.width
|
||||||
|
text: "Limited right now: " + PowerProfiles.degraded
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
leftPadding: 10
|
||||||
|
topPadding: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,9 @@ PanelWindow {
|
|||||||
if (root.visible) {
|
if (root.visible) {
|
||||||
KdeConnect.refresh();
|
KdeConnect.refresh();
|
||||||
HomeAssistant.refresh();
|
HomeAssistant.refresh();
|
||||||
|
// The daemon owns the power profile and anything on the system can
|
||||||
|
// change it, so the panel asks rather than trusting what it last saw.
|
||||||
|
PowerProfiles.refresh();
|
||||||
openAnim.restart();
|
openAnim.restart();
|
||||||
} else {
|
} else {
|
||||||
panel.reset();
|
panel.reset();
|
||||||
|
|||||||
@@ -130,6 +130,21 @@ Item {
|
|||||||
onToggled: Caffeine.toggle()
|
onToggled: Caffeine.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle {
|
||||||
|
width: root.cellWidth
|
||||||
|
icon: ColorScheme.dark ? "weather-clear-night-symbolic" : "weather-clear-symbolic"
|
||||||
|
label: "Appearance"
|
||||||
|
sublabel: ColorScheme.dark ? "Dark" : "Light"
|
||||||
|
// "active" reads as the non-default state across this grid, and
|
||||||
|
// dark is what Panama ships, so light is the lit one.
|
||||||
|
active: !ColorScheme.dark
|
||||||
|
// Writes the preference and stops. ColorScheme propagates it to
|
||||||
|
// GTK, the portal, the terminals, the launcher, btop, tmux and
|
||||||
|
// the lock screen; nothing here needs to know that list.
|
||||||
|
onToggled: SystemSettings.commitPreference("colorScheme",
|
||||||
|
ColorScheme.dark ? "light" : "dark")
|
||||||
|
}
|
||||||
|
|
||||||
Toggle {
|
Toggle {
|
||||||
width: root.cellWidth
|
width: root.cellWidth
|
||||||
icon: "night-light-symbolic"
|
icon: "night-light-symbolic"
|
||||||
@@ -190,6 +205,38 @@ Item {
|
|||||||
color: Theme.alpha(Theme.fg, 0.1)
|
color: Theme.alpha(Theme.fg, 0.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Power ───────────────────────────────────────────────────────────
|
||||||
|
// Only where a power-profiles daemon is actually running. A desktop
|
||||||
|
// without one should not show a control that cannot do anything.
|
||||||
|
RowButton {
|
||||||
|
visible: PowerProfiles.available
|
||||||
|
width: content.width
|
||||||
|
icon: {
|
||||||
|
switch (PowerProfiles.active) {
|
||||||
|
case "power-saver": return "power-profile-power-saver-symbolic";
|
||||||
|
case "performance": return "power-profile-performance-symbolic";
|
||||||
|
default: return "power-profile-balanced-symbolic";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iconFallback: "preferences-system-power-symbolic"
|
||||||
|
label: "Power profile"
|
||||||
|
sublabel: PowerProfiles.degraded !== ""
|
||||||
|
? PowerProfiles.label(PowerProfiles.active) + " · limited"
|
||||||
|
: PowerProfiles.label(PowerProfiles.active)
|
||||||
|
selected: root.expandedSection === "power"
|
||||||
|
onClicked: root.expand("power")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
width: content.width
|
||||||
|
expanded: root.expandedSection === "power"
|
||||||
|
|
||||||
|
PowerProfileList {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sliders ─────────────────────────────────────────────────────────
|
// ── Sliders ─────────────────────────────────────────────────────────
|
||||||
AudioSlider {
|
AudioSlider {
|
||||||
width: content.width
|
width: content.width
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full-height rather than vertically centred so slot contents can anchor
|
// Full-height rather than vertically centered so slot contents can anchor
|
||||||
// their own verticalCenter to it without a size loop.
|
// their own verticalCenter to it without a size loop.
|
||||||
Row {
|
Row {
|
||||||
id: trailingRow
|
id: trailingRow
|
||||||
|
|||||||
@@ -137,7 +137,18 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: root.networks
|
// root.networks is a fresh array on every signal-strength tick (the
|
||||||
|
// sort comparator reads signalStrength/connected/known, so any of
|
||||||
|
// those changing on ANY network recomputes the whole list). Handing
|
||||||
|
// that straight to Repeater would reset the model and rebuild every
|
||||||
|
// delegate each tick, blowing away whichever row has its password
|
||||||
|
// Section open and focused. ScriptModel diffs by object identity
|
||||||
|
// (WifiNetwork instances are unique QObjects) and turns a reorder
|
||||||
|
// into move operations, so existing delegates -- and their expanded
|
||||||
|
// state -- survive.
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.networks
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: entry
|
id: entry
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ RowButton 1.0 RowButton.qml
|
|||||||
ScrollColumn 1.0 ScrollColumn.qml
|
ScrollColumn 1.0 ScrollColumn.qml
|
||||||
Section 1.0 Section.qml
|
Section 1.0 Section.qml
|
||||||
WifiList 1.0 WifiList.qml
|
WifiList 1.0 WifiList.qml
|
||||||
|
PowerProfileList 1.0 PowerProfileList.qml
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
// Choosing the desktop accent.
|
||||||
|
//
|
||||||
|
// Each swatch is drawn as the GRADIENT it will actually produce, not a flat
|
||||||
|
// dot, because the gradient is the thing being chosen -- the focused window
|
||||||
|
// border, the bar hairline and every active state are the two colors meeting.
|
||||||
|
// A row of flat circles would misrepresent all of them.
|
||||||
|
//
|
||||||
|
// Named accents rather than a color wheel: each name carries a curated pair
|
||||||
|
// per scheme, so every choice stays legible in both light and dark. See
|
||||||
|
// config/Theme.qml for the palette and the reasoning.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
readonly property string current: DesktopPreferences.get("accentName") || "blue"
|
||||||
|
|
||||||
|
// The schema's own option list, not Theme.accents directly -- two lists
|
||||||
|
// hand-kept in sync is how they drift. This is the same pattern
|
||||||
|
// ChoiceRow.qml uses for every other enum row.
|
||||||
|
readonly property var spec: PreferenceSchema.spec("accentName")
|
||||||
|
readonly property var options: root.spec && root.spec.options ? root.spec.options : []
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
// The schema's option order is the palette's order, so blue is first
|
||||||
|
// because it is what Panama ships.
|
||||||
|
model: root.options
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: entry
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property string name: entry.modelData.value
|
||||||
|
readonly property var pair: Theme.accents[entry.name]
|
||||||
|
readonly property bool selected: entry.name === root.current
|
||||||
|
readonly property color start: Theme.dark ? entry.pair.dark : entry.pair.light
|
||||||
|
readonly property color end: Theme.dark ? entry.pair.darkSecondary : entry.pair.lightSecondary
|
||||||
|
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
// The hit target is the whole swatch+label unit, not just the
|
||||||
|
// 46px circle: the label exists specifically so someone with a
|
||||||
|
// color vision deficiency can identify an accent without it, and
|
||||||
|
// a label that cannot itself be tapped defeats that.
|
||||||
|
HoverHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: {
|
||||||
|
if (!SystemSettings.commitPreference("accentName", entry.name))
|
||||||
|
console.warn("AccentPicker: commitPreference rejected accent", entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 46
|
||||||
|
height: 46
|
||||||
|
radius: 23
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
// The ring sits outside the gradient rather than over it, so a
|
||||||
|
// selected swatch still shows its true colors.
|
||||||
|
border.width: entry.selected ? 2 : 1
|
||||||
|
border.color: entry.selected ? Theme.fg : Theme.alpha(Theme.fg, 0.14)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: entry.selected ? 4 : 3
|
||||||
|
radius: width / 2
|
||||||
|
border.width: 0
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
GradientStop { position: 0.0; color: entry.start }
|
||||||
|
GradientStop { position: 1.0; color: entry.end }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always shown, not a tooltip. Telling swatches apart by color is
|
||||||
|
// exactly what someone with a color vision deficiency cannot do,
|
||||||
|
// and it is the reason the palette is named rather than freeform --
|
||||||
|
// hiding the names behind a hover would waste that.
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: entry.modelData.label
|
||||||
|
color: entry.selected ? Theme.fg : Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: entry.selected ? Font.DemiBold : Font.Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,12 +101,18 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Colour scheme"
|
title: "Color scheme"
|
||||||
subtitle: ColorScheme.lastError !== ""
|
subtitle: ColorScheme.lastError !== ""
|
||||||
? ColorScheme.lastError
|
? ColorScheme.lastError
|
||||||
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow."
|
: "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow."
|
||||||
|
|
||||||
ChoiceRow { setting: "colorScheme"; divider: false }
|
ChoiceRow { setting: "colorScheme" }
|
||||||
|
|
||||||
|
// Drawn as the gradient each accent produces rather than a flat dot,
|
||||||
|
// because the gradient is what is being chosen.
|
||||||
|
AccentPicker {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// because that genuinely is a Panama preference.
|
// because that genuinely is a Panama preference.
|
||||||
//
|
//
|
||||||
// Changing the timezone or network time needs privilege. timedatectl asks
|
// Changing the timezone or network time needs privilege. timedatectl asks
|
||||||
// polkit, and a cancelled dialog surfaces as an error rather than as a value
|
// polkit, and a canceled dialog surfaces as an error rather than as a value
|
||||||
// that appears to have been accepted.
|
// that appears to have been accepted.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -33,7 +33,7 @@ SettingsPage {
|
|||||||
SettingRow {
|
SettingRow {
|
||||||
label: "Set automatically"
|
label: "Set automatically"
|
||||||
detail: DateTime.ntpEnabled
|
detail: DateTime.ntpEnabled
|
||||||
? (DateTime.ntpSynchronised ? "Synchronised with a time server" : "Waiting to synchronise")
|
? (DateTime.ntpSynchronized ? "Synchronized with a time server" : "Waiting to synchronize")
|
||||||
: "The clock is set by hand"
|
: "The clock is set by hand"
|
||||||
controlWidth: 48
|
controlWidth: 48
|
||||||
divider: false
|
divider: false
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Rectangle {
|
|||||||
|
|
||||||
// Stands in for the wallpaper. Static: an animated gradient here would
|
// Stands in for the wallpaper. Static: an animated gradient here would
|
||||||
// repaint forever behind a settings page.
|
// repaint forever behind a settings page.
|
||||||
// Follows the colour scheme. A preview that stays dark while the shell
|
// Follows the color scheme. A preview that stays dark while the shell
|
||||||
// around it is light does not read as "your desktop" -- it reads as a
|
// around it is light does not read as "your desktop" -- it reads as a
|
||||||
// screenshot of someone else's.
|
// screenshot of someone else's.
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
@@ -129,7 +129,7 @@ Rectangle {
|
|||||||
required property bool focused
|
required property bool focused
|
||||||
|
|
||||||
// The gradient border is drawn as a filled rounded rect with the window
|
// The gradient border is drawn as a filled rounded rect with the window
|
||||||
// body inset on top of it: QML's Rectangle border takes a colour, not a
|
// body inset on top of it: QML's Rectangle border takes a color, not a
|
||||||
// gradient, and the prism border is the whole signature here.
|
// gradient, and the prism border is the whole signature here.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: frame
|
id: frame
|
||||||
|
|||||||
@@ -440,9 +440,9 @@ SettingsPage {
|
|||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
objectName: "health-fedora-handoff:color"
|
objectName: "health-fedora-handoff:color"
|
||||||
label: "Colour profiles"
|
label: "Color profiles"
|
||||||
detail: "ICC profiles for displays, printers, and scanners"
|
detail: "Assigning an ICC profile here has no visible effect in this session: the colord daemon that loads it onto a display isn't running under Hyprland"
|
||||||
action: "Open colour"
|
action: "Open color"
|
||||||
onTriggered: SystemSettings.openGnomePanel("color")
|
onTriggered: SystemSettings.openGnomePanel("color")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ SettingsPage {
|
|||||||
|
|
||||||
title: "Mouse & Touchpad"
|
title: "Mouse & Touchpad"
|
||||||
lede: InputDevices.hasTouchpad
|
lede: InputDevices.hasTouchpad
|
||||||
? "Pointer behaviour for your mouse and touchpad."
|
? "Pointer behavior for your mouse and touchpad."
|
||||||
: "Pointer behaviour. Touchpad settings appear when a touchpad is attached."
|
: "Pointer behavior. Touchpad settings appear when a touchpad is attached."
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Mouse"
|
title: "Mouse"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Idle behaviour"
|
title: "Idle behavior"
|
||||||
subtitle: IdleLock.managed
|
subtitle: IdleLock.managed
|
||||||
? "Idle timings are managed here. Changes take effect immediately."
|
? "Idle timings are managed here. Changes take effect immediately."
|
||||||
: "hypridle is running its shipped configuration. Turn on management below to make these adjustable."
|
: "hypridle is running its shipped configuration. Turn on management below to make these adjustable."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Settings
|
# Settings
|
||||||
|
|
||||||
The control centre for everything Panama owns. Anything the system owns —
|
The control center for everything Panama owns. Anything the system owns —
|
||||||
hardware, accounts, printers — is delegated to GNOME Settings and labelled as
|
hardware, accounts, printers — is delegated to GNOME Settings and labeled as
|
||||||
such rather than half-reimplemented.
|
such rather than half-reimplemented.
|
||||||
|
|
||||||
## System Health
|
## System Health
|
||||||
@@ -28,7 +28,7 @@ package, runs `sudo`, deletes user data, or repairs a service Panama does not
|
|||||||
own. A repair remains degraded until a fresh scan observes recovery.
|
own. A repair remains degraded until a fresh scan observes recovery.
|
||||||
|
|
||||||
The final card is the ownership boundary. Network configuration and the exact
|
The final card is the ownership boundary. Network configuration and the exact
|
||||||
Users, Sharing, Colour profiles, and Digital wellbeing handoffs open GNOME
|
Users, Sharing, Color profiles, and Digital wellbeing handoffs open GNOME
|
||||||
Settings because Fedora's system services own those areas.
|
Settings because Fedora's system services own those areas.
|
||||||
|
|
||||||
## Adding a setting
|
## Adding a setting
|
||||||
@@ -64,7 +64,7 @@ file.
|
|||||||
Every preference has **one primary page**, derived from its schema `group` and
|
Every preference has **one primary page**, derived from its schema `group` and
|
||||||
the route in `services/SettingsSearch.qml`. Search results always open that
|
the route in `services/SettingsSearch.qml`. Search results always open that
|
||||||
owner. A control may appear on a second page only when the same adaptation is
|
owner. A control may appear on a second page only when the same adaptation is
|
||||||
part of another established mental model; otherwise use a labelled handoff to
|
part of another established mental model; otherwise use a labeled handoff to
|
||||||
the owner instead of duplicating it.
|
the owner instead of duplicating it.
|
||||||
|
|
||||||
### Intentional mirrors
|
### Intentional mirrors
|
||||||
@@ -72,7 +72,7 @@ the owner instead of duplicating it.
|
|||||||
| Setting | Primary page | Mirror | Why the mirror earns its place |
|
| Setting | Primary page | Mirror | Why the mirror earns its place |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `animationsEnabled` | Appearance | Accessibility | Reduced motion belongs both to visual polish and motion accessibility. |
|
| `animationsEnabled` | Appearance | Accessibility | Reduced motion belongs both to visual polish and motion accessibility. |
|
||||||
| `cursorInactiveTimeout` | Mouse | Accessibility | Pointer visibility is configured with pointer behaviour but affects motor and visual access. |
|
| `cursorInactiveTimeout` | Mouse | Accessibility | Pointer visibility is configured with pointer behavior but affects motor and visual access. |
|
||||||
| `cursorSize` | Accessibility | Mouse | Large cursors are an accessibility adaptation that users also look for beside pointer controls. |
|
| `cursorSize` | Accessibility | Mouse | Large cursors are an accessibility adaptation that users also look for beside pointer controls. |
|
||||||
| `inactiveOpacity` | Appearance | Accessibility | Window translucency is an appearance choice with a direct readability impact. |
|
| `inactiveOpacity` | Appearance | Accessibility | Window translucency is an appearance choice with a direct readability impact. |
|
||||||
| `lockMinutes` | Power | Privacy | Idle timing owns the mechanism; privacy owns the expectation that the unattended desktop locks. |
|
| `lockMinutes` | Power | Privacy | Idle timing owns the mechanism; privacy owns the expectation that the unattended desktop locks. |
|
||||||
@@ -93,12 +93,14 @@ Mirrors must remain the same schema-backed control, never a second preference
|
|||||||
or a copied default. Additions to this table require a concrete discoverability
|
or a copied default. Additions to this table require a concrete discoverability
|
||||||
reason and an update to `tests/quickshell/settings-ownership-contract.sh`.
|
reason and an update to `tests/quickshell/settings-ownership-contract.sh`.
|
||||||
|
|
||||||
Window border colour follows the same ownership rule. The inactive border is a
|
Window border color follows the same ownership rule. The inactive border is a
|
||||||
**scheme-relative role** owned by `ColorScheme.qml`: it changes only to retain
|
**scheme-relative role** owned by `ColorScheme.qml`: it changes only to retain
|
||||||
neutral contrast in light and dark modes. The focused Prism border is the
|
neutral contrast in light and dark modes. The focused Prism border is the
|
||||||
accent role owned by the visual theme (and, eventually, an accent picker).
|
**accent role**, driven by the chosen `accentName` and also owned by
|
||||||
`ColorScheme.qml` must never write the focused border, so changing schemes
|
`ColorScheme.qml`: each accent carries a separate pair for light and dark, so
|
||||||
cannot erase a user-selected accent.
|
`ColorScheme.qml` restates the focused border alongside the inactive one on
|
||||||
|
every scheme change, rather than leaving a scheme flip to erase a
|
||||||
|
user-selected accent.
|
||||||
|
|
||||||
## The rows
|
## The rows
|
||||||
|
|
||||||
@@ -163,7 +165,7 @@ file with `qs -p`, and discover its PID from the exact Config path in
|
|||||||
| File | Holds |
|
| File | Holds |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `~/.config/panama/settings.json` | Everything in the schema. Read by the shell *and* by `hypr/prefs.lua` |
|
| `~/.config/panama/settings.json` | Everything in the schema. Read by the shell *and* by `hypr/prefs.lua` |
|
||||||
| `$XDG_STATE_HOME/panama/panama-home.json` | Home accessory favourites and aliases |
|
| `$XDG_STATE_HOME/panama/panama-home.json` | Home accessory favorites and aliases |
|
||||||
| `$XDG_STATE_HOME/panama/backups/` | Settings snapshots |
|
| `$XDG_STATE_HOME/panama/backups/` | Settings snapshots |
|
||||||
| `$XDG_STATE_HOME/panama/hypridle.conf` | Generated idle config |
|
| `$XDG_STATE_HOME/panama/hypridle.conf` | Generated idle config |
|
||||||
| `$XDG_STATE_HOME/panama/hyprlock.conf` | Generated lock-screen appearance |
|
| `$XDG_STATE_HOME/panama/hyprlock.conf` | Generated lock-screen appearance |
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ FocusScope {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
signal captured(string chord)
|
signal captured(string chord)
|
||||||
signal cancelled
|
signal canceled
|
||||||
|
|
||||||
property string message: ""
|
property string message: ""
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ FocusScope {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
|
|
||||||
if (event.key === 0x01000000) { // Escape
|
if (event.key === 0x01000000) { // Escape
|
||||||
root.cancelled();
|
root.canceled();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root.isModifierOnly(event.key))
|
if (root.isModifierOnly(event.key))
|
||||||
@@ -127,6 +127,6 @@ FocusScope {
|
|||||||
|
|
||||||
onActiveFocusChanged: {
|
onActiveFocusChanged: {
|
||||||
if (!activeFocus)
|
if (!activeFocus)
|
||||||
root.cancelled();
|
root.canceled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// description in hypr/keybinds.lua, and this page just groups and renders them.
|
// description in hypr/keybinds.lua, and this page just groups and renders them.
|
||||||
//
|
//
|
||||||
// The hardware settings above the list are real controls. Keyboard layout,
|
// The hardware settings above the list are real controls. Keyboard layout,
|
||||||
// repeat behaviour, and pointer response are Hyprland's, so Panama owns them;
|
// repeat behavior, and pointer response are Hyprland's, so Panama owns them;
|
||||||
// device-specific configuration stays with GNOME.
|
// device-specific configuration stays with GNOME.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
@@ -174,7 +174,7 @@ SettingsPage {
|
|||||||
Keybinds.rebind(bindRow.modelData.luaChord, chord);
|
Keybinds.rebind(bindRow.modelData.luaChord, chord);
|
||||||
root.capturingChord = "";
|
root.capturingChord = "";
|
||||||
}
|
}
|
||||||
onCancelled: root.capturingChord = ""
|
onCanceled: root.capturingChord = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//
|
//
|
||||||
// Range, step, label, explanation, and unit all come from PreferenceSchema.
|
// Range, step, label, explanation, and unit all come from PreferenceSchema.
|
||||||
//
|
//
|
||||||
// Three behaviours worth knowing:
|
// Three behaviors worth knowing:
|
||||||
//
|
//
|
||||||
// * The row is responsive. The Settings window is a normal tiled window, so
|
// * The row is responsive. The Settings window is a normal tiled window, so
|
||||||
// its width is whatever the layout gives it -- anywhere from a half-screen
|
// its width is whatever the layout gives it -- anywhere from a half-screen
|
||||||
@@ -51,9 +51,11 @@ Item {
|
|||||||
return typeof value === "number" ? value : root.minimum;
|
return typeof value === "number" ? value : root.minimum;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shown while dragging; -1 means "nothing pending, show what is stored".
|
// Shown while dragging; null means "nothing pending, show what is stored".
|
||||||
property real pending: -1
|
// Not -1: several schema entries (e.g. pointerSensitivity) are legitimately
|
||||||
readonly property real shown: root.pending >= 0 ? root.pending : root.stored
|
// negative, and -1 would be indistinguishable from a real committed value.
|
||||||
|
property var pending: null
|
||||||
|
readonly property real shown: root.pending !== null ? root.pending : root.stored
|
||||||
|
|
||||||
// Below this the label and a usable slider cannot share a line without one
|
// Below this the label and a usable slider cannot share a line without one
|
||||||
// of them becoming useless.
|
// of them becoming useless.
|
||||||
@@ -61,7 +63,7 @@ Item {
|
|||||||
|
|
||||||
// A fixed trailing width rather than a share of the row. A proportional
|
// A fixed trailing width rather than a share of the row. A proportional
|
||||||
// control looks reasonable at 900px and absurd at 4500px, where the slider
|
// control looks reasonable at 900px and absurd at 4500px, where the slider
|
||||||
// would be a metre long next to a two-word label -- and this window is
|
// would be a meter long next to a two-word label -- and this window is
|
||||||
// tiled, so it really can be that wide.
|
// tiled, so it really can be that wide.
|
||||||
readonly property int controlSpan: 300
|
readonly property int controlSpan: 300
|
||||||
|
|
||||||
@@ -176,7 +178,7 @@ Item {
|
|||||||
id: commitTimer
|
id: commitTimer
|
||||||
interval: 140
|
interval: 140
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (root.pending < 0)
|
if (root.pending === null)
|
||||||
return;
|
return;
|
||||||
SystemSettings.commitPreference(root.setting, root.pending);
|
SystemSettings.commitPreference(root.setting, root.pending);
|
||||||
// Hand the display back to the stored value. If the write was
|
// Hand the display back to the stored value. If the write was
|
||||||
@@ -188,6 +190,6 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: releaseTimer
|
id: releaseTimer
|
||||||
interval: 160
|
interval: 160
|
||||||
onTriggered: root.pending = -1
|
onTriggered: root.pending = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// setting it edits.
|
// setting it edits.
|
||||||
//
|
//
|
||||||
// Rejected input is shown as rejected rather than silently dropped or quietly
|
// Rejected input is shown as rejected rather than silently dropped or quietly
|
||||||
// sanitised. Several of these strings are serialised into an hl.config payload,
|
// sanitised. Several of these strings are serialized into an hl.config payload,
|
||||||
// where stripping an unexpected character would turn a typo into a different
|
// where stripping an unexpected character would turn a typo into a different
|
||||||
// working setting -- so the schema's pattern decides, the field turns red when
|
// working setting -- so the schema's pattern decides, the field turns red when
|
||||||
// it fails, and nothing is committed until it passes.
|
// it fails, and nothing is committed until it passes.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// clock. This is the same control with the readout formatted as a time, so the
|
// clock. This is the same control with the readout formatted as a time, so the
|
||||||
// night light schedule says "5:30 PM" rather than a number you have to convert.
|
// night light schedule says "5:30 PM" rather than a number you have to convert.
|
||||||
//
|
//
|
||||||
// Honours the 24-hour clock preference, because a user who has asked for 18:30
|
// Honors the 24-hour clock preference, because a user who has asked for 18:30
|
||||||
// everywhere else should not be shown 6:30 PM here.
|
// everywhere else should not be shown 6:30 PM here.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|||||||
@@ -61,3 +61,4 @@ PrivacyPage 1.0 PrivacyPage.qml
|
|||||||
RegionPage 1.0 RegionPage.qml
|
RegionPage 1.0 RegionPage.qml
|
||||||
SearchPicker 1.0 SearchPicker.qml
|
SearchPicker 1.0 SearchPicker.qml
|
||||||
OnlineAccountsPage 1.0 OnlineAccountsPage.qml
|
OnlineAccountsPage 1.0 OnlineAccountsPage.qml
|
||||||
|
AccentPicker 1.0 AccentPicker.qml
|
||||||
|
|||||||
@@ -255,7 +255,11 @@ def watch(start: int, end: int) -> int:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
loop = GLib.MainLoop()
|
loop = GLib.MainLoop()
|
||||||
|
try:
|
||||||
registry = EDataServer.SourceRegistry.new_sync(None)
|
registry = EDataServer.SourceRegistry.new_sync(None)
|
||||||
|
except Exception:
|
||||||
|
_write_snapshot(_unavailable_snapshot())
|
||||||
|
return 1
|
||||||
debounce_source = 0
|
debounce_source = 0
|
||||||
subscriptions: list[int] = []
|
subscriptions: list[int] = []
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ class DoctorConfig:
|
|||||||
runtime_dir: Path
|
runtime_dir: Path
|
||||||
path: str
|
path: str
|
||||||
timeout: float
|
timeout: float
|
||||||
|
# Defaults to this file's own directory, where its sibling helpers
|
||||||
|
# (panama-action, panama-brightness, calendar-agenda) actually live.
|
||||||
|
scripts_dir: Path = Path(__file__).resolve().parent
|
||||||
|
# Repair actions (service restarts, the Quickshell restart-shell action)
|
||||||
|
# can legitimately run longer than a quick health-check probe -- a
|
||||||
|
# Quickshell restart alone waits for the old process to exit, the new one
|
||||||
|
# to start, and settle. Reusing `timeout` here would kill a slow-but-
|
||||||
|
# successful repair and report it as failed even though a following
|
||||||
|
# health scan would show everything recovered. See run_repair_command.
|
||||||
|
repair_timeout: float = 15.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def command_env(self) -> dict[str, str]:
|
def command_env(self) -> dict[str, str]:
|
||||||
@@ -114,8 +124,13 @@ SYSTEMCTL_COMMANDS = {
|
|||||||
"hypridle": ("systemctl", "--user", "is-active", "--quiet", "hypridle.service"),
|
"hypridle": ("systemctl", "--user", "is-active", "--quiet", "hypridle.service"),
|
||||||
"vicinae": ("systemctl", "--user", "is-active", "--quiet", "vicinae.service"),
|
"vicinae": ("systemctl", "--user", "is-active", "--quiet", "vicinae.service"),
|
||||||
"pipewire": ("systemctl", "--user", "is-active", "--quiet", "pipewire.service"),
|
"pipewire": ("systemctl", "--user", "is-active", "--quiet", "pipewire.service"),
|
||||||
"nextcloud": ("systemctl", "--user", "is-active", "--quiet", "nextcloud.service"),
|
# System-scope, not --user: RustDesk ships an enabled *system* service
|
||||||
"rustdesk": ("systemctl", "--user", "is-active", "--quiet", "rustdesk.service"),
|
# (`rustdesk --service`, root-owned) that spawns the session --server and
|
||||||
|
# --tray on its own -- see autostart.lua's comment on why Panama doesn't
|
||||||
|
# start it a second time. Querying --user here always reports inactive,
|
||||||
|
# since no such user-scope unit exists, regardless of whether the real
|
||||||
|
# service is running.
|
||||||
|
"rustdesk": ("systemctl", "is-active", "--quiet", "rustdesk.service"),
|
||||||
}
|
}
|
||||||
REPAIR_COMMANDS = MappingProxyType({
|
REPAIR_COMMANDS = MappingProxyType({
|
||||||
"desktop.hyprpaper": ("systemctl", "--user", "restart", "hyprpaper.service"),
|
"desktop.hyprpaper": ("systemctl", "--user", "restart", "hyprpaper.service"),
|
||||||
@@ -193,11 +208,25 @@ def config_from_environment() -> DoctorConfig:
|
|||||||
state_home = environment_path("PANAMA_DOCTOR_STATE_HOME", Path(os.environ.get("XDG_STATE_HOME", home / ".local/state")))
|
state_home = environment_path("PANAMA_DOCTOR_STATE_HOME", Path(os.environ.get("XDG_STATE_HOME", home / ".local/state")))
|
||||||
runtime_dir = environment_path("PANAMA_DOCTOR_RUNTIME_DIR", Path(os.environ.get("XDG_RUNTIME_DIR", "/run/user/0")))
|
runtime_dir = environment_path("PANAMA_DOCTOR_RUNTIME_DIR", Path(os.environ.get("XDG_RUNTIME_DIR", "/run/user/0")))
|
||||||
root = environment_path("PANAMA_DOCTOR_ROOT", Path(__file__).resolve().parents[4])
|
root = environment_path("PANAMA_DOCTOR_ROOT", Path(__file__).resolve().parents[4])
|
||||||
|
# Sibling helpers (panama-action, panama-brightness, calendar-agenda) live
|
||||||
|
# next to this file. PATH is not a reliable way to find them -- nothing in
|
||||||
|
# this repository puts the Quickshell scripts directory on PATH -- so they
|
||||||
|
# are invoked by resolved path instead, the same way check_hyprlock already
|
||||||
|
# resolves panama-lock.
|
||||||
|
scripts_dir = environment_path("PANAMA_DOCTOR_SCRIPTS_DIR", Path(__file__).resolve().parent)
|
||||||
try:
|
try:
|
||||||
timeout = float(os.environ.get("PANAMA_DOCTOR_TIMEOUT", "3"))
|
timeout = float(os.environ.get("PANAMA_DOCTOR_TIMEOUT", "3"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
timeout = 3.0
|
timeout = 3.0
|
||||||
return DoctorConfig(root, home, config_home, state_home, runtime_dir, os.environ.get("PANAMA_DOCTOR_PATH", os.environ.get("PATH", "")), max(0.05, min(timeout, 15.0)))
|
timeout = max(0.05, min(timeout, 15.0))
|
||||||
|
try:
|
||||||
|
repair_timeout = float(os.environ.get("PANAMA_DOCTOR_REPAIR_TIMEOUT", "15"))
|
||||||
|
except ValueError:
|
||||||
|
repair_timeout = 15.0
|
||||||
|
# Never shorter than the probe timeout, and bounded so a hung repair still
|
||||||
|
# gives up rather than blocking the caller indefinitely.
|
||||||
|
repair_timeout = max(timeout, min(repair_timeout, 30.0))
|
||||||
|
return DoctorConfig(root, home, config_home, state_home, runtime_dir, os.environ.get("PANAMA_DOCTOR_PATH", os.environ.get("PATH", "")), timeout, scripts_dir, repair_timeout)
|
||||||
|
|
||||||
|
|
||||||
def run_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> CommandResult:
|
def run_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> CommandResult:
|
||||||
@@ -222,7 +251,7 @@ def run_repair_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path
|
|||||||
command,
|
command,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=config.timeout,
|
timeout=config.repair_timeout,
|
||||||
check=False,
|
check=False,
|
||||||
env=config.command_env,
|
env=config.command_env,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
@@ -274,6 +303,16 @@ def service_check(check_id: str, title: str, service: str, config: DoctorConfig,
|
|||||||
return Check(check_id, group_for(check_id), title, "warning", "Service is not active.", action)
|
return Check(check_id, group_for(check_id), title, "warning", "Service is not active.", action)
|
||||||
|
|
||||||
|
|
||||||
|
def process_check(check_id: str, title: str, process: str, config: DoctorConfig, action: Action | None = None) -> Check:
|
||||||
|
"""Like service_check, for autostarted apps with no systemd unit behind them."""
|
||||||
|
result = run_command(("pgrep", "-u", str(os.getuid()), "-x", process), config)
|
||||||
|
if result.state == "ok":
|
||||||
|
return Check(check_id, group_for(check_id), title, "ok", "Process is running.")
|
||||||
|
if result.state in {"missing", "unavailable"}:
|
||||||
|
return Check(check_id, group_for(check_id), title, "error", "Required process probe is unavailable.")
|
||||||
|
return Check(check_id, group_for(check_id), title, "warning", "Process is not running.", action)
|
||||||
|
|
||||||
|
|
||||||
def ipc_target(config: DoctorConfig, target: str) -> CommandResult:
|
def ipc_target(config: DoctorConfig, target: str) -> CommandResult:
|
||||||
result = run_command(("qs", "ipc", "show"), config)
|
result = run_command(("qs", "ipc", "show"), config)
|
||||||
if result.state != "ok":
|
if result.state != "ok":
|
||||||
@@ -357,7 +396,7 @@ def check_hyprlock(config: DoctorConfig) -> Check:
|
|||||||
|
|
||||||
|
|
||||||
def check_brightness(config: DoctorConfig) -> Check:
|
def check_brightness(config: DoctorConfig) -> Check:
|
||||||
result = run_command(("panama-brightness", "list"), config)
|
result = run_command((str(config.scripts_dir / "panama-brightness"), "list"), config)
|
||||||
instructions = Action("instructions", "View setup instructions", target="ddc-permissions")
|
instructions = Action("instructions", "View setup instructions", target="ddc-permissions")
|
||||||
if result.state == "timeout":
|
if result.state == "timeout":
|
||||||
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI probe timed out.", instructions)
|
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI probe timed out.", instructions)
|
||||||
@@ -378,9 +417,18 @@ def check_brightness(config: DoctorConfig) -> Check:
|
|||||||
|
|
||||||
|
|
||||||
def check_nextcloud(config: DoctorConfig) -> Check:
|
def check_nextcloud(config: DoctorConfig) -> Check:
|
||||||
if not (config.config_home / "autostart" / "nextcloud.desktop").is_file():
|
# The client's autostart entry is Title-cased ("Nextcloud.desktop") on
|
||||||
|
# current nextcloud-client packages, but that has changed case before --
|
||||||
|
# match either so a future package update doesn't silently reintroduce
|
||||||
|
# this as "autostart is not configured" again.
|
||||||
|
autostart_dir = config.config_home / "autostart"
|
||||||
|
if not any(autostart_dir.glob("[Nn]extcloud.desktop")):
|
||||||
return Check("integration.nextcloud", "integrations", "Nextcloud", "unconfigured", "Nextcloud autostart is not configured.")
|
return Check("integration.nextcloud", "integrations", "Nextcloud", "unconfigured", "Nextcloud autostart is not configured.")
|
||||||
return service_check("integration.nextcloud", "Nextcloud", "nextcloud", config, Action("open", "Open Nextcloud"))
|
# No systemd unit backs this (see the autostart.lua comment on why Panama
|
||||||
|
# doesn't start it as one): the client is a plain autostarted process, so
|
||||||
|
# check for the process directly instead of a systemctl service that was
|
||||||
|
# never going to exist.
|
||||||
|
return process_check("integration.nextcloud", "Nextcloud", "nextcloud", config, Action("open", "Open Nextcloud"))
|
||||||
|
|
||||||
|
|
||||||
def check_rustdesk(config: DoctorConfig) -> Check:
|
def check_rustdesk(config: DoctorConfig) -> Check:
|
||||||
@@ -418,7 +466,7 @@ def check_home_assistant(config: DoctorConfig) -> Check:
|
|||||||
|
|
||||||
|
|
||||||
def check_calendar(config: DoctorConfig) -> Check:
|
def check_calendar(config: DoctorConfig) -> Check:
|
||||||
result = run_command(("calendar-agenda", "probe"), config)
|
result = run_command((str(config.scripts_dir / "calendar-agenda"), "probe"), config)
|
||||||
action = Action("open", "Open Date & Time", target="datetime")
|
action = Action("open", "Open Date & Time", target="datetime")
|
||||||
if result.state == "missing":
|
if result.state == "missing":
|
||||||
return Check("integration.calendar", "integrations", "Calendar", "unconfigured", "Calendar integration is not installed.")
|
return Check("integration.calendar", "integrations", "Calendar", "unconfigured", "Calendar integration is not installed.")
|
||||||
@@ -583,6 +631,11 @@ def snapshot(config: DoctorConfig) -> dict[str, object]:
|
|||||||
|
|
||||||
def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult:
|
def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult:
|
||||||
command = REPAIR_COMMANDS[check_id]
|
command = REPAIR_COMMANDS[check_id]
|
||||||
|
if check_id == "desktop.quickshell":
|
||||||
|
# panama-action is a sibling helper script, not a PATH-resolved
|
||||||
|
# executable; see check_brightness and check_calendar for the same
|
||||||
|
# resolution against the same bug.
|
||||||
|
command = (str(config.scripts_dir / command[0]), *command[1:])
|
||||||
exit_code, _ = run_repair_command(command, config)
|
exit_code, _ = run_repair_command(command, config)
|
||||||
message = "Repair completed. A fresh health check will verify recovery." if exit_code == 0 \
|
message = "Repair completed. A fresh health check will verify recovery." if exit_code == 0 \
|
||||||
else "The authored repair command could not be completed."
|
else "The authored repair command could not be completed."
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ generated="$state_dir/hypridle.conf"
|
|||||||
dropin_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/hypridle.service.d"
|
dropin_dir="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/hypridle.service.d"
|
||||||
dropin="$dropin_dir/panama.conf"
|
dropin="$dropin_dir/panama.conf"
|
||||||
|
|
||||||
|
# Set by generate() to the mktemp path it is currently writing, so concurrent
|
||||||
|
# invocations (e.g. rapid settings changes each spawning `apply`) never share
|
||||||
|
# a tmp file and interleave writes into a corrupt hypridle.conf. Cleared once
|
||||||
|
# the atomic mv below lands, so this is a no-op on a normal exit.
|
||||||
|
generated_tmp=""
|
||||||
|
cleanup() { rm -f "$generated_tmp" 2>/dev/null || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
read_setting() {
|
read_setting() {
|
||||||
local key="$1" fallback="$2"
|
local key="$1" fallback="$2"
|
||||||
[[ -r "$settings" ]] || { printf '%s' "$fallback"; return; }
|
[[ -r "$settings" ]] || { printf '%s' "$fallback"; return; }
|
||||||
@@ -53,6 +61,10 @@ generate() {
|
|||||||
load
|
load
|
||||||
mkdir -p "$state_dir"
|
mkdir -p "$state_dir"
|
||||||
|
|
||||||
|
# Unique per invocation, in the same directory as the destination so the
|
||||||
|
# final mv is an atomic same-filesystem rename rather than a copy.
|
||||||
|
generated_tmp="$(mktemp "$generated.XXXXXX")"
|
||||||
|
|
||||||
{
|
{
|
||||||
printf '# Generated by panama-idle from %s\n' "$settings"
|
printf '# Generated by panama-idle from %s\n' "$settings"
|
||||||
printf '# Do not edit: it is rewritten whenever the idle settings change.\n'
|
printf '# Do not edit: it is rewritten whenever the idle settings change.\n'
|
||||||
@@ -91,9 +103,10 @@ generate() {
|
|||||||
printf ' on-timeout = systemctl suspend\n'
|
printf ' on-timeout = systemctl suspend\n'
|
||||||
printf '}\n'
|
printf '}\n'
|
||||||
fi
|
fi
|
||||||
} >"$generated.tmp"
|
} >"$generated_tmp"
|
||||||
|
|
||||||
mv "$generated.tmp" "$generated"
|
mv "$generated_tmp" "$generated"
|
||||||
|
generated_tmp=""
|
||||||
}
|
}
|
||||||
|
|
||||||
install_dropin() {
|
install_dropin() {
|
||||||
|
|||||||
@@ -39,13 +39,36 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def daemon_origin():
|
def secrets_name_owner_pid():
|
||||||
"""Whether the running secrets daemon came from PAM or from D-Bus activation.
|
"""PID currently owning the org.freedesktop.secrets D-Bus name, if any.
|
||||||
|
|
||||||
A D-Bus-activated daemon is the signature of the crash-and-replace case
|
This is the only reliable way to identify which daemon actually answers
|
||||||
above: it is the one that cannot have the login password. PAM's daemon lives
|
Secret Service calls right now.
|
||||||
outside the app slice, so the cgroup tells the two apart.
|
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gio", "2.0")
|
||||||
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
|
result = bus.call_sync(
|
||||||
|
"org.freedesktop.DBus",
|
||||||
|
"/org/freedesktop/DBus",
|
||||||
|
"org.freedesktop.DBus",
|
||||||
|
"GetConnectionUnixProcessID",
|
||||||
|
GLib.Variant("(s)", ("org.freedesktop.secrets",)),
|
||||||
|
GLib.VariantType("(u)"),
|
||||||
|
Gio.DBusCallFlags.NONE,
|
||||||
|
-1,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
return result.unpack()[0]
|
||||||
|
except Exception: # noqa: BLE001 - no name owner is a legitimate state
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def any_keyring_daemon_running():
|
||||||
try:
|
try:
|
||||||
for pid in os.listdir("/proc"):
|
for pid in os.listdir("/proc"):
|
||||||
if not pid.isdigit():
|
if not pid.isdigit():
|
||||||
@@ -55,19 +78,46 @@ def daemon_origin():
|
|||||||
cmdline = handle.read().decode("utf-8", "replace")
|
cmdline = handle.read().decode("utf-8", "replace")
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
if "gnome-keyring-daemon" not in cmdline:
|
if "gnome-keyring-daemon" in cmdline:
|
||||||
continue
|
return True
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def daemon_origin():
|
||||||
|
"""Whether the running secrets daemon came from PAM or from D-Bus activation.
|
||||||
|
|
||||||
|
A D-Bus-activated daemon is the signature of the crash-and-replace case
|
||||||
|
above: it is the one that cannot have the login password. PAM's daemon lives
|
||||||
|
outside the app slice, so the cgroup tells the two apart.
|
||||||
|
|
||||||
|
A machine can have two gnome-keyring-daemon processes at once -- a
|
||||||
|
lingering PAM one alongside its D-Bus-activated replacement -- so which
|
||||||
|
process this reports on matters: it must be the one that actually owns
|
||||||
|
org.freedesktop.secrets right now, not merely the first one /proc happens
|
||||||
|
to enumerate.
|
||||||
|
"""
|
||||||
|
owner_pid = secrets_name_owner_pid()
|
||||||
|
if owner_pid is None:
|
||||||
|
return "unknown" if any_keyring_daemon_running() else "none"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(f"/proc/{pid}/cgroup", "r") as handle:
|
with open(f"/proc/{owner_pid}/cmdline", "rb") as handle:
|
||||||
|
cmdline = handle.read().decode("utf-8", "replace")
|
||||||
|
except OSError:
|
||||||
|
return "unknown"
|
||||||
|
if "gnome-keyring-daemon" not in cmdline:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(f"/proc/{owner_pid}/cgroup", "r") as handle:
|
||||||
cgroup = handle.read()
|
cgroup = handle.read()
|
||||||
except OSError:
|
except OSError:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
if re.search(r"dbus-.*org\.freedesktop\.secrets", cgroup):
|
if re.search(r"dbus-.*org\.freedesktop\.secrets", cgroup):
|
||||||
return "dbus"
|
return "dbus"
|
||||||
return "pam"
|
return "pam"
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
return "none"
|
|
||||||
|
|
||||||
|
|
||||||
def load_service():
|
def load_service():
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Generates Panama's hyprlock configuration into the state directory. The
|
# Generates Panama's hyprlock configuration into the state directory. The
|
||||||
# tracked config is never rewritten and remains the fallback if generation
|
# gitignored fallback config (rendered from the tracked .template at link
|
||||||
# fails, so a malformed preference can never leave the session without a
|
# time) is never rewritten by this script and remains the fallback if
|
||||||
# working locker.
|
# generation fails, so a malformed preference can never leave the session
|
||||||
|
# without a working locker.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -78,6 +79,31 @@ valid_path() {
|
|||||||
[[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* ]]
|
[[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by
|
||||||
|
# hand -- there is no shared source between QML and a shell script), primary
|
||||||
|
# hue per scheme only: the lock screen shows a flat focus ring, not the
|
||||||
|
# two-stop gradient the window border does. Falls back to blue for an unknown
|
||||||
|
# name, matching Theme.accentPair's own fallback.
|
||||||
|
accent_hex() {
|
||||||
|
local name="$1" scheme="$2"
|
||||||
|
case "$name" in
|
||||||
|
orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;;
|
||||||
|
teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;;
|
||||||
|
green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;;
|
||||||
|
amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;;
|
||||||
|
orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;;
|
||||||
|
rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;;
|
||||||
|
slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;;
|
||||||
|
*) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# hyprlock wants "R, G, B" decimal, not hex.
|
||||||
|
hex_to_rgb() {
|
||||||
|
local hex="$1"
|
||||||
|
printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
|
||||||
|
}
|
||||||
|
|
||||||
load_preferences() {
|
load_preferences() {
|
||||||
if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
|
if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
|
||||||
settings_valid=true
|
settings_valid=true
|
||||||
@@ -99,6 +125,12 @@ load_preferences() {
|
|||||||
color_scheme="$(read_string colorScheme dark)"
|
color_scheme="$(read_string colorScheme dark)"
|
||||||
[[ "$color_scheme" == dark || "$color_scheme" == light ]] || color_scheme=dark
|
[[ "$color_scheme" == dark || "$color_scheme" == light ]] || color_scheme=dark
|
||||||
|
|
||||||
|
accent_name="$(read_string accentName blue)"
|
||||||
|
case "$accent_name" in
|
||||||
|
blue|orchid|teal|green|amber|orange|rose|slate) ;;
|
||||||
|
*) accent_name=blue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
wallpaper_mode="$(read_string wallpaperMode single)"
|
wallpaper_mode="$(read_string wallpaperMode single)"
|
||||||
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|
||||||
|| wallpaper_mode=single
|
|| wallpaper_mode=single
|
||||||
@@ -121,8 +153,6 @@ load_preferences() {
|
|||||||
background_color='rgba(225, 226, 231, 1.0)'
|
background_color='rgba(225, 226, 231, 1.0)'
|
||||||
foreground_color='rgba(55, 96, 191, 1.0)'
|
foreground_color='rgba(55, 96, 191, 1.0)'
|
||||||
dim_color='rgba(97, 114, 176, 1.0)'
|
dim_color='rgba(97, 114, 176, 1.0)'
|
||||||
accent_color='rgba(46, 125, 233, 1.0)'
|
|
||||||
accent_ring_color='rgba(46, 125, 233, 0.9)'
|
|
||||||
error_color='rgba(245, 42, 101, 1.0)'
|
error_color='rgba(245, 42, 101, 1.0)'
|
||||||
field_color='rgba(208, 213, 227, 0.85)'
|
field_color='rgba(208, 213, 227, 0.85)'
|
||||||
dim_hex='6172b0'
|
dim_hex='6172b0'
|
||||||
@@ -131,13 +161,18 @@ load_preferences() {
|
|||||||
background_color='rgba(34, 36, 54, 1.0)'
|
background_color='rgba(34, 36, 54, 1.0)'
|
||||||
foreground_color='rgba(200, 211, 245, 1.0)'
|
foreground_color='rgba(200, 211, 245, 1.0)'
|
||||||
dim_color='rgba(130, 139, 184, 1.0)'
|
dim_color='rgba(130, 139, 184, 1.0)'
|
||||||
accent_color='rgba(130, 170, 255, 1.0)'
|
|
||||||
accent_ring_color='rgba(130, 170, 255, 0.9)'
|
|
||||||
error_color='rgba(255, 117, 127, 1.0)'
|
error_color='rgba(255, 117, 127, 1.0)'
|
||||||
field_color='rgba(46, 47, 61, 0.85)'
|
field_color='rgba(46, 47, 61, 0.85)'
|
||||||
dim_hex='828bb8'
|
dim_hex='828bb8'
|
||||||
error_hex='ff757f'
|
error_hex='ff757f'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The focus ring is the accent role, not a scheme-relative one: it follows
|
||||||
|
# accentName the same way ColorScheme.qml's active_border does, and needs
|
||||||
|
# both the accent and the scheme since each accent carries a separate pair.
|
||||||
|
accent_rgb="$(hex_to_rgb "$(accent_hex "$accent_name" "$color_scheme")")"
|
||||||
|
accent_color="rgba($accent_rgb, 1.0)"
|
||||||
|
accent_ring_color="rgba($accent_rgb, 0.9)"
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor_names() {
|
monitor_names() {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ adjust_microphone() {
|
|||||||
|
|
||||||
brightness_percent() {
|
brightness_percent() {
|
||||||
local output="$1" percent
|
local output="$1" percent
|
||||||
percent="$(awk -F, 'NR == 1 { value=$5; gsub(/%/, "", value); print value }' <<<"$output")"
|
percent="$(awk -F, 'NR == 1 { value=$4; gsub(/%/, "", value); print value }' <<<"$output")"
|
||||||
[[ $percent =~ ^[0-9]+$ ]] || return 1
|
[[ $percent =~ ^[0-9]+$ ]] || return 1
|
||||||
printf '%s\n' "$percent"
|
printf '%s\n' "$percent"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,15 +67,19 @@ cmd_list() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_set() {
|
cmd_set() {
|
||||||
local profile="${1:-}"
|
local profile="${1:-}" status
|
||||||
# Constrained rather than passed through: this reaches a system service.
|
# Constrained rather than passed through: this reaches a system service.
|
||||||
[[ "$profile" =~ ^[a-z-]+$ ]] || {
|
[[ "$profile" =~ ^[a-z-]+$ ]] || {
|
||||||
printf 'panama-power-profile: refusing a profile name with unexpected characters\n' >&2
|
printf 'panama-power-profile: refusing a profile name with unexpected characters\n' >&2
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
# pipefail is set above, so $? here is busctl's real exit status, not
|
||||||
|
# head's -- a failed write (daemon stopped, profile rejected) must be
|
||||||
|
# reported rather than always claimed as a success.
|
||||||
busctl set-property "$BUS_NAME" "$OBJECT" "$BUS_NAME" ActiveProfile s "$profile" 2>&1 >/dev/null \
|
busctl set-property "$BUS_NAME" "$OBJECT" "$BUS_NAME" ActiveProfile s "$profile" 2>&1 >/dev/null \
|
||||||
| head -2 >&2
|
| head -2 >&2
|
||||||
return 0
|
status=$?
|
||||||
|
return "$status"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-list}" in
|
case "${1:-list}" in
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def valid_home(value: Any) -> bool:
|
|||||||
|
|
||||||
def validate_home(value: Any, label: str) -> dict[str, Any]:
|
def validate_home(value: Any, label: str) -> dict[str, Any]:
|
||||||
if not valid_home(value):
|
if not valid_home(value):
|
||||||
fail(f"{label} does not contain valid Home favourites.")
|
fail(f"{label} does not contain valid Home favorites.")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Propagates the colour scheme to applications that do not read the desktop
|
# Propagates the color scheme to applications that do not read the desktop
|
||||||
# portal.
|
# portal.
|
||||||
#
|
#
|
||||||
# Most modern applications DO follow org.freedesktop.portal.Settings and need
|
# Most modern applications DO follow org.freedesktop.portal.Settings and need
|
||||||
@@ -16,21 +16,61 @@
|
|||||||
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
|
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
|
||||||
# scheme in light mode, so it is generated from a template here instead.
|
# scheme in light mode, so it is generated from a template here instead.
|
||||||
#
|
#
|
||||||
# panama-theme-apps dark|light
|
# panama-theme-apps dark|light [accent]
|
||||||
#
|
#
|
||||||
# kitty gets it twice: the generated include file so terminals opened later
|
# kitty gets it twice: the generated include file so terminals opened later
|
||||||
# start correct, and a live `set-colors` over its control socket so terminals
|
# start correct, and a live `set-colors` over its control socket so terminals
|
||||||
# already open change now. Without the second, a scheme change appears to do
|
# already open change now. Without the second, a scheme change appears to do
|
||||||
# nothing until you open a new window.
|
# nothing until you open a new window.
|
||||||
|
#
|
||||||
|
# kitty's active_border_color and hyprlock's focus ring are also the accent
|
||||||
|
# role, not just the scheme -- see accent_hex() below, which mirrors the same
|
||||||
|
# lookup hypr/looks.lua and scripts/panama-lock carry for the same reason.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
scheme="${1:-dark}"
|
scheme="${1:-dark}"
|
||||||
case "$scheme" in
|
case "$scheme" in
|
||||||
dark|light) ;;
|
dark|light) ;;
|
||||||
*) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;;
|
*) printf 'usage: panama-theme-apps [dark|light] [accent]\n' >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Falls back to blue for an unknown or omitted name, matching Theme.qml's own
|
||||||
|
# accentPair fallback -- so a caller that only ever knew about scheme (a stale
|
||||||
|
# ColorScheme.qml, a manual invocation) still gets a real accent instead of an
|
||||||
|
# empty string reaching sed.
|
||||||
|
accent="${2:-blue}"
|
||||||
|
case "$accent" in
|
||||||
|
blue|orchid|teal|green|amber|orange|rose|slate) ;;
|
||||||
|
*) accent=blue ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by
|
||||||
|
# hand -- there is no shared source between QML and a shell script), primary
|
||||||
|
# hue per scheme only: both consumers below draw a flat color, not the
|
||||||
|
# two-stop gradient the window border does.
|
||||||
|
accent_hex() {
|
||||||
|
local name="$1" scheme="$2"
|
||||||
|
case "$name" in
|
||||||
|
orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;;
|
||||||
|
teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;;
|
||||||
|
green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;;
|
||||||
|
amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;;
|
||||||
|
orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;;
|
||||||
|
rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;;
|
||||||
|
slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;;
|
||||||
|
*) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# hyprlock wants "R, G, B" decimal, not hex.
|
||||||
|
hex_to_rgb() {
|
||||||
|
local hex="$1"
|
||||||
|
printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
accent_border_hex="$(accent_hex "$accent" "$scheme")"
|
||||||
|
|
||||||
# ── hyprlock ─────────────────────────────────────────────────────────────────
|
# ── hyprlock ─────────────────────────────────────────────────────────────────
|
||||||
# The lock screen. hyprlock is launched fresh on every lock (`pidof hyprlock ||
|
# The lock screen. hyprlock is launched fresh on every lock (`pidof hyprlock ||
|
||||||
# hyprlock`), so it reads this file each time and needs no restart.
|
# hyprlock`), so it reads this file each time and needs no restart.
|
||||||
@@ -45,7 +85,6 @@ lock_template="$lock_dir/hyprlock.conf.template"
|
|||||||
if [[ "$scheme" == "light" ]]; then
|
if [[ "$scheme" == "light" ]]; then
|
||||||
lock_fg="55, 96, 191" # #3760bf
|
lock_fg="55, 96, 191" # #3760bf
|
||||||
lock_muted="97, 114, 176" # #6172b0
|
lock_muted="97, 114, 176" # #6172b0
|
||||||
lock_accent="46, 125, 233" # #2e7de9
|
|
||||||
lock_error="245, 42, 101" # #f52a65
|
lock_error="245, 42, 101" # #f52a65
|
||||||
lock_bg="225, 226, 231" # #e1e2e7
|
lock_bg="225, 226, 231" # #e1e2e7
|
||||||
lock_field="208, 213, 227" # #d0d5e3
|
lock_field="208, 213, 227" # #d0d5e3
|
||||||
@@ -54,7 +93,6 @@ if [[ "$scheme" == "light" ]]; then
|
|||||||
else
|
else
|
||||||
lock_fg="200, 211, 245" # #c8d3f5
|
lock_fg="200, 211, 245" # #c8d3f5
|
||||||
lock_muted="130, 139, 184" # #828bb8
|
lock_muted="130, 139, 184" # #828bb8
|
||||||
lock_accent="130, 170, 255" # #82aaff
|
|
||||||
lock_error="255, 117, 127" # #ff757f
|
lock_error="255, 117, 127" # #ff757f
|
||||||
lock_bg="34, 36, 54" # #222436
|
lock_bg="34, 36, 54" # #222436
|
||||||
lock_field="46, 47, 61" # #2e2f3d
|
lock_field="46, 47, 61" # #2e2f3d
|
||||||
@@ -62,11 +100,15 @@ else
|
|||||||
lock_error_hex="ff757f"
|
lock_error_hex="ff757f"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The focus ring is the accent role, not a scheme-relative one -- follows
|
||||||
|
# accentName the same way scripts/panama-lock's own generator does.
|
||||||
|
lock_accent="$(hex_to_rgb "$accent_border_hex")"
|
||||||
|
|
||||||
status_hyprlock="skipped"
|
status_hyprlock="skipped"
|
||||||
if [[ -r "$lock_template" ]]; then
|
if [[ -r "$lock_template" ]]; then
|
||||||
# Written atomically: a lock triggered mid-write would otherwise read a
|
# Written atomically: a lock triggered mid-write would otherwise read a
|
||||||
# truncated config and fall back to hyprlock's own defaults, which is a
|
# truncated config and fall back to hyprlock's own defaults, which is a
|
||||||
# bright grey screen with none of this desktop's identity.
|
# bright gray screen with none of this desktop's identity.
|
||||||
if sed -e "s/@FG@/$lock_fg/g" \
|
if sed -e "s/@FG@/$lock_fg/g" \
|
||||||
-e "s/@MUTED@/$lock_muted/g" \
|
-e "s/@MUTED@/$lock_muted/g" \
|
||||||
-e "s/@ACCENT@/$lock_accent/g" \
|
-e "s/@ACCENT@/$lock_accent/g" \
|
||||||
@@ -117,7 +159,7 @@ fi
|
|||||||
# it on the next write.
|
# it on the next write.
|
||||||
#
|
#
|
||||||
# btop reads its theme once at startup, so a running instance keeps the old
|
# btop reads its theme once at startup, so a running instance keeps the old
|
||||||
# colours until it is restarted. That is acceptable for a monitor you open when
|
# colors until it is restarted. That is acceptable for a monitor you open when
|
||||||
# you want it, and forcing a restart would kill a process the user is watching.
|
# you want it, and forcing a restart would kill a process the user is watching.
|
||||||
btop_conf="${XDG_CONFIG_HOME:-$HOME/.config}/btop/btop.conf"
|
btop_conf="${XDG_CONFIG_HOME:-$HOME/.config}/btop/btop.conf"
|
||||||
btop_theme="tokyonight-moon"
|
btop_theme="tokyonight-moon"
|
||||||
@@ -169,9 +211,16 @@ theme_file="$kitty_dir/themes/tokyonight-moon.conf"
|
|||||||
status_kitty="skipped"
|
status_kitty="skipped"
|
||||||
if [[ -r "$theme_file" ]]; then
|
if [[ -r "$theme_file" ]]; then
|
||||||
# Written atomically: kitty may read this while a new window is starting.
|
# Written atomically: kitty may read this while a new window is starting.
|
||||||
|
# The accent override is appended after the copied theme rather than
|
||||||
|
# edited into the tracked theme file: kitty applies settings top to
|
||||||
|
# bottom, so a later active_border_color line wins, and this file is
|
||||||
|
# itself generated -- the tracked per-scheme theme stays scheme-only.
|
||||||
if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \
|
if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \
|
||||||
|
&& printf 'active_border_color #%s\n' "$accent_border_hex" >>"$kitty_dir/current-theme.conf.tmp" \
|
||||||
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
|
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
|
||||||
status_kitty="written"
|
status_kitty="written"
|
||||||
|
else
|
||||||
|
rm -f "$kitty_dir/current-theme.conf.tmp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Live-apply to running terminals. kitty appends its PID to the socket name
|
# Live-apply to running terminals. kitty appends its PID to the socket name
|
||||||
@@ -182,7 +231,8 @@ if [[ -r "$theme_file" ]]; then
|
|||||||
applied=0
|
applied=0
|
||||||
while read -r socket; do
|
while read -r socket; do
|
||||||
[[ -n "$socket" ]] || continue
|
[[ -n "$socket" ]] || continue
|
||||||
if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1; then
|
if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1 \
|
||||||
|
&& kitty @ --to "unix:@${socket#@}" set-colors --override "active_border_color=#$accent_border_hex" >/dev/null 2>&1; then
|
||||||
applied=$((applied + 1))
|
applied=$((applied + 1))
|
||||||
fi
|
fi
|
||||||
done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u)
|
done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u)
|
||||||
|
|||||||
@@ -26,6 +26,16 @@
|
|||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
|
# The payload file (created in cmd_qr) is tracked at script scope so it can be
|
||||||
|
# removed no matter how the script exits -- success, an emit_error exit 0, or a
|
||||||
|
# signal -- rather than only on a clean function return.
|
||||||
|
payload_file=""
|
||||||
|
|
||||||
|
cleanup_payload() {
|
||||||
|
[[ -n $payload_file ]] && rm -f -- "$payload_file"
|
||||||
|
}
|
||||||
|
trap cleanup_payload EXIT
|
||||||
|
|
||||||
emit_error() {
|
emit_error() {
|
||||||
printf '{"networks":[],"path":"","error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
|
printf '{"networks":[],"path":"","error":%s}\n' "$(jq -Rn --arg e "$1" '$e')"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -35,22 +45,33 @@ command -v nmcli >/dev/null 2>&1 || emit_error 'NetworkManager is not available'
|
|||||||
command -v qrencode >/dev/null 2>&1 || emit_error 'qrencode is not installed, so a Wi-Fi QR code cannot be drawn'
|
command -v qrencode >/dev/null 2>&1 || emit_error 'qrencode is not installed, so a Wi-Fi QR code cannot be drawn'
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
local rows=() name ssid psk
|
local rows=() name type ssid psk
|
||||||
while IFS= read -r name; do
|
while IFS= read -r name; do
|
||||||
[[ -n "$name" ]] || continue
|
[[ -n "$name" ]] || continue
|
||||||
ssid="$(nmcli -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
|
|
||||||
|
# nmcli's terse mode backslash-escapes ':' and '\' WITHIN a field so a
|
||||||
|
# combined NAME,TYPE line stays splittable -- but a plain awk -F:
|
||||||
|
# doesn't know that, so a name containing either character (e.g.
|
||||||
|
# "Cafe: Guest") gets split in the wrong place and TYPE no longer
|
||||||
|
# lines up, silently dropping the connection from this list. Querying
|
||||||
|
# one field at a time with escaping turned off (-e no) sidesteps the
|
||||||
|
# problem entirely: there's nothing to split, so each value comes
|
||||||
|
# back exactly as stored.
|
||||||
|
type="$(nmcli -e no -g connection.type connection show "$name" 2>/dev/null)"
|
||||||
|
[[ "$type" == "802-11-wireless" ]] || continue
|
||||||
|
|
||||||
|
ssid="$(nmcli -e no -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
|
||||||
[[ -n "$ssid" ]] || ssid="$name"
|
[[ -n "$ssid" ]] || ssid="$name"
|
||||||
|
|
||||||
# Only networks whose passphrase this user can actually read are
|
# Only networks whose passphrase this user can actually read are
|
||||||
# shareable. An enterprise network has no passphrase to share at all,
|
# shareable. An enterprise network has no passphrase to share at all,
|
||||||
# and a QR code for one would simply not work.
|
# and a QR code for one would simply not work.
|
||||||
psk="$(nmcli -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null)"
|
psk="$(nmcli -e no -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null)"
|
||||||
|
|
||||||
rows+=("$(jq -cn --arg name "$name" --arg ssid "$ssid" \
|
rows+=("$(jq -cn --arg name "$name" --arg ssid "$ssid" \
|
||||||
--argjson shareable "$([[ -n "$psk" ]] && echo true || echo false)" \
|
--argjson shareable "$([[ -n "$psk" ]] && echo true || echo false)" \
|
||||||
'{name: $name, ssid: $ssid, shareable: $shareable}')")
|
'{name: $name, ssid: $ssid, shareable: $shareable}')")
|
||||||
done < <(nmcli -t -f NAME,TYPE connection show 2>/dev/null \
|
done < <(nmcli -e no -t -f NAME connection show 2>/dev/null)
|
||||||
| awk -F: '$2 == "802-11-wireless" { print $1 }')
|
|
||||||
|
|
||||||
if [[ ${#rows[@]} -eq 0 ]]; then
|
if [[ ${#rows[@]} -eq 0 ]]; then
|
||||||
printf '{"networks":[],"path":"","error":"No saved Wi-Fi networks."}\n'
|
printf '{"networks":[],"path":"","error":"No saved Wi-Fi networks."}\n'
|
||||||
@@ -77,11 +98,16 @@ cmd_qr() {
|
|||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
[[ -n "$name" ]] || emit_error 'no network named'
|
[[ -n "$name" ]] || emit_error 'no network named'
|
||||||
|
|
||||||
local ssid hidden psk_file payload_file out_dir out_file
|
local ssid hidden psk_file out_dir out_file
|
||||||
ssid="$(nmcli -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
|
# -e no here too: $name is the literal connection name (list emits it
|
||||||
|
# un-escaped -- see cmd_list), and nmcli's terse escaping is a one-way
|
||||||
|
# transform on VALUES, not something connection-show lookups expect on
|
||||||
|
# their NAME argument. Escaping $ssid/$psk here would feed escape_field
|
||||||
|
# an already-escaped value below and double-escape it.
|
||||||
|
ssid="$(nmcli -e no -g 802-11-wireless.ssid connection show "$name" 2>/dev/null)"
|
||||||
[[ -n "$ssid" ]] || emit_error "There is no saved network called \"$name\"."
|
[[ -n "$ssid" ]] || emit_error "There is no saved network called \"$name\"."
|
||||||
|
|
||||||
hidden="$(nmcli -g 802-11-wireless.hidden connection show "$name" 2>/dev/null)"
|
hidden="$(nmcli -e no -g 802-11-wireless.hidden connection show "$name" 2>/dev/null)"
|
||||||
[[ "$hidden" == "yes" ]] && hidden=true || hidden=false
|
[[ "$hidden" == "yes" ]] && hidden=true || hidden=false
|
||||||
|
|
||||||
out_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama"
|
out_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama"
|
||||||
@@ -96,13 +122,12 @@ cmd_qr() {
|
|||||||
# Built in a file rather than a variable that could be echoed, and piped to
|
# Built in a file rather than a variable that could be echoed, and piped to
|
||||||
# qrencode on stdin so the passphrase never appears in argv.
|
# qrencode on stdin so the passphrase never appears in argv.
|
||||||
payload_file="$(mktemp "$out_dir/payload.XXXXXX")" || emit_error 'could not create a temporary file'
|
payload_file="$(mktemp "$out_dir/payload.XXXXXX")" || emit_error 'could not create a temporary file'
|
||||||
trap 'rm -f "$payload_file"' RETURN
|
|
||||||
|
|
||||||
{
|
{
|
||||||
printf 'WIFI:T:WPA;S:'
|
printf 'WIFI:T:WPA;S:'
|
||||||
printf '%s' "$ssid" | escape_field
|
printf '%s' "$ssid" | escape_field
|
||||||
printf ';P:'
|
printf ';P:'
|
||||||
nmcli -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null | escape_field
|
nmcli -e no -s -g 802-11-wireless-security.psk connection show "$name" 2>/dev/null | escape_field
|
||||||
printf ';H:%s;;' "$hidden"
|
printf ';H:%s;;' "$hidden"
|
||||||
} >"$payload_file"
|
} >"$payload_file"
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ Singleton {
|
|||||||
Process {
|
Process {
|
||||||
id: writer
|
id: writer
|
||||||
onExited: {
|
onExited: {
|
||||||
|
reader.exited = false;
|
||||||
|
reader.streamFinished = false;
|
||||||
|
reader.settled = false;
|
||||||
reader.command = [root.helperPath, "get", String(root.writingBus)];
|
reader.command = [root.helperPath, "get", String(root.writingBus)];
|
||||||
reader.running = true;
|
reader.running = true;
|
||||||
}
|
}
|
||||||
@@ -145,9 +148,36 @@ Singleton {
|
|||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: reader
|
id: reader
|
||||||
|
|
||||||
|
property string outputText: ""
|
||||||
|
property bool exited: false
|
||||||
|
property bool streamFinished: false
|
||||||
|
property bool settled: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
const actual = parseInt(this.text.trim());
|
reader.outputText = this.text;
|
||||||
|
reader.streamFinished = true;
|
||||||
|
root.settleReader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `running` can still read true at the moment onStreamFinished fires --
|
||||||
|
// the same exited/streamFinished ordering hazard HomeAssistantConfig.qml
|
||||||
|
// guards against -- so pump() must not be re-entered from here directly.
|
||||||
|
// Settle on whichever of exited/streamFinished arrives last instead.
|
||||||
|
onExited: (code, status) => {
|
||||||
|
reader.exited = true;
|
||||||
|
root.settleReader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function settleReader(): void {
|
||||||
|
if (reader.settled || !reader.exited || !reader.streamFinished)
|
||||||
|
return;
|
||||||
|
reader.settled = true;
|
||||||
|
|
||||||
|
const actual = parseInt(reader.outputText.trim());
|
||||||
if (!isNaN(actual)) {
|
if (!isNaN(actual)) {
|
||||||
root.displays = root.displays.map(display =>
|
root.displays = root.displays.map(display =>
|
||||||
display.bus === root.writingBus
|
display.bus === root.writingBus
|
||||||
@@ -159,5 +189,3 @@ Singleton {
|
|||||||
root.pump();
|
root.pump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ Singleton {
|
|||||||
property int fixtureNow: 0
|
property int fixtureNow: 0
|
||||||
property bool watchWanted: false
|
property bool watchWanted: false
|
||||||
|
|
||||||
|
// Counts watch attempts that exit without ever emitting a snapshot, so a
|
||||||
|
// helper that is missing or crashes on launch (e.g. no evolution-data-server)
|
||||||
|
// is reported instead of retried forever in silence.
|
||||||
|
property int consecutiveWatchFailures: 0
|
||||||
|
property bool watchProducedSnapshot: false
|
||||||
|
readonly property int maxConsecutiveWatchFailures: 3
|
||||||
|
|
||||||
readonly property int nowEpoch: fixtureNow > 0 ? fixtureNow : Math.floor(clock.date.getTime() / 1000)
|
readonly property int nowEpoch: fixtureNow > 0 ? fixtureNow : Math.floor(clock.date.getTime() / 1000)
|
||||||
readonly property var nextEvent: {
|
readonly property var nextEvent: {
|
||||||
const candidates = root.events.filter(event => !event.allDay && Number(event.end) > root.nowEpoch);
|
const candidates = root.events.filter(event => !event.allDay && Number(event.end) > root.nowEpoch);
|
||||||
@@ -135,14 +142,20 @@ Singleton {
|
|||||||
function _restartWatch(): void {
|
function _restartWatch(): void {
|
||||||
if (root.fixtureMode || root.rangeStart <= 0 || root.rangeEnd <= root.rangeStart)
|
if (root.fixtureMode || root.rangeStart <= 0 || root.rangeEnd <= root.rangeStart)
|
||||||
return;
|
return;
|
||||||
|
// Once the helper has been declared unavailable, don't flash back to
|
||||||
|
// "loading" on every retry -- only a real snapshot should clear it.
|
||||||
|
if (root.phase !== "unavailable")
|
||||||
root.phase = root.events.length > 0 ? root.phase : "loading";
|
root.phase = root.events.length > 0 ? root.phase : "loading";
|
||||||
root.watchWanted = false;
|
root.watchWanted = false;
|
||||||
|
root.watchProducedSnapshot = false;
|
||||||
startTimer.restart();
|
startTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
function consumeSnapshot(data: string): void {
|
function consumeSnapshot(data: string): void {
|
||||||
if (root.fixtureMode || data.trim() === "")
|
if (root.fixtureMode || data.trim() === "")
|
||||||
return;
|
return;
|
||||||
|
root.watchProducedSnapshot = true;
|
||||||
|
root.consecutiveWatchFailures = 0;
|
||||||
try {
|
try {
|
||||||
const snapshot = JSON.parse(data);
|
const snapshot = JSON.parse(data);
|
||||||
if (snapshot.ok !== true) {
|
if (snapshot.ok !== true) {
|
||||||
@@ -263,6 +276,7 @@ Singleton {
|
|||||||
root.errors = [];
|
root.errors = [];
|
||||||
root.selectedDate = new Date();
|
root.selectedDate = new Date();
|
||||||
root.phase = "loading";
|
root.phase = "loading";
|
||||||
|
root.consecutiveWatchFailures = 0;
|
||||||
root.setVisibleMonth(root.selectedDate.getFullYear(), root.selectedDate.getMonth());
|
root.setVisibleMonth(root.selectedDate.getFullYear(), root.selectedDate.getMonth());
|
||||||
root._restartWatch();
|
root._restartWatch();
|
||||||
}
|
}
|
||||||
@@ -275,6 +289,16 @@ Singleton {
|
|||||||
onRead: data => root.consumeSnapshot(data)
|
onRead: data => root.consumeSnapshot(data)
|
||||||
}
|
}
|
||||||
onExited: (code, status) => {
|
onExited: (code, status) => {
|
||||||
|
if (!root.watchProducedSnapshot) {
|
||||||
|
root.consecutiveWatchFailures += 1;
|
||||||
|
// The helper died before ever emitting a snapshot, repeatedly --
|
||||||
|
// stop pretending this is still loading and surface the same
|
||||||
|
// "unavailable" state probe()/collect_snapshot() report.
|
||||||
|
if (root.consecutiveWatchFailures >= root.maxConsecutiveWatchFailures) {
|
||||||
|
root.phase = "unavailable";
|
||||||
|
root.errors = [{ code: "eds-unavailable" }];
|
||||||
|
}
|
||||||
|
}
|
||||||
if (root.watchWanted && !root.fixtureMode)
|
if (root.watchWanted && !root.fixtureMode)
|
||||||
restartTimer.restart();
|
restartTimer.restart();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,20 +402,35 @@ esac'
|
|||||||
id: recProc
|
id: recProc
|
||||||
onExited: (code, status) => {
|
onExited: (code, status) => {
|
||||||
recTimer.stop();
|
recTimer.stop();
|
||||||
|
const path = root.recordingPath;
|
||||||
// SIGINT gives exit code 2 (or 130 through a shell); both mean the
|
// SIGINT gives exit code 2 (or 130 through a shell); both mean the
|
||||||
// user pressed stop and the file was finalised normally.
|
// user pressed stop and the file was finalised normally. Any other
|
||||||
if (root.recordingPath !== "") {
|
// code means wf-recorder died or errored before finalising, so the
|
||||||
|
// file may be missing or truncated -- never report success then.
|
||||||
|
const clean = code === 2 || code === 130;
|
||||||
|
if (path !== "") {
|
||||||
|
if (clean) {
|
||||||
StatusEvents.publish({
|
StatusEvents.publish({
|
||||||
key: "capture-recording",
|
key: "capture-recording",
|
||||||
glyph: "\u{F044A}",
|
glyph: "\u{F044A}",
|
||||||
title: "Screen recording saved",
|
title: "Screen recording saved",
|
||||||
detail: root.recordingPath.split("/").pop(),
|
detail: path.split("/").pop(),
|
||||||
tone: "ok",
|
tone: "ok",
|
||||||
priority: StatusEvents.importantPriority,
|
priority: StatusEvents.importantPriority,
|
||||||
actionId: "open-path",
|
actionId: "open-path",
|
||||||
actionData: root.recordingPath
|
actionData: path
|
||||||
});
|
});
|
||||||
Quickshell.execDetached(["sh", "-c", root._recDoneScript, "qs-capture", root.recordingPath]);
|
Quickshell.execDetached(["sh", "-c", root._recDoneScript, "qs-capture", path]);
|
||||||
|
} else {
|
||||||
|
StatusEvents.publish({
|
||||||
|
key: "capture-recording",
|
||||||
|
glyph: "\u{F044A}",
|
||||||
|
title: "Screen recording failed",
|
||||||
|
detail: "wf-recorder exited unexpectedly (code " + code + ")",
|
||||||
|
tone: "warn",
|
||||||
|
priority: StatusEvents.importantPriority
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
root.recordingPath = "";
|
root.recordingPath = "";
|
||||||
root.recordingSeconds = 0;
|
root.recordingSeconds = 0;
|
||||||
|
|||||||
@@ -55,6 +55,18 @@ Singleton {
|
|||||||
// installed. Distinct from an empty history.
|
// installed. Distinct from an empty history.
|
||||||
property bool available: true
|
property bool available: true
|
||||||
|
|
||||||
|
// query's `exited` and its stdout `streamFinished` are not guaranteed to
|
||||||
|
// fire in a particular order (the same signal-ordering hazard
|
||||||
|
// HomeAssistantConfig.qml's settle-both pattern guards against). These
|
||||||
|
// track which of the two have arrived for the query currently in flight
|
||||||
|
// so the result is only finalized once both are known -- otherwise a
|
||||||
|
// failed query's empty stdout could be parsed as "empty history" before
|
||||||
|
// the non-zero exit code is seen, or vice versa.
|
||||||
|
property bool _queryExited: false
|
||||||
|
property int _queryExitCode: 0
|
||||||
|
property bool _queryStdoutDone: false
|
||||||
|
property string _queryStdoutText: ""
|
||||||
|
|
||||||
// Wall-clock seconds at the moment `entries` was filled. Relative times are
|
// Wall-clock seconds at the moment `entries` was filled. Relative times are
|
||||||
// rendered against this rather than against a live clock, so a row's label
|
// rendered against this rather than against a live clock, so a row's label
|
||||||
// cannot change while the user is reading it and nothing has to tick.
|
// cannot change while the user is reading it and nothing has to tick.
|
||||||
@@ -69,6 +81,8 @@ Singleton {
|
|||||||
if (query.running)
|
if (query.running)
|
||||||
return;
|
return;
|
||||||
root.loading = true;
|
root.loading = true;
|
||||||
|
root._queryExited = false;
|
||||||
|
root._queryStdoutDone = false;
|
||||||
query.running = true;
|
query.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,16 +160,18 @@ Singleton {
|
|||||||
command: ["sqlite3", "-readonly", "-json", root.dbPath, root.sql]
|
command: ["sqlite3", "-readonly", "-json", root.dbPath, root.sql]
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: root._parse(this.text)
|
onStreamFinished: {
|
||||||
|
root._queryStdoutDone = true;
|
||||||
|
root._queryStdoutText = this.text;
|
||||||
|
root._settleQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
root.loading = false;
|
root.loading = false;
|
||||||
if (exitCode !== 0) {
|
root._queryExited = true;
|
||||||
root.available = false;
|
root._queryExitCode = exitCode;
|
||||||
root.entries = [];
|
root._settleQuery();
|
||||||
root.refreshed();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +185,29 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called from both query.onExited and its stdout streamFinished. Only
|
||||||
|
// finalizes once both signals have arrived, since their firing order is
|
||||||
|
// not guaranteed -- see the _queryExited / _queryStdoutDone comment
|
||||||
|
// above. A non-zero exit always means the history is unavailable,
|
||||||
|
// regardless of what (if anything) stdout produced; only a clean exit
|
||||||
|
// reaches _parse, where empty stdout is legitimately "no history yet".
|
||||||
|
function _settleQuery(): void {
|
||||||
|
if (!root._queryExited || !root._queryStdoutDone)
|
||||||
|
return;
|
||||||
|
const exitCode = root._queryExitCode;
|
||||||
|
const text = root._queryStdoutText;
|
||||||
|
root._queryExited = false;
|
||||||
|
root._queryStdoutDone = false;
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
root.available = false;
|
||||||
|
root.entries = [];
|
||||||
|
root.refreshed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root._parse(text);
|
||||||
|
}
|
||||||
|
|
||||||
function _parse(text: string): void {
|
function _parse(text: string): void {
|
||||||
root.available = true;
|
root.available = true;
|
||||||
root.queriedAt = Date.now() / 1000;
|
root.queriedAt = Date.now() / 1000;
|
||||||
@@ -181,7 +220,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
root.entries = JSON.parse(text).map(row => root._normalise(row));
|
root.entries = JSON.parse(text).map(row => root._normalize(row));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Clipboard: could not parse history —", e);
|
console.warn("Clipboard: could not parse history —", e);
|
||||||
root.entries = [];
|
root.entries = [];
|
||||||
@@ -192,7 +231,7 @@ Singleton {
|
|||||||
|
|
||||||
// Turn a raw row into what the UI actually asks questions of, so no
|
// Turn a raw row into what the UI actually asks questions of, so no
|
||||||
// delegate has to know about column names or Vicinae's integer kinds.
|
// delegate has to know about column names or Vicinae's integer kinds.
|
||||||
function _normalise(row: var): var {
|
function _normalize(row: var): var {
|
||||||
const encrypted = row.enc !== 0;
|
const encrypted = row.enc !== 0;
|
||||||
const textual = row.kind === root.kindText || row.kind === root.kindLink;
|
const textual = row.kind === root.kindText || row.kind === root.kindLink;
|
||||||
const raw = row.preview || "";
|
const raw = row.preview || "";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pragma Singleton
|
|||||||
// because every token there is a binding. Two other things draw on this desktop
|
// because every token there is a binding. Two other things draw on this desktop
|
||||||
// and do not read Panama's store:
|
// and do not read Panama's store:
|
||||||
//
|
//
|
||||||
// GTK applications read gsettings colour-scheme and gtk-theme
|
// GTK applications read gsettings color-scheme and gtk-theme
|
||||||
// the compositor draws window borders and shadows
|
// the compositor draws window borders and shadows
|
||||||
//
|
//
|
||||||
// A toolbar or a window border still wearing the other scheme is more jarring
|
// A toolbar or a window border still wearing the other scheme is more jarring
|
||||||
@@ -29,8 +29,28 @@ Singleton {
|
|||||||
readonly property string inactiveBorderDark: "rgba(3b426199)"
|
readonly property string inactiveBorderDark: "rgba(3b426199)"
|
||||||
readonly property string inactiveBorderLight: "rgba(a8aecb99)"
|
readonly property string inactiveBorderLight: "rgba(a8aecb99)"
|
||||||
readonly property string inactiveBorder: root.dark ? root.inactiveBorderDark : root.inactiveBorderLight
|
readonly property string inactiveBorder: root.dark ? root.inactiveBorderDark : root.inactiveBorderLight
|
||||||
|
|
||||||
|
// The focused border follows the chosen accent. `ee` is the shipped alpha
|
||||||
|
// for the Prism gradient; Theme owns which colors, this owns the form the
|
||||||
|
// compositor wants them in.
|
||||||
|
function hyprColor(value: var): string {
|
||||||
|
// Qt gives "#rrggbb" -- or "#aarrggbb" if the color ever carries an
|
||||||
|
// alpha channel. slice(-6) keeps the trailing rrggbb either way;
|
||||||
|
// slice(0, 6) would instead grab "aarrgg" out of an 8-digit string and
|
||||||
|
// call it RGB. Hyprland wants rgba(rrggbbaa).
|
||||||
|
return "rgba(" + String(value).replace("#", "").slice(-6) + "ee)";
|
||||||
|
}
|
||||||
|
readonly property string accentBorderStart: root.hyprColor(Theme.accent)
|
||||||
|
readonly property string accentBorderEnd: root.hyprColor(Theme.accentSecondary)
|
||||||
property string lastError: ""
|
property string lastError: ""
|
||||||
|
|
||||||
|
// What the last push actually sent, so apply() can tell an accent-only
|
||||||
|
// change apart from a scheme change and skip the steps that do not depend
|
||||||
|
// on whichever did not move. Their starting values do not matter: the
|
||||||
|
// first apply() always runs with force set, which ignores both.
|
||||||
|
property bool appliedDark: false
|
||||||
|
property string appliedAccentName: ""
|
||||||
|
|
||||||
// Applied one command at a time: Process runs a single command, and several
|
// Applied one command at a time: Process runs a single command, and several
|
||||||
// of these are separate programs.
|
// of these are separate programs.
|
||||||
property var pending: []
|
property var pending: []
|
||||||
@@ -39,7 +59,7 @@ Singleton {
|
|||||||
id: runner
|
id: runner
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
root.lastError = "The colour scheme could not be applied everywhere.";
|
root.lastError = "The color scheme could not be applied everywhere.";
|
||||||
root.drain();
|
root.drain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +85,7 @@ Singleton {
|
|||||||
Timer {
|
Timer {
|
||||||
id: settle
|
id: settle
|
||||||
interval: 1200
|
interval: 1200
|
||||||
onTriggered: root.apply()
|
onTriggered: root.apply(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -76,12 +96,30 @@ Singleton {
|
|||||||
Timer {
|
Timer {
|
||||||
id: coalesce
|
id: coalesce
|
||||||
interval: 250
|
interval: 250
|
||||||
onTriggered: root.apply()
|
onTriggered: root.apply(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function apply(): void {
|
// `force` pushes every step regardless of what moved -- startup needs
|
||||||
|
// that, because gsettings and the compositor keep their own state and
|
||||||
|
// have no way to know a previous Quickshell session already told them the
|
||||||
|
// answer. Everywhere else this reacts to ANY preference changing (see
|
||||||
|
// onRevisionChanged above), so most calls have nothing to do with either
|
||||||
|
// scheme or accent; only the steps whose inputs actually moved since the
|
||||||
|
// last push run, which keeps sampling accent swatches from also rewriting
|
||||||
|
// gsettings and re-running the whole app-theming script on every sample.
|
||||||
|
function apply(force: bool): void {
|
||||||
root.lastError = "";
|
root.lastError = "";
|
||||||
|
|
||||||
|
const accentName = DesktopPreferences.get("accentName") || "blue";
|
||||||
|
const schemeChanged = force || root.dark !== root.appliedDark;
|
||||||
|
const accentChanged = force || accentName !== root.appliedAccentName;
|
||||||
|
|
||||||
|
if (!schemeChanged && !accentChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const commands = [];
|
||||||
|
|
||||||
|
if (schemeChanged) {
|
||||||
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
||||||
|
|
||||||
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
||||||
@@ -99,21 +137,51 @@ Singleton {
|
|||||||
// because the failure mode is silent in exactly this way.
|
// because the failure mode is silent in exactly this way.
|
||||||
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
||||||
|
|
||||||
const commands = [
|
commands.push(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme]);
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
|
commands.push(["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]);
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Unfocused window borders need scheme-relative contrast. The focused
|
// Unfocused window borders need scheme-relative contrast, and stay
|
||||||
// Prism border is deliberately owned by the accent/theme layer.
|
// this service's to own. The FOCUSED border below is the accent
|
||||||
|
// role and belongs to the theme -- see modules/settings/README.md.
|
||||||
|
// Unlike that role, this one does not depend on the accent, so an
|
||||||
|
// accent-only change never needs to restate it.
|
||||||
commands.push(["hyprctl", "eval",
|
commands.push(["hyprctl", "eval",
|
||||||
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
|
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
|
||||||
|
}
|
||||||
|
|
||||||
// Applications that predate org.freedesktop.appearance and carry their
|
if (schemeChanged || accentChanged) {
|
||||||
// own palettes -- terminals, chiefly. Everything that reads the portal
|
// Each accent carries a separate pair for light and dark, so this
|
||||||
// (GTK4, Qt6, Chromium, Electron) is already handled by the gsettings
|
// runs on either kind of change: a scheme flip restates the same
|
||||||
// write above and needs nothing here.
|
// accent's other pair, and an accent change restates the same
|
||||||
commands.push([root.appThemePath, root.dark ? "dark" : "light"]);
|
// scheme's other colors.
|
||||||
|
//
|
||||||
|
// A two-stop gradient at the shipped angle. Written as a Lua TABLE:
|
||||||
|
// the string form of a gradient carries only one stop, and passing
|
||||||
|
// "rgba(a) rgba(b) 115deg" as a string is accepted and silently
|
||||||
|
// keeps the previous value. Multi-stop must be the table form --
|
||||||
|
// built by the same serializeValue() SystemSettings uses for every
|
||||||
|
// other gradient, rather than a second hand-rolled copy of that
|
||||||
|
// escaping here.
|
||||||
|
const activeBorder = SystemSettings.serializeValue({
|
||||||
|
colors: [root.accentBorderStart, root.accentBorderEnd],
|
||||||
|
angle: 115
|
||||||
|
});
|
||||||
|
commands.push(["hyprctl", "eval",
|
||||||
|
`hl.config({ general = { col = { active_border = ${activeBorder} } } })`]);
|
||||||
|
|
||||||
|
// Applications that predate org.freedesktop.appearance and carry
|
||||||
|
// their own palettes -- terminals, chiefly. Everything that reads
|
||||||
|
// the portal (GTK4, Qt6, Chromium, Electron) is already handled by
|
||||||
|
// the gsettings write above and needs nothing here. The accent is
|
||||||
|
// passed through too: kitty's border and the hyprlock fallback
|
||||||
|
// template are also the accent role, not just the scheme -- so
|
||||||
|
// this still has to run on an accent-only change, even though the
|
||||||
|
// scheme-relative work it also does is then redundant.
|
||||||
|
commands.push([root.appThemePath, root.dark ? "dark" : "light", accentName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
root.appliedDark = root.dark;
|
||||||
|
root.appliedAccentName = accentName;
|
||||||
|
|
||||||
root.enqueue(commands);
|
root.enqueue(commands);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,4 +146,11 @@ Singleton {
|
|||||||
onWifiDeviceChanged: root.syncScanners()
|
onWifiDeviceChanged: root.syncScanners()
|
||||||
onWifiEnabledChanged: root.syncScanners()
|
onWifiEnabledChanged: root.syncScanners()
|
||||||
onAdapterChanged: root.syncScanners()
|
onAdapterChanged: root.syncScanners()
|
||||||
|
|
||||||
|
// adapter.enabled has no property on root to bind onXChanged to, so it
|
||||||
|
// needs its own Connections -- the Bluetooth equivalent of onWifiEnabledChanged.
|
||||||
|
Connections {
|
||||||
|
target: root.adapter
|
||||||
|
function onEnabledChanged(): void { root.syncScanners(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Singleton {
|
|||||||
|
|
||||||
property string timezone: ""
|
property string timezone: ""
|
||||||
property bool ntpEnabled: false
|
property bool ntpEnabled: false
|
||||||
property bool ntpSynchronised: false
|
property bool ntpSynchronized: false
|
||||||
property string localTime: ""
|
property string localTime: ""
|
||||||
property string universalTime: ""
|
property string universalTime: ""
|
||||||
property string rtcTime: ""
|
property string rtcTime: ""
|
||||||
@@ -79,7 +79,7 @@ Singleton {
|
|||||||
// status is re-read either way.
|
// status is re-read either way.
|
||||||
root.lastError = exitCode === 0
|
root.lastError = exitCode === 0
|
||||||
? ""
|
? ""
|
||||||
: "The system rejected that change, or authentication was cancelled.";
|
: "The system rejected that change, or authentication was canceled.";
|
||||||
root.refresh();
|
root.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ Singleton {
|
|||||||
else if (key === "NTP")
|
else if (key === "NTP")
|
||||||
root.ntpEnabled = value === "yes";
|
root.ntpEnabled = value === "yes";
|
||||||
else if (key === "NTPSynchronized")
|
else if (key === "NTPSynchronized")
|
||||||
root.ntpSynchronised = value === "yes";
|
root.ntpSynchronized = value === "yes";
|
||||||
}
|
}
|
||||||
root.lastError = "";
|
root.lastError = "";
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ Singleton {
|
|||||||
// caller-supplied text reaches the command.
|
// caller-supplied text reaches the command.
|
||||||
function setTimezone(zone: string): bool {
|
function setTimezone(zone: string): bool {
|
||||||
if (root.zones.indexOf(zone) < 0) {
|
if (root.zones.indexOf(zone) < 0) {
|
||||||
root.lastError = "That is not a timezone this system recognises.";
|
root.lastError = "That is not a timezone this system recognizes.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (writeRun.running)
|
if (writeRun.running)
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ Singleton {
|
|||||||
root.revertWithMessage("The display rejected that change and Panama restored the previous setting.");
|
root.revertWithMessage("The display rejected that change and Panama restored the previous setting.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
verifyTimer.attempts = 0;
|
|
||||||
verifyTimer.ticks = 0;
|
verifyTimer.ticks = 0;
|
||||||
verifyTimer.restart();
|
verifyTimer.restart();
|
||||||
}
|
}
|
||||||
@@ -107,7 +106,6 @@ Singleton {
|
|||||||
// Exit status is advisory only. Hyprland's Lua bridge can report
|
// Exit status is advisory only. Hyprland's Lua bridge can report
|
||||||
// success without applying a value, so exact readback decides.
|
// success without applying a value, so exact readback decides.
|
||||||
root.revertVerificationActive = true;
|
root.revertVerificationActive = true;
|
||||||
revertVerifyTimer.attempts = 0;
|
|
||||||
revertVerifyTimer.ticks = 0;
|
revertVerifyTimer.ticks = 0;
|
||||||
revertVerifyTimer.restart();
|
revertVerifyTimer.restart();
|
||||||
}
|
}
|
||||||
@@ -143,7 +141,7 @@ Singleton {
|
|||||||
? persistedPrimaries[0].name
|
? persistedPrimaries[0].name
|
||||||
: (origin?.name ?? raw[0]?.name ?? "");
|
: (origin?.name ?? raw[0]?.name ?? "");
|
||||||
root.monitors = raw.map(monitor => {
|
root.monitors = raw.map(monitor => {
|
||||||
const modes = root.normaliseModes(monitor.availableModes ?? []);
|
const modes = root.normalizeModes(monitor.availableModes ?? []);
|
||||||
const width = monitor.width ?? 0;
|
const width = monitor.width ?? 0;
|
||||||
const height = monitor.height ?? 0;
|
const height = monitor.height ?? 0;
|
||||||
const refreshRate = monitor.refreshRate ?? 0;
|
const refreshRate = monitor.refreshRate ?? 0;
|
||||||
@@ -202,7 +200,7 @@ Singleton {
|
|||||||
// resolution at distinct rates such as 60.00 and 59.94. Those identities
|
// resolution at distinct rates such as 60.00 and 59.94. Those identities
|
||||||
// remain separate because confirmation and recovery must read back the
|
// remain separate because confirmation and recovery must read back the
|
||||||
// exact mode the user chose, even when their rounded labels look similar.
|
// exact mode the user chose, even when their rounded labels look similar.
|
||||||
function normaliseModes(raw: var): var {
|
function normalizeModes(raw: var): var {
|
||||||
const seen = {};
|
const seen = {};
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const entry of raw) {
|
for (const entry of raw) {
|
||||||
@@ -542,7 +540,6 @@ Singleton {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: verifyTimer
|
id: verifyTimer
|
||||||
property int attempts: 0
|
|
||||||
property int ticks: 0
|
property int ticks: 0
|
||||||
interval: 120
|
interval: 120
|
||||||
repeat: true
|
repeat: true
|
||||||
@@ -552,14 +549,12 @@ Singleton {
|
|||||||
root.verificationTimedOut();
|
root.verificationTimedOut();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root.refresh())
|
root.refresh();
|
||||||
attempts++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: revertVerifyTimer
|
id: revertVerifyTimer
|
||||||
property int attempts: 0
|
|
||||||
property int ticks: 0
|
property int ticks: 0
|
||||||
interval: 120
|
interval: 120
|
||||||
repeat: true
|
repeat: true
|
||||||
@@ -569,8 +564,7 @@ Singleton {
|
|||||||
root.revertVerificationTimedOut();
|
root.revertVerificationTimedOut();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root.refresh())
|
root.refresh();
|
||||||
attempts++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ Singleton {
|
|||||||
root.searching = false;
|
root.searching = false;
|
||||||
if (exitCode !== 0)
|
if (exitCode !== 0)
|
||||||
root.lastError = "Could not reach the location service.";
|
root.lastError = "Could not reach the location service.";
|
||||||
|
|
||||||
|
// Typing kept going while this fetch was in flight -- rather than
|
||||||
|
// leaving the newer query stranded until another keystroke, go
|
||||||
|
// fetch it now. run() no-ops if pending is now too short.
|
||||||
|
if (root.pending !== root.lastQuery)
|
||||||
|
root.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +114,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stores a chosen place. Coordinates are rounded to four decimals -- roughly
|
// Stores a chosen place. Coordinates are rounded to four decimals -- roughly
|
||||||
// ten metres, far finer than a weather reading resolves, and it keeps a
|
// ten meters, far finer than a weather reading resolves, and it keeps a
|
||||||
// precise home location out of the settings file.
|
// precise home location out of the settings file.
|
||||||
function choose(place: var): bool {
|
function choose(place: var): bool {
|
||||||
const latitude = Math.round(place.latitude * 10000) / 10000;
|
const latitude = Math.round(place.latitude * 10000) / 10000;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "AMD ... [Radeon RX 7700 XT / 7800 XT] (rev c8)" is what lspci gives; the
|
// "AMD ... [Radeon RX 7700 XT / 7800 XT] (rev c8)" is what lspci gives; the
|
||||||
// bracketed marketing name is the part anyone recognises.
|
// bracketed marketing name is the part anyone recognizes.
|
||||||
function shortName(name: string): string {
|
function shortName(name: string): string {
|
||||||
const bracketed = String(name).match(/\[([^\]]+)\]\s*(?:\(rev[^)]*\))?\s*$/);
|
const bracketed = String(name).match(/\[([^\]]+)\]\s*(?:\(rev[^)]*\))?\s*$/);
|
||||||
return bracketed ? bracketed[1] : String(name).replace(/\s*\(rev[^)]*\)\s*$/, "");
|
return bracketed ? bracketed[1] : String(name).replace(/\s*\(rev[^)]*\)\s*$/, "");
|
||||||
|
|||||||
@@ -95,7 +95,16 @@ Singleton {
|
|||||||
|
|
||||||
property string payload: ""
|
property string payload: ""
|
||||||
|
|
||||||
onStarted: copyProcess.write(copyProcess.payload)
|
stdinEnabled: true
|
||||||
|
onStarted: {
|
||||||
|
copyProcess.write(copyProcess.payload);
|
||||||
|
// wl-copy reads stdin until EOF before it exits; leaving the
|
||||||
|
// channel open (Process.write alone never closes it) would hang
|
||||||
|
// it forever waiting for more input. Disabling stdin closes the
|
||||||
|
// write side -- see HomeAssistantConfig.qml's writeProc for the
|
||||||
|
// same stdinEnabled pattern.
|
||||||
|
copyProcess.stdinEnabled = false;
|
||||||
|
}
|
||||||
onExited: (exitCode, exitStatus) => {
|
onExited: (exitCode, exitStatus) => {
|
||||||
root.lastCopyResult = exitCode === 0
|
root.lastCopyResult = exitCode === 0
|
||||||
? "Report copied."
|
? "Report copied."
|
||||||
@@ -339,6 +348,10 @@ Singleton {
|
|||||||
return false;
|
return false;
|
||||||
copyProcess.payload = JSON.stringify(root.snapshot, null, 2);
|
copyProcess.payload = JSON.stringify(root.snapshot, null, 2);
|
||||||
root.lastCopyResult = "";
|
root.lastCopyResult = "";
|
||||||
|
// Re-arm stdin: the previous run closed it (see copyProcess.onStarted)
|
||||||
|
// and a disabled channel stays closed even after being set back to
|
||||||
|
// true mid-run, so each new run needs it explicitly re-enabled.
|
||||||
|
copyProcess.stdinEnabled = true;
|
||||||
copyProcess.exec(["wl-copy"]);
|
copyProcess.exec(["wl-copy"]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ Singleton {
|
|||||||
property string actionKind: ""
|
property string actionKind: ""
|
||||||
property string actionPath: ""
|
property string actionPath: ""
|
||||||
|
|
||||||
|
// actionProc's `exited` and its stdout `streamFinished` are not guaranteed
|
||||||
|
// to fire in a particular order (same hazard HomeAssistantConfig.qml's
|
||||||
|
// settle-both pattern guards against). These track which of the two have
|
||||||
|
// been observed for the action currently in flight so finishAction() is
|
||||||
|
// only ever called once both have arrived, with the real stdout JSON as
|
||||||
|
// the authoritative result.
|
||||||
|
property bool actionExited: false
|
||||||
|
property bool actionStdoutDone: false
|
||||||
|
property string actionStdoutText: ""
|
||||||
|
|
||||||
readonly property var preferredPhone: {
|
readonly property var preferredPhone: {
|
||||||
const phones = root.devices.filter(device => device.type === "phone" && device.paired);
|
const phones = root.devices.filter(device => device.type === "phone" && device.paired);
|
||||||
return phones.find(device => device.reachable) ?? phones[0] ?? null;
|
return phones.find(device => device.reachable) ?? phones[0] ?? null;
|
||||||
@@ -66,6 +76,8 @@ Singleton {
|
|||||||
root.transferActive = true;
|
root.transferActive = true;
|
||||||
root.transferFileName = path.split("/").pop();
|
root.transferFileName = path.split("/").pop();
|
||||||
root.transferDeviceName = root.preferredPhone.name;
|
root.transferDeviceName = root.preferredPhone.name;
|
||||||
|
root.actionExited = false;
|
||||||
|
root.actionStdoutDone = false;
|
||||||
actionProc.command = [root.helperPath, "send-file", root.preferredPhone.id, path];
|
actionProc.command = [root.helperPath, "send-file", root.preferredPhone.id, path];
|
||||||
actionProc.running = true;
|
actionProc.running = true;
|
||||||
}
|
}
|
||||||
@@ -83,6 +95,8 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
root.actionKind = kind;
|
root.actionKind = kind;
|
||||||
root.actionPath = "";
|
root.actionPath = "";
|
||||||
|
root.actionExited = false;
|
||||||
|
root.actionStdoutDone = false;
|
||||||
actionProc.command = [root.helperPath, command, root.preferredPhone.id];
|
actionProc.command = [root.helperPath, command, root.preferredPhone.id];
|
||||||
actionProc.running = true;
|
actionProc.running = true;
|
||||||
}
|
}
|
||||||
@@ -95,6 +109,22 @@ Singleton {
|
|||||||
root.transferActive = false;
|
root.transferActive = false;
|
||||||
root.transferFileName = "";
|
root.transferFileName = "";
|
||||||
root.transferDeviceName = "";
|
root.transferDeviceName = "";
|
||||||
|
root.actionExited = false;
|
||||||
|
root.actionStdoutDone = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from both actionProc.onExited and its stdout streamFinished.
|
||||||
|
// Only finalizes once both signals have arrived for the in-flight action,
|
||||||
|
// since their firing order is not guaranteed -- see the actionExited /
|
||||||
|
// actionStdoutDone comment above.
|
||||||
|
function settleAction(): void {
|
||||||
|
if (root.actionKind === "")
|
||||||
|
return;
|
||||||
|
if (!root.actionExited || !root.actionStdoutDone)
|
||||||
|
return;
|
||||||
|
root.actionExited = false;
|
||||||
|
root.actionStdoutDone = false;
|
||||||
|
root.finishAction(root.actionStdoutText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishAction(text: string): void {
|
function finishAction(text: string): void {
|
||||||
@@ -198,11 +228,15 @@ Singleton {
|
|||||||
Process {
|
Process {
|
||||||
id: actionProc
|
id: actionProc
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: root.finishAction(this.text)
|
onStreamFinished: {
|
||||||
|
root.actionStdoutDone = true;
|
||||||
|
root.actionStdoutText = this.text;
|
||||||
|
root.settleAction();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onExited: (code, status) => {
|
onExited: (code, status) => {
|
||||||
if (root.actionKind !== "")
|
root.actionExited = true;
|
||||||
root.finishAction("");
|
root.settleAction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Singleton {
|
|||||||
readonly property bool scheduled: root.inWindow(clock.hours + clock.minutes / 60)
|
readonly property bool scheduled: root.inWindow(clock.hours + clock.minutes / 60)
|
||||||
|
|
||||||
// A manual toggle always wins: it drops out of the schedule rather than
|
// A manual toggle always wins: it drops out of the schedule rather than
|
||||||
// being silently reverted a minute later. Same as GNOME's behaviour when
|
// being silently reverted a minute later. Same as GNOME's behavior when
|
||||||
// you flip night light off during a scheduled evening.
|
// you flip night light off during a scheduled evening.
|
||||||
function toggle(): void {
|
function toggle(): void {
|
||||||
if (root.automatic) {
|
if (root.automatic) {
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ pragma Singleton
|
|||||||
// The freedesktop notification server, plus the two lists the UI renders:
|
// The freedesktop notification server, plus the two lists the UI renders:
|
||||||
//
|
//
|
||||||
// popups — what Toasts.qml is currently showing (transient, timed)
|
// popups — what Toasts.qml is currently showing (transient, timed)
|
||||||
// history — GNOME's message tray, what NotificationCenter.qml shows
|
// history — GNOME's message tray, what NotificationList.qml shows
|
||||||
//
|
//
|
||||||
// A notification lives exactly as long as `tracked` is true, so history holds
|
// A notification lives exactly as long as `tracked` is true, so history holds
|
||||||
// the *live* objects rather than copies: that keeps actions and inline replies
|
// the *live* objects rather than copies: that keeps actions and inline replies
|
||||||
// working from the tray, which is what GNOME does. The cost is that a
|
// working from the tray, which is what GNOME does. The cost is that a
|
||||||
// notification an app closes itself (progress bars, "download finished"
|
// notification an app closes itself (progress bars, "download finished"
|
||||||
// replacing "downloading") disappears from history too — correct behaviour,
|
// replacing "downloading") disappears from history too — correct behavior,
|
||||||
// but the reason history is not append-only.
|
// but the reason history is not append-only.
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Singleton {
|
|||||||
// Suppresses toasts entirely. Notifications still reach history.
|
// Suppresses toasts entirely. Notifications still reach history.
|
||||||
property bool doNotDisturb: false
|
property bool doNotDisturb: false
|
||||||
|
|
||||||
// Cleared when the notification centre is opened. The bar binds to this.
|
// Cleared when the notification center is opened. The bar binds to this.
|
||||||
property int unreadCount: 0
|
property int unreadCount: 0
|
||||||
|
|
||||||
// Kept separate from the persisted map so this version can safely run
|
// Kept separate from the persisted map so this version can safely run
|
||||||
@@ -80,6 +80,21 @@ Singleton {
|
|||||||
return at ? Qt.formatDateTime(at, Settings.use24Hour ? "HH:mm" : "h:mm AP") : "";
|
return at ? Qt.formatDateTime(at, Settings.use24Hour ? "HH:mm" : "h:mm AP") : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Freedesktop timeout resolution, shared by the toast countdown (Toast.qml)
|
||||||
|
// and the no-display expiry a transient notification gets while Do Not
|
||||||
|
// Disturb is on (below). Critical urgency and an explicit expireTimeout
|
||||||
|
// override policy; -1 ("server decides") falls back to it. 0 means "never
|
||||||
|
// auto-expire" per spec.
|
||||||
|
function notificationTimeoutMs(notification: var): int {
|
||||||
|
if (notification.urgency === NotificationUrgency.Critical)
|
||||||
|
return Settings.notificationTimeoutCriticalMs;
|
||||||
|
if (notification.expireTimeout === 0)
|
||||||
|
return 0;
|
||||||
|
if (notification.expireTimeout > 0)
|
||||||
|
return Math.round(notification.expireTimeout * 1000);
|
||||||
|
return Settings.notificationTimeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property bool hasNotifications: root.history.length > 0
|
readonly property bool hasNotifications: root.history.length > 0
|
||||||
|
|
||||||
function notificationAppId(notification: var): string {
|
function notificationAppId(notification: var): string {
|
||||||
@@ -156,7 +171,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// history grouped by app, in most-recent-app-first order — the shape
|
// history grouped by app, in most-recent-app-first order — the shape
|
||||||
// NotificationCenter.qml renders directly.
|
// NotificationList.qml renders directly.
|
||||||
readonly property var groups: {
|
readonly property var groups: {
|
||||||
const out = [];
|
const out = [];
|
||||||
const byApp = {};
|
const byApp = {};
|
||||||
@@ -219,8 +234,44 @@ Singleton {
|
|||||||
root.unreadCount += 1;
|
root.unreadCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.doNotDisturb)
|
if (!root.doNotDisturb) {
|
||||||
root.popups = [notification].concat(root.popups);
|
root.popups = [notification].concat(root.popups);
|
||||||
|
} else if (notification.transient) {
|
||||||
|
// Never shown, and (being transient) never filed in history
|
||||||
|
// either — nothing will otherwise dismiss() it, so schedule
|
||||||
|
// the same release its popup timeout would have given it.
|
||||||
|
root.scheduleTransientExpiry(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs a DND-hidden transient notification through the same lifetime it
|
||||||
|
// would have gotten as a visible popup (Toast.qml's countdown), just
|
||||||
|
// without ever showing it, so it still gets released instead of staying
|
||||||
|
// tracked forever.
|
||||||
|
function scheduleTransientExpiry(notification: var): void {
|
||||||
|
const ms = root.notificationTimeoutMs(notification);
|
||||||
|
if (ms <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const timer = Qt.createQmlObject("import QtQuick; Timer { repeat: false }", root);
|
||||||
|
timer.interval = ms;
|
||||||
|
timer.triggered.connect(() => {
|
||||||
|
timer.destroy();
|
||||||
|
root.releaseTransient(notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Closed some other way first (app-side close, dismissAll()) — cancel
|
||||||
|
// the pending timer instead of firing a stale dismiss() later.
|
||||||
|
notification.closed.connect(() => timer.destroy());
|
||||||
|
timer.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transient notifications are never filed in history, so nothing else
|
||||||
|
// holds a reference once their lifetime ends — release tracked state
|
||||||
|
// directly. Shared by the DND-hidden expiry above and dismissAll() below.
|
||||||
|
function releaseTransient(n: var): void {
|
||||||
|
if (n.transient)
|
||||||
|
n.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Mutation ────────────────────────────────────────────────────────────
|
// ── Mutation ────────────────────────────────────────────────────────────
|
||||||
@@ -243,10 +294,7 @@ Singleton {
|
|||||||
if (next.length === root.popups.length)
|
if (next.length === root.popups.length)
|
||||||
return;
|
return;
|
||||||
root.popups = next;
|
root.popups = next;
|
||||||
|
root.releaseTransient(n);
|
||||||
// Transients were never in history, so nothing else holds them.
|
|
||||||
if (n.transient)
|
|
||||||
n.dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismiss(n: Notification): void {
|
function dismiss(n: Notification): void {
|
||||||
@@ -257,10 +305,19 @@ Singleton {
|
|||||||
// Copy first: dismiss() re-enters through forget() and rewrites both
|
// Copy first: dismiss() re-enters through forget() and rewrites both
|
||||||
// lists while we iterate.
|
// lists while we iterate.
|
||||||
const all = root.history.slice();
|
const all = root.history.slice();
|
||||||
|
|
||||||
|
// Transient notifications are never filed in history — whether
|
||||||
|
// currently shown as a popup or hidden by Do Not Disturb with a
|
||||||
|
// pending scheduleTransientExpiry() timer — so releasing them needs
|
||||||
|
// its own pass over the server's full tracked set.
|
||||||
|
const transients = root.active.values.filter(n => n.transient);
|
||||||
|
|
||||||
root.history = [];
|
root.history = [];
|
||||||
root.popups = [];
|
root.popups = [];
|
||||||
for (const n of all)
|
for (const n of all)
|
||||||
n.dismiss();
|
n.dismiss();
|
||||||
|
for (const n of transients)
|
||||||
|
root.releaseTransient(n);
|
||||||
root.unreadCount = 0;
|
root.unreadCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Singleton {
|
|||||||
|
|
||||||
// Non-empty when the machine cannot actually deliver the profile it is set
|
// Non-empty when the machine cannot actually deliver the profile it is set
|
||||||
// to -- thermal throttling, or a laptop running on battery. Worth showing,
|
// to -- thermal throttling, or a laptop running on battery. Worth showing,
|
||||||
// because otherwise "performance" is a claim the hardware is not honouring.
|
// because otherwise "performance" is a claim the hardware is not honoring.
|
||||||
property string degraded: ""
|
property string degraded: ""
|
||||||
|
|
||||||
readonly property bool available: root.profiles.length > 0
|
readonly property bool available: root.profiles.length > 0
|
||||||
@@ -52,7 +52,7 @@ Singleton {
|
|||||||
function detail(profile: string): string {
|
function detail(profile: string): string {
|
||||||
switch (profile) {
|
switch (profile) {
|
||||||
case "power-saver": return "Reduces performance to save energy and run quieter";
|
case "power-saver": return "Reduces performance to save energy and run quieter";
|
||||||
case "balanced": return "Standard behaviour, scaling up only when needed";
|
case "balanced": return "Standard behavior, scaling up only when needed";
|
||||||
case "performance": return "Holds higher clocks, using more power and making more noise";
|
case "performance": return "Holds higher clocks, using more power and making more noise";
|
||||||
default: return "";
|
default: return "";
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user