Compare commits
116
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63f4bdb547 | ||
|
|
1a93a788fa | ||
|
|
1debd37d01 | ||
|
|
4341628b2f | ||
|
|
7777f5c1aa | ||
|
|
bc7bf3185c | ||
|
|
f3d91c14a1 | ||
|
|
21beb066e1 | ||
|
|
d2898d8806 | ||
|
|
0c3935f818 | ||
|
|
70a9d27c98 | ||
|
|
5cf2943ccf | ||
|
|
d0d9e196b0 | ||
|
|
2613768efd | ||
|
|
101797f8f0 | ||
|
|
b9336d5930 | ||
|
|
c36e423890 | ||
|
|
1c12892252 | ||
|
|
52818b7290 | ||
|
|
c8004e400a | ||
|
|
3e98cc2216 | ||
|
|
a80a6f4dda | ||
|
|
3520700983 | ||
|
|
a845534110 | ||
|
|
db34b1e6ed | ||
|
|
0f3bddc452 | ||
|
|
9aea519e54 | ||
|
|
e5a2a430e0 | ||
|
|
bfa9c58b09 | ||
|
|
1360a80f07 | ||
|
|
8be3fc2fdd | ||
|
|
2d69ce7648 | ||
|
|
d87f4b6d6a | ||
|
|
9fc1fdbbbb | ||
|
|
dd8d93c387 | ||
|
|
b8832a0174 | ||
|
|
3f07d25858 | ||
|
|
df1dcdfad5 | ||
|
|
91f7273f41 | ||
|
|
f9eba1e8c5 | ||
|
|
b1edfb6fe4 | ||
|
|
1afa41526a | ||
|
|
08b16fa03f | ||
|
|
ccf46c40ef | ||
|
|
60a321e6f4 | ||
|
|
7d5c65be03 | ||
|
|
f8e7512e01 | ||
|
|
b7ce2c6e43 | ||
|
|
1f62256024 | ||
|
|
4ef2f01baa | ||
|
|
6042c0b1c0 | ||
|
|
0c4d132ee1 | ||
|
|
faa9a00716 | ||
|
|
27af4fd443 | ||
|
|
b16834fea6 | ||
|
|
8bfae70284 | ||
|
|
4279da999a | ||
|
|
54b8a82803 | ||
|
|
2e8292599a | ||
|
|
5562323eb8 | ||
|
|
2cc109f5e1 | ||
|
|
52d896b054 | ||
|
|
e4409ed6aa | ||
|
|
f0321432b4 | ||
|
|
d855a26bd9 | ||
|
|
607acb0a2d | ||
|
|
9b2fa80ab7 | ||
|
|
8a0a09dbb4 | ||
|
|
816ea68f9a | ||
|
|
3238623934 | ||
|
|
21aa9223df | ||
|
|
e2e03252e4 | ||
|
|
0ca83f74c7 | ||
|
|
d58c431199 | ||
|
|
3ff414fb1b | ||
|
|
77f8102148 | ||
|
|
0fd59c59e3 | ||
|
|
b1f9891849 | ||
|
|
6026bf308b | ||
|
|
24d8bfd641 | ||
|
|
14529c011f | ||
|
|
7378c18ab9 | ||
|
|
9ce7040b91 | ||
|
|
af0ba13573 | ||
|
|
09e5f1ad6b | ||
|
|
7541a45e77 | ||
|
|
1a91d00f2c | ||
|
|
19063a3e02 | ||
|
|
5e6dc9e533 | ||
|
|
ca64d4b9b6 | ||
|
|
b28edcd01f | ||
|
|
9c574f9eb7 | ||
|
|
d1b2cd2083 | ||
|
|
8c4dee0ca8 | ||
|
|
71ee18048e | ||
|
|
668983e16d | ||
|
|
17e23a6cf4 | ||
|
|
d18fe51553 | ||
|
|
9f9515ffe0 | ||
|
|
94353aa0bd | ||
|
|
3a2295ff53 | ||
|
|
de1b8a7673 | ||
|
|
39a26adcc2 | ||
|
|
eff0bc44ff | ||
|
|
c038c57278 | ||
|
|
420b7aa1a6 | ||
|
|
f22e405b50 | ||
|
|
3cfa592db9 | ||
|
|
81096e2d95 | ||
|
|
c415c4f176 | ||
|
|
86742da63d | ||
|
|
3c521cf5fa | ||
|
|
c087998710 | ||
|
|
b9800bfbab | ||
|
|
7b8270fdd6 | ||
|
|
6d3f888784 |
@@ -13,3 +13,12 @@
|
|||||||
# Python helper bytecode is local runtime state.
|
# Python helper bytecode is local runtime state.
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
|
# Generated from the colour scheme; machine state, not configuration.
|
||||||
|
/config/dot/kitty/current-theme.conf
|
||||||
|
|
||||||
|
# Generated from the colour scheme; machine state, not configuration.
|
||||||
|
/config/dot/gtk-3.0/settings.ini
|
||||||
|
/config/dot/gtk-4.0/settings.ini
|
||||||
|
/config/dot/tmux/current-theme.conf
|
||||||
|
/config/dot/hypr/hyprlock.conf
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ same Tokyo Night Moon palette.
|
|||||||
| Piece | What it is |
|
| Piece | What it is |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README |
|
| `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README |
|
||||||
| `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Panama Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI |
|
| `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI |
|
||||||
| `config/dot/vicinae/` | Raycast-style launcher, themed |
|
| `config/dot/vicinae/` | Raycast-style launcher, themed |
|
||||||
| `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) |
|
| `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) |
|
||||||
| `config/dot/wofi/` | Fallback launcher, in case the shell fails to start |
|
| `config/dot/wofi/` | Fallback launcher, in case the shell fails to start |
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Tokyo Night Day for btop, the light counterpart to tokyonight-moon.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# background. Same colours as kitty/themes/tokyonight-day.conf so the terminal
|
||||||
|
# and what runs inside it cannot disagree.
|
||||||
|
#
|
||||||
|
# Gradients keep the same low -> middle -> high meaning as the dark theme, using
|
||||||
|
# Day's darker, more saturated accents: the light versions of these hues are too
|
||||||
|
# faint to read as a filled meter.
|
||||||
|
|
||||||
|
theme[main_bg]="#e1e2e7"
|
||||||
|
theme[main_fg]="#3760bf"
|
||||||
|
theme[title]="#3760bf"
|
||||||
|
theme[hi_fg]="#2e7de9"
|
||||||
|
theme[selected_bg]="#c4c8da"
|
||||||
|
theme[selected_fg]="#2e7de9"
|
||||||
|
theme[inactive_fg]="#7079a8"
|
||||||
|
theme[proc_misc]="#587539"
|
||||||
|
|
||||||
|
theme[cpu_box]="#a8aecb"
|
||||||
|
theme[mem_box]="#a8aecb"
|
||||||
|
theme[net_box]="#a8aecb"
|
||||||
|
theme[proc_box]="#a8aecb"
|
||||||
|
theme[div_line]="#c4c8da"
|
||||||
|
|
||||||
|
theme[temp_start]="#2e7de9"
|
||||||
|
theme[temp_mid]="#8c6c3e"
|
||||||
|
theme[temp_end]="#f52a65"
|
||||||
|
|
||||||
|
theme[cpu_start]="#2e7de9"
|
||||||
|
theme[cpu_mid]="#9854f1"
|
||||||
|
theme[cpu_end]="#f52a65"
|
||||||
|
|
||||||
|
theme[free_start]="#c4c8da"
|
||||||
|
theme[free_mid]="#007197"
|
||||||
|
theme[free_end]="#2e7de9"
|
||||||
|
|
||||||
|
theme[cached_start]="#007197"
|
||||||
|
theme[cached_mid]="#2e7de9"
|
||||||
|
theme[cached_end]="#9854f1"
|
||||||
|
|
||||||
|
theme[available_start]="#8c6c3e"
|
||||||
|
theme[available_mid]="#b15c00"
|
||||||
|
theme[available_end]="#f52a65"
|
||||||
|
|
||||||
|
theme[used_start]="#587539"
|
||||||
|
theme[used_mid]="#8c6c3e"
|
||||||
|
theme[used_end]="#f52a65"
|
||||||
|
|
||||||
|
theme[download_start]="#c4c8da"
|
||||||
|
theme[download_mid]="#2e7de9"
|
||||||
|
theme[download_end]="#007197"
|
||||||
|
|
||||||
|
theme[upload_start]="#c4c8da"
|
||||||
|
theme[upload_mid]="#9854f1"
|
||||||
|
theme[upload_end]="#7847bd"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Tokyo Night Moon for btop, matched to Panama's palette.
|
||||||
|
#
|
||||||
|
# btop ships a "tokyo-night" theme, but it is the Night variant (#1a1b26). The
|
||||||
|
# rest of this desktop is Moon (#222436), and two Tokyo Nights side by side read
|
||||||
|
# as a mistake rather than as a choice.
|
||||||
|
#
|
||||||
|
# The *_start/_mid/_end triples are gradients btop draws meters with: low,
|
||||||
|
# middle, and high. They run blue -> yellow -> red so a saturated resource is
|
||||||
|
# obvious at a glance without reading the number.
|
||||||
|
|
||||||
|
theme[main_bg]="#222436"
|
||||||
|
theme[main_fg]="#c8d3f5"
|
||||||
|
theme[title]="#c8d3f5"
|
||||||
|
theme[hi_fg]="#82aaff"
|
||||||
|
theme[selected_bg]="#3b4261"
|
||||||
|
theme[selected_fg]="#82aaff"
|
||||||
|
theme[inactive_fg]="#636da6"
|
||||||
|
theme[proc_misc]="#a5e8b5"
|
||||||
|
|
||||||
|
theme[cpu_box]="#4d5685"
|
||||||
|
theme[mem_box]="#4d5685"
|
||||||
|
theme[net_box]="#4d5685"
|
||||||
|
theme[proc_box]="#4d5685"
|
||||||
|
theme[div_line]="#3b4261"
|
||||||
|
|
||||||
|
theme[temp_start]="#82aaff"
|
||||||
|
theme[temp_mid]="#ffc777"
|
||||||
|
theme[temp_end]="#ff757f"
|
||||||
|
|
||||||
|
theme[cpu_start]="#82aaff"
|
||||||
|
theme[cpu_mid]="#c099ff"
|
||||||
|
theme[cpu_end]="#ff757f"
|
||||||
|
|
||||||
|
theme[free_start]="#3b4261"
|
||||||
|
theme[free_mid]="#589ed7"
|
||||||
|
theme[free_end]="#86e1fc"
|
||||||
|
|
||||||
|
theme[cached_start]="#86e1fc"
|
||||||
|
theme[cached_mid]="#82aaff"
|
||||||
|
theme[cached_end]="#c099ff"
|
||||||
|
|
||||||
|
theme[available_start]="#ffc777"
|
||||||
|
theme[available_mid]="#ff966c"
|
||||||
|
theme[available_end]="#ff757f"
|
||||||
|
|
||||||
|
theme[used_start]="#a5e8b5"
|
||||||
|
theme[used_mid]="#ffc777"
|
||||||
|
theme[used_end]="#ff757f"
|
||||||
|
|
||||||
|
theme[download_start]="#3b4261"
|
||||||
|
theme[download_mid]="#82aaff"
|
||||||
|
theme[download_end]="#86e1fc"
|
||||||
|
|
||||||
|
theme[upload_start]="#3b4261"
|
||||||
|
theme[upload_mid]="#c099ff"
|
||||||
|
theme[upload_end]="#fca7ea"
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
# GENERATED FILE -- edit settings.ini.template instead.
|
||||||
|
#
|
||||||
|
# The theme name and dark preference below follow Panama's colour
|
||||||
|
# scheme, so this file is regenerated on every switch and is not
|
||||||
|
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
||||||
|
# XSETTINGS and this file is ignored; under Hyprland there is no
|
||||||
|
# settings daemon, so for GTK3 it is authoritative -- which is why it
|
||||||
|
# has to change with the scheme rather than being pinned to dark.
|
||||||
|
#
|
||||||
[Settings]
|
[Settings]
|
||||||
# These values must match `gsettings get org.gnome.desktop.interface ...`.
|
# These values must match `gsettings get org.gnome.desktop.interface ...`.
|
||||||
#
|
#
|
||||||
@@ -6,12 +15,12 @@
|
|||||||
# this file becomes authoritative for GTK3 — which is why it previously named
|
# this file becomes authoritative for GTK3 — which is why it previously named
|
||||||
# themes that aren't installed (Tahoe-Dark, WhiteSur-cursors) without anything
|
# themes that aren't installed (Tahoe-Dark, WhiteSur-cursors) without anything
|
||||||
# appearing broken.
|
# appearing broken.
|
||||||
gtk-theme-name=adw-gtk3-dark
|
gtk-theme-name=@GTK_THEME@
|
||||||
gtk-icon-theme-name=Adwaita
|
gtk-icon-theme-name=Adwaita
|
||||||
gtk-font-name=Adwaita Sans 11
|
gtk-font-name=Adwaita Sans 11
|
||||||
gtk-cursor-theme-name=oreo_blue_cursors
|
gtk-cursor-theme-name=oreo_blue_cursors
|
||||||
gtk-cursor-theme-size=24
|
gtk-cursor-theme-size=24
|
||||||
gtk-application-prefer-dark-theme=1
|
gtk-application-prefer-dark-theme=@PREFER_DARK@
|
||||||
|
|
||||||
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
||||||
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
|
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[Settings]
|
|
||||||
# libadwaita apps normally take their dark preference from the
|
|
||||||
# org.freedesktop.appearance portal (served by xdg-desktop-portal-gtk, which
|
|
||||||
# reads gsettings). This file is the fallback for plain GTK4 apps and for the
|
|
||||||
# window before the portal answers.
|
|
||||||
gtk-application-prefer-dark-theme=1
|
|
||||||
gtk-theme-name=adw-gtk3-dark
|
|
||||||
gtk-icon-theme-name=Adwaita
|
|
||||||
gtk-font-name=Adwaita Sans 11
|
|
||||||
gtk-cursor-theme-name=oreo_blue_cursors
|
|
||||||
gtk-cursor-theme-size=24
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# GENERATED FILE -- edit settings.ini.template instead.
|
||||||
|
#
|
||||||
|
# The theme name and dark preference below follow Panama's colour
|
||||||
|
# scheme, so this file is regenerated on every switch and is not
|
||||||
|
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
||||||
|
# XSETTINGS and this file is ignored; under Hyprland there is no
|
||||||
|
# settings daemon, so for GTK3 it is authoritative -- which is why it
|
||||||
|
# has to change with the scheme rather than being pinned to dark.
|
||||||
|
#
|
||||||
|
[Settings]
|
||||||
|
# libadwaita apps normally take their dark preference from the
|
||||||
|
# org.freedesktop.appearance portal (served by xdg-desktop-portal-gtk, which
|
||||||
|
# reads gsettings). This file is the fallback for plain GTK4 apps and for the
|
||||||
|
# window before the portal answers.
|
||||||
|
gtk-application-prefer-dark-theme=@PREFER_DARK@
|
||||||
|
gtk-theme-name=@GTK_THEME@
|
||||||
|
gtk-icon-theme-name=Adwaita
|
||||||
|
gtk-font-name=Adwaita Sans 11
|
||||||
|
gtk-cursor-theme-name=oreo_blue_cursors
|
||||||
|
gtk-cursor-theme-size=24
|
||||||
@@ -36,7 +36,32 @@ 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 | Panama Settings 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; labelled 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
|
||||||
|
|
||||||
|
Panama stays silent while the desktop is healthy. A compact bar indicator
|
||||||
|
appears only for actionable warnings or errors and opens the same **System
|
||||||
|
Health** page available from Settings and the Vicinae command **Panama: Check
|
||||||
|
System Health**. The terminal summary is available with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.config/quickshell/scripts/panama-doctor --summary
|
||||||
|
```
|
||||||
|
|
||||||
|
The doctor reports authored, redacted observations about Panama-owned services,
|
||||||
|
tools, links, and configured integrations. It does not read secrets, clipboard
|
||||||
|
or notification contents, calendar events, SSIDs, or device addresses. Repairs
|
||||||
|
are a small allow-list: Panama user services, Panama-owned links and launcher
|
||||||
|
commands, duplicate Panama Caffeine inhibitors, and a confirmed shell restart.
|
||||||
|
They never install packages, invoke `sudo`, delete user data, or rewrite
|
||||||
|
arbitrary configuration.
|
||||||
|
|
||||||
|
Generic Fedora configuration remains with the system tools that own it. The
|
||||||
|
final System Health card hands network settings, users, sharing, colour
|
||||||
|
profiles, and digital wellbeing to their exact GNOME Settings panels rather
|
||||||
|
than presenting inert Hyprland controls.
|
||||||
|
|
||||||
## GNOME extension migration
|
## GNOME extension migration
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Don't "fix" them.
|
|||||||
| File | Contents |
|
| File | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
||||||
| `prefs.lua` | Reads the settings file Panama Settings 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` | Colours, blur, glow, shadows, animations, VRR, scanout |
|
||||||
@@ -59,7 +59,7 @@ generated elsewhere:
|
|||||||
`quickshell/scripts/panama-idle` regenerates the hypridle config from the
|
`quickshell/scripts/panama-idle` regenerates the hypridle config from the
|
||||||
settings store and restarts the daemon. `hypridle.conf` in this directory
|
settings store and restarts the daemon. `hypridle.conf` in this directory
|
||||||
remains the shipped default and is what runs when the drop-in is not installed;
|
remains the shipped default and is what runs when the drop-in is not installed;
|
||||||
Panama Settings shows which of the two states you are in rather than presenting
|
The Settings app shows which of the two states you are in rather than presenting
|
||||||
controls that quietly do nothing.
|
controls that quietly do nothing.
|
||||||
|
|
||||||
Remove the drop-in and go back to the shipped config with:
|
Remove the drop-in and go back to the shipped config with:
|
||||||
@@ -77,7 +77,7 @@ relationship is:
|
|||||||
`prefs.get("key", <shipped value>)`, so the config still works standalone with
|
`prefs.get("key", <shipped value>)`, so the config still works standalone with
|
||||||
no settings file at all.
|
no settings file at all.
|
||||||
- **The JSON is the truth.** Hyprland and Quickshell both read it.
|
- **The JSON is the truth.** Hyprland and Quickshell both read it.
|
||||||
- **Panama Settings is the editor.** It writes the file *and* applies the change
|
- **The Settings app is the editor.** It writes the file *and* applies the change
|
||||||
live, so nothing needs a reload and the two sides cannot drift apart.
|
live, so nothing needs a reload and the two sides cannot drift apart.
|
||||||
|
|
||||||
To add an adjustable setting: add an entry to
|
To add an adjustable setting: add an entry to
|
||||||
@@ -95,7 +95,7 @@ accepts the config in each of those states.
|
|||||||
|
|
||||||
Every bind in `keybinds.lua` goes through a local `bind()` wrapper that
|
Every bind in `keybinds.lua` goes through a local `bind()` wrapper that
|
||||||
substitutes the chord from a stored override, so shortcuts can be moved from
|
substitutes the chord from a stored override, so shortcuts can be moved from
|
||||||
Panama Settings without editing this file.
|
the Settings app without editing this file.
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||||
@@ -118,7 +118,7 @@ shipped one, so a hand-edited `settings.json` cannot cost you a keymap.
|
|||||||
|
|
||||||
Every `hl.bind` must pass a `description`. Hyprland reports Lua-defined binds
|
Every `hl.bind` must pass a `description`. Hyprland reports Lua-defined binds
|
||||||
with dispatcher `__lua` and a bytecode offset as the argument, so a bind without
|
with dispatcher `__lua` and a bytecode offset as the argument, so a bind without
|
||||||
one has nothing readable beside its chord, and Panama Settings drops it from the
|
one has nothing readable beside its chord, and the Settings app drops it from the
|
||||||
Input & Shortcuts page rather than showing a mystery row.
|
Input & Shortcuts page rather than showing a mystery row.
|
||||||
`tests/quickshell/keybinds-contract.sh` fails if any bind lacks a description, so
|
`tests/quickshell/keybinds-contract.sh` fails if any bind lacks a description, so
|
||||||
this cannot regress silently.
|
this cannot regress silently.
|
||||||
@@ -275,7 +275,7 @@ The mental model is unchanged from Forge:
|
|||||||
| `SUPER + V` | Clipboard history |
|
| `SUPER + V` | Clipboard history |
|
||||||
| `SUPER + .` | Emoji picker |
|
| `SUPER + .` | Emoji picker |
|
||||||
| `SUPER + S` | Quick settings |
|
| `SUPER + S` | Quick settings |
|
||||||
| `SUPER + I` | Panama 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 centre |
|
||||||
| `SUPER + grave` | Workspace overview |
|
| `SUPER + grave` | Workspace overview |
|
||||||
@@ -288,7 +288,7 @@ The mental model is unchanged from Forge:
|
|||||||
|
|
||||||
### Apps
|
### Apps
|
||||||
`SUPER + T` terminal · `N` neovim · `W` browser · `F` files · `C` calculator ·
|
`SUPER + T` terminal · `N` neovim · `W` browser · `F` files · `C` calculator ·
|
||||||
`E` mail · `I` Panama Settings · `CTRL + SHIFT + Esc` system monitor. GNOME
|
`E` mail · `I` Settings · `CTRL + SHIFT + Esc` system monitor. GNOME
|
||||||
Settings remains searchable in Vicinae for hardware and account panels.
|
Settings remains searchable in Vicinae for hardware and account panels.
|
||||||
|
|
||||||
## Calendar and notifications
|
## Calendar and notifications
|
||||||
@@ -326,7 +326,7 @@ 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 favourites at rest and every selected light
|
||||||
when expanded. Use **Panama Settings → Home & Phone** to choose favourites,
|
when expanded. Use **Settings → Home & Phone** to choose favourites,
|
||||||
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
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
-- instead. They still work here; see the session notes in autostart.lua.
|
-- instead. They still work here; see the session notes in autostart.lua.
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = require("prefs")
|
||||||
|
|
||||||
-- ── GPU selection ───────────────────────────────────────────────────────────
|
-- ── GPU selection ───────────────────────────────────────────────────────────
|
||||||
-- This box has a discrete RX 7800 XT (0000:03:00.0) and a Granite Ridge iGPU
|
-- This box has a discrete RX 7800 XT (0000:03:00.0) and a Granite Ridge iGPU
|
||||||
-- (0000:12:00.0). The monitor hangs off the dGPU, so the dGPU must render.
|
-- (0000:12:00.0). The monitor hangs off the dGPU, so the dGPU must render.
|
||||||
@@ -47,7 +49,7 @@ end
|
|||||||
-- (it has cursors/ + index.theme and no hyprcursors/ directory or manifest.hl),
|
-- (it has cursors/ + index.theme and no hyprcursors/ directory or manifest.hl),
|
||||||
-- so setting it would point hyprcursor at nothing. Hyprland falls back to the
|
-- so setting it would point hyprcursor at nothing. Hyprland falls back to the
|
||||||
-- XCursor path, which is what we want.
|
-- XCursor path, which is what we want.
|
||||||
hl.env("XCURSOR_THEME", "oreo_blue_cursors")
|
hl.env("XCURSOR_THEME", prefs.get("cursorTheme", "oreo_blue_cursors"))
|
||||||
hl.env("XCURSOR_SIZE", "24")
|
hl.env("XCURSOR_SIZE", "24")
|
||||||
|
|
||||||
-- ── Toolkits ────────────────────────────────────────────────────────────────
|
-- ── Toolkits ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
general {
|
general {
|
||||||
# `pidof` guard prevents stacking lockers if this fires twice.
|
# `pidof` guard prevents stacking lockers if this fires twice.
|
||||||
lock_cmd = pidof hyprlock || hyprlock
|
lock_cmd = pidof hyprlock || ~/.config/quickshell/scripts/panama-lock run
|
||||||
before_sleep_cmd = loginctl lock-session
|
before_sleep_cmd = loginctl lock-session
|
||||||
after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# hyprlock — lock screen
|
# hyprlock — lock screen.
|
||||||
#
|
#
|
||||||
# Tokyo Night Moon, matching quickshell/config/Theme.qml:
|
# GENERATED FILE. Edit hyprlock.conf.template and re-run
|
||||||
# accent #82aaff fg #c8d3f5 dim #828bb8 bg #222436 red #ff757f
|
# quickshell/scripts/panama-theme-apps; editing this copy is overwritten on the
|
||||||
|
# next colour scheme change.
|
||||||
|
#
|
||||||
|
# The colours 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
|
||||||
|
# stayed dark when everything else went light.
|
||||||
|
#
|
||||||
|
# hyprlock takes rgba(r, g, b, a) in DECIMAL rather than hex, which is why the
|
||||||
|
# template carries "R, G, B" triples where the rest of Panama uses hex. The two
|
||||||
|
# Pango markup values are the exception and want ##rrggbb.
|
||||||
#
|
#
|
||||||
# hyprlang syntax, not Lua — hyprlock is a separate project from Hyprland.
|
# hyprlang syntax, not Lua — hyprlock is a separate project from Hyprland.
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -45,7 +54,7 @@ background {
|
|||||||
vibrancy_darkness = 0.05
|
vibrancy_darkness = 0.05
|
||||||
|
|
||||||
# Shown if the screenshot is unavailable.
|
# Shown if the screenshot is unavailable.
|
||||||
color = rgba(34, 36, 54, 1.0)
|
color = rgba(@BG@, 1.0)
|
||||||
|
|
||||||
zindex = -1
|
zindex = -1
|
||||||
}
|
}
|
||||||
@@ -54,7 +63,7 @@ background {
|
|||||||
label {
|
label {
|
||||||
monitor =
|
monitor =
|
||||||
text = cmd[update:1000] date +"%-I:%M"
|
text = cmd[update:1000] date +"%-I:%M"
|
||||||
color = rgba(200, 211, 245, 1.0)
|
color = rgba(@FG@, 1.0)
|
||||||
font_size = 120
|
font_size = 120
|
||||||
font_family = Adwaita Sans Light
|
font_family = Adwaita Sans Light
|
||||||
position = 0, 260
|
position = 0, 260
|
||||||
@@ -65,7 +74,7 @@ label {
|
|||||||
label {
|
label {
|
||||||
monitor =
|
monitor =
|
||||||
text = cmd[update:60000] date +"%A, %B %-d"
|
text = cmd[update:60000] date +"%A, %B %-d"
|
||||||
color = rgba(130, 139, 184, 1.0)
|
color = rgba(@MUTED@, 1.0)
|
||||||
font_size = 24
|
font_size = 24
|
||||||
font_family = Adwaita Sans
|
font_family = Adwaita Sans
|
||||||
position = 0, 160
|
position = 0, 160
|
||||||
@@ -84,18 +93,18 @@ input-field {
|
|||||||
outline_thickness = 2
|
outline_thickness = 2
|
||||||
rounding = 26
|
rounding = 26
|
||||||
|
|
||||||
outer_color = rgba(130, 170, 255, 0.9)
|
outer_color = rgba(@ACCENT@, 0.9)
|
||||||
inner_color = rgba(46, 47, 61, 0.85)
|
inner_color = rgba(@FIELD@, 0.85)
|
||||||
font_color = rgba(200, 211, 245, 1.0)
|
font_color = rgba(@FG@, 1.0)
|
||||||
check_color = rgba(130, 170, 255, 1.0)
|
check_color = rgba(@ACCENT@, 1.0)
|
||||||
fail_color = rgba(255, 117, 127, 1.0)
|
fail_color = rgba(@ERROR@, 1.0)
|
||||||
|
|
||||||
dots_size = 0.25
|
dots_size = 0.25
|
||||||
dots_spacing = 0.3
|
dots_spacing = 0.3
|
||||||
dots_center = true
|
dots_center = true
|
||||||
|
|
||||||
placeholder_text = <span foreground="##828bb8"><i>Password</i></span>
|
placeholder_text = <span foreground="##@MUTED_HEX@"><i>Password</i></span>
|
||||||
fail_text = <span foreground="##ff757f"><i>$FAIL ($ATTEMPTS)</i></span>
|
fail_text = <span foreground="##@ERROR_HEX@"><i>$FAIL ($ATTEMPTS)</i></span>
|
||||||
|
|
||||||
fade_on_empty = false
|
fade_on_empty = false
|
||||||
hide_input = false
|
hide_input = false
|
||||||
@@ -105,7 +114,7 @@ input-field {
|
|||||||
label {
|
label {
|
||||||
monitor =
|
monitor =
|
||||||
text = $USER
|
text = $USER
|
||||||
color = rgba(200, 211, 245, 0.9)
|
color = rgba(@FG@, 0.9)
|
||||||
font_size = 16
|
font_size = 16
|
||||||
font_family = Adwaita Sans
|
font_family = Adwaita Sans
|
||||||
position = 0, -110
|
position = 0, -110
|
||||||
@@ -10,9 +10,9 @@ local prefs = require("prefs")
|
|||||||
hl.config({
|
hl.config({
|
||||||
input = {
|
input = {
|
||||||
kb_layout = prefs.get("keyboardLayout", "us"),
|
kb_layout = prefs.get("keyboardLayout", "us"),
|
||||||
kb_variant = "",
|
kb_variant = prefs.get("keyboardVariant", ""),
|
||||||
kb_model = "",
|
kb_model = "",
|
||||||
kb_options = "",
|
kb_options = prefs.get("keyboardOptions", "caps:escape_shifted_capslock"),
|
||||||
kb_rules = "",
|
kb_rules = "",
|
||||||
|
|
||||||
numlock_by_default = prefs.get("numlockByDefault", true),
|
numlock_by_default = prefs.get("numlockByDefault", true),
|
||||||
@@ -21,10 +21,18 @@ hl.config({
|
|||||||
repeat_delay = prefs.get("keyRepeatDelay", 500),
|
repeat_delay = prefs.get("keyRepeatDelay", 500),
|
||||||
repeat_rate = prefs.get("keyRepeatRate", 33),
|
repeat_rate = prefs.get("keyRepeatRate", 33),
|
||||||
|
|
||||||
-- 1 = click to focus. GNOME's behaviour; NOT sloppy focus.
|
-- 1 = FOLLOW. The window under the pointer takes focus. This comment
|
||||||
|
-- previously claimed 1 was "click to focus, GNOME's behaviour", which
|
||||||
|
-- is the opposite of what Hyprland does -- `hyprctl descriptions` gives
|
||||||
|
-- map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}],
|
||||||
|
-- so click-to-focus is 0. Changing the shipped value is a behaviour
|
||||||
|
-- decision rather than a correction, so the value is left alone and
|
||||||
|
-- only the description is fixed; Settings exposes all four.
|
||||||
follow_mouse = prefs.getInt("followMouse", 1),
|
follow_mouse = prefs.getInt("followMouse", 1),
|
||||||
|
|
||||||
-- Don't refocus on mouse move alone -- only on click.
|
-- Softens follow_mouse: with this off, focus changes only when the
|
||||||
|
-- pointer crosses a window boundary, not on every movement inside one.
|
||||||
|
-- Still focus-follows-pointer, just less twitchy.
|
||||||
mouse_refocus = false,
|
mouse_refocus = false,
|
||||||
|
|
||||||
-- Flat pointer response, no acceleration. Matters for gaming.
|
-- Flat pointer response, no acceleration. Matters for gaming.
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ local sysmonitor = "flatpak run io.missioncenter.MissionCenter"
|
|||||||
-- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the
|
-- Vicinae is the Raycast-style launcher. `vicinae toggle` shows/hides the
|
||||||
-- window against the already-running server (started in autostart.lua).
|
-- window against the already-running server (started in autostart.lua).
|
||||||
local launcher = "vicinae toggle"
|
local launcher = "vicinae toggle"
|
||||||
|
local osd = function(action)
|
||||||
|
return "$HOME/.config/quickshell/scripts/panama-osd " .. action
|
||||||
|
end
|
||||||
|
|
||||||
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
||||||
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
||||||
@@ -77,7 +80,7 @@ bind(mod .. " + W", hl.dsp.exec_cmd(browser), { description = "Browser" })
|
|||||||
bind(mod .. " + F", hl.dsp.exec_cmd(files), { description = "Files" })
|
bind(mod .. " + F", hl.dsp.exec_cmd(files), { description = "Files" })
|
||||||
bind(mod .. " + C", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
bind(mod .. " + C", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
||||||
bind(mod .. " + E", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
bind(mod .. " + E", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
||||||
bind(mod .. " + I", hl.dsp.exec_cmd(settings), { description = "Panama Settings" })
|
bind(mod .. " + I", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||||
bind("CTRL + SHIFT + Escape", hl.dsp.exec_cmd(sysmonitor), { description = "System monitor" })
|
bind("CTRL + SHIFT + Escape", hl.dsp.exec_cmd(sysmonitor), { description = "System monitor" })
|
||||||
|
|
||||||
-- ── Launcher ────────────────────────────────────────────────────────────────
|
-- ── Launcher ────────────────────────────────────────────────────────────────
|
||||||
@@ -192,9 +195,21 @@ bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = t
|
|||||||
bind(mod .. " + SHIFT + P", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
bind(mod .. " + SHIFT + P", 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).
|
-- Window cycling (GNOME: cycle-windows on SUPER+Tab), now with an overlay
|
||||||
bind(mod .. " + Tab", hl.dsp.window.cycle_next({ next = true }), { description = "Next window" })
|
-- showing what you are choosing between.
|
||||||
bind(mod .. " + SHIFT + Tab", hl.dsp.window.cycle_next({ next = false }), { description = "Previous window" })
|
--
|
||||||
|
-- The gesture needs three binds, not two. Tab steps the selection, and the
|
||||||
|
-- switch is only COMMITTED when the modifier is released -- which is the sole
|
||||||
|
-- way the compositor can tell the gesture is finished. That release bind is on
|
||||||
|
-- the bare modifier, so it fires on EVERY Super release in the session; the
|
||||||
|
-- handler returns immediately when no switch is open, which is why this is
|
||||||
|
-- affordable.
|
||||||
|
--
|
||||||
|
-- The release bind carries no description on purpose: it is not a shortcut
|
||||||
|
-- anyone would look up or rebind, and the Shortcuts page lists what it finds.
|
||||||
|
bind(mod .. " + Tab", hl.dsp.exec_cmd(qs("switcher", "next")), { description = "Next window" })
|
||||||
|
bind(mod .. " + SHIFT + Tab", hl.dsp.exec_cmd(qs("switcher", "previous")), { description = "Previous window" })
|
||||||
|
bind(mod, hl.dsp.exec_cmd(qs("switcher", "commit")), { release = true, description = "Commit window switch" })
|
||||||
-- Jump back to the previously focused window.
|
-- Jump back to the previously focused window.
|
||||||
bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" })
|
bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" })
|
||||||
|
|
||||||
@@ -252,23 +267,23 @@ bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { descri
|
|||||||
-- ── Media and volume ────────────────────────────────────────────────────────
|
-- ── Media and volume ────────────────────────────────────────────────────────
|
||||||
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
||||||
-- 6% steps match the GNOME volume-step setting.
|
-- 6% steps match the GNOME volume-step setting.
|
||||||
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 6%+"), { locked = true, repeating = true , description = "Volume up" })
|
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 6")), { locked = true, repeating = true , description = "Volume up" })
|
||||||
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 6%-"), { locked = true, repeating = true , description = "Volume down" })
|
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 6")), { locked = true, repeating = true , description = "Volume down" })
|
||||||
bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true , description = "Mute" })
|
bind("XF86AudioMute", hl.dsp.exec_cmd(osd("volume toggle")), { locked = true , description = "Mute" })
|
||||||
bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true , description = "Mute microphone" })
|
bind("XF86AudioMicMute", hl.dsp.exec_cmd(osd("microphone toggle")), { locked = true , description = "Mute microphone" })
|
||||||
|
|
||||||
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
||||||
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 1%+"), { locked = true, repeating = true , description = "Volume up (fine)" })
|
bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 1")), { locked = true, repeating = true , description = "Volume up (fine)" })
|
||||||
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"), { locked = true, repeating = true , description = "Volume down (fine)" })
|
bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 1")), { locked = true, repeating = true , description = "Volume down (fine)" })
|
||||||
|
|
||||||
bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
bind("XF86AudioPlay", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
||||||
bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
bind("XF86AudioPause", hl.dsp.exec_cmd(osd("media play-pause")), { locked = true , description = "Play or pause" })
|
||||||
bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true , description = "Next track" })
|
bind("XF86AudioNext", hl.dsp.exec_cmd(osd("media next")), { locked = true , description = "Next track" })
|
||||||
bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true , description = "Previous track" })
|
bind("XF86AudioPrev", hl.dsp.exec_cmd(osd("media previous")), { locked = true , description = "Previous track" })
|
||||||
bind("XF86AudioStop", hl.dsp.exec_cmd("playerctl stop"), { locked = true , description = "Stop playback" })
|
bind("XF86AudioStop", hl.dsp.exec_cmd(osd("media stop")), { locked = true , description = "Stop playback" })
|
||||||
|
|
||||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true , description = "Brightness up" })
|
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
||||||
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true , description = "Brightness down" })
|
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd(osd("brightness down 5")), { locked = true, repeating = true , description = "Brightness down" })
|
||||||
|
|
||||||
-- Hardware keys GNOME mapped that have obvious equivalents.
|
-- Hardware keys GNOME mapped that have obvious equivalents.
|
||||||
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||||
|
|||||||
+56
-19
@@ -22,17 +22,22 @@ hl.config({
|
|||||||
border_size = prefs.get("borderSize", 2),
|
border_size = prefs.get("borderSize", 2),
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
-- The prism: blue leads, orchid follows, on a diagonal so the pair
|
-- The focused accent role: blue leads, orchid follows, on a
|
||||||
-- is visible on both a tall and a wide window. Same two colours as
|
-- diagonal so the pair is visible on both a tall and a wide
|
||||||
-- the shell's hairline (quickshell/widgets/PrismEdge.qml) and the
|
-- window. ColorScheme never writes this role; a future accent
|
||||||
-- tmux theme this palette came from.
|
-- picker can own it without fighting light/dark mode.
|
||||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
||||||
-- Unfocused windows get no colour at all. The gradient only means
|
-- The neutral inactive role follows the colour scheme because a
|
||||||
-- something if exactly one window on screen is wearing it.
|
-- dark neutral disappears against a light desktop.
|
||||||
inactive_border = "rgba(3b426199)",
|
-- services/ColorScheme.qml applies the same values live; this is
|
||||||
|
-- the value a fresh session starts from.
|
||||||
|
inactive_border = prefs.get("colorScheme", "dark") == "light"
|
||||||
|
and "rgba(a8aecb99)" or "rgba(3b426199)",
|
||||||
},
|
},
|
||||||
|
|
||||||
resize_on_border = true,
|
resize_on_border = prefs.get("resizeOnBorder", true),
|
||||||
|
extend_border_grab_area = prefs.getInt("borderGrabArea", 15),
|
||||||
|
hover_icon_on_border = prefs.get("hoverIconOnBorder", true),
|
||||||
|
|
||||||
-- Enables the per-window "immediate" rule used for games in rules.lua.
|
-- Enables the per-window "immediate" rule used for games in rules.lua.
|
||||||
-- Harmless on its own; tearing only happens where a rule opts in.
|
-- Harmless on its own; tearing only happens where a rule opts in.
|
||||||
@@ -40,16 +45,22 @@ hl.config({
|
|||||||
|
|
||||||
layout = "dwindle",
|
layout = "dwindle",
|
||||||
|
|
||||||
snap = { enabled = true },
|
snap = {
|
||||||
|
enabled = true,
|
||||||
|
window_gap = prefs.getInt("snapWindowGap", 10),
|
||||||
|
monitor_gap = prefs.getInt("snapMonitorGap", 10),
|
||||||
|
respect_gaps = prefs.get("snapRespectGaps", false),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
decoration = {
|
decoration = {
|
||||||
-- 18 to match the shell's popover radius, so a window and a panel sitting
|
-- 18 to match the shell's popover radius, so a window and a panel sitting
|
||||||
-- next to each other read as the same object family.
|
-- next to each other read as the same object family.
|
||||||
rounding = prefs.get("windowRounding", 18),
|
rounding = prefs.get("windowRounding", 18),
|
||||||
rounding_power = 2,
|
rounding_power = prefs.get("roundingPower", 2),
|
||||||
|
|
||||||
active_opacity = 1.0,
|
active_opacity = prefs.get("activeOpacity", 1.0),
|
||||||
|
fullscreen_opacity = prefs.get("fullscreenOpacity", 1.0),
|
||||||
inactive_opacity = prefs.get("inactiveOpacity", 1.0),
|
inactive_opacity = prefs.get("inactiveOpacity", 1.0),
|
||||||
|
|
||||||
blur = {
|
blur = {
|
||||||
@@ -79,9 +90,13 @@ hl.config({
|
|||||||
shadow = {
|
shadow = {
|
||||||
enabled = prefs.get("shadowEnabled", true),
|
enabled = prefs.get("shadowEnabled", true),
|
||||||
range = prefs.get("shadowRange", 20),
|
range = prefs.get("shadowRange", 20),
|
||||||
render_power = 3,
|
render_power = prefs.getInt("shadowRenderPower", 3),
|
||||||
sharp = false,
|
sharp = prefs.get("shadowSharp", false),
|
||||||
color = "rgba(15161eee)",
|
color = "rgba(15161eee)",
|
||||||
|
-- Deliberately not a setting: a two-axis offset needs a control we
|
||||||
|
-- do not have, and a slider bound to half a value is worse than
|
||||||
|
-- leaving it alone. SystemSettings understands the vec2 shape
|
||||||
|
-- already, so adding it later is only a matter of the widget.
|
||||||
offset = { 0, 4 },
|
offset = { 0, 4 },
|
||||||
scale = 1.0,
|
scale = 1.0,
|
||||||
},
|
},
|
||||||
@@ -106,16 +121,38 @@ hl.config({
|
|||||||
dwindle = {
|
dwindle = {
|
||||||
-- Keep the split orientation a window was created with. Closest match
|
-- Keep the split orientation a window was created with. Closest match
|
||||||
-- to how the Forge extension behaved on GNOME.
|
-- to how the Forge extension behaved on GNOME.
|
||||||
preserve_split = true,
|
preserve_split = prefs.get("preserveSplit", true),
|
||||||
smart_resizing = true,
|
smart_resizing = true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
-- Only in effect when the tiling layout is "master". Panama ships dwindle,
|
||||||
|
-- but Settings offers master as a choice, and a layout you can select and
|
||||||
|
-- cannot configure is barely a choice at all.
|
||||||
|
master = {
|
||||||
|
mfact = prefs.get("masterFactor", 0.55),
|
||||||
|
orientation = prefs.get("masterOrientation", "left"),
|
||||||
|
new_status = prefs.get("masterNewStatus", "slave"),
|
||||||
|
new_on_top = prefs.get("masterNewOnTop", false),
|
||||||
|
},
|
||||||
|
|
||||||
misc = {
|
misc = {
|
||||||
force_default_wallpaper = 0,
|
force_default_wallpaper = 0,
|
||||||
disable_hyprland_logo = true,
|
-- Stored as "show the logo / show the splash" and written as Hyprland's
|
||||||
disable_splash_rendering = true,
|
-- `disable_*`, matching the `invert` flag on these entries in
|
||||||
|
-- PreferenceSchema so both sides agree about which way round they are.
|
||||||
|
disable_hyprland_logo = not prefs.get("hyprlandLogo", false),
|
||||||
|
disable_splash_rendering = not prefs.get("hyprlandSplash", false),
|
||||||
|
|
||||||
font_family = "Adwaita Sans",
|
-- Keep native Wayland selection paste and GTK's matching preference
|
||||||
|
-- in lockstep. DesktopStyle applies the GTK half only after this value
|
||||||
|
-- has been read back and stored by SystemSettings.
|
||||||
|
middle_click_paste = prefs.get("middleClickPaste", true),
|
||||||
|
|
||||||
|
-- Same setting as Theme.fontFamily in the shell. If only the QML side
|
||||||
|
-- followed the preference, the compositor and the shell would disagree
|
||||||
|
-- about the interface font and nobody would notice until a tooltip
|
||||||
|
-- rendered in a different typeface.
|
||||||
|
font_family = prefs.get("interfaceFont", "Adwaita Sans"),
|
||||||
|
|
||||||
-- Variable refresh rate. 3 = enable only for fullscreen windows whose
|
-- Variable refresh rate. 3 = enable only for fullscreen windows whose
|
||||||
-- content type is "video" or "game" -- the tag rules.lua applies to
|
-- content type is "video" or "game" -- the tag rules.lua applies to
|
||||||
@@ -164,8 +201,8 @@ hl.config({
|
|||||||
},
|
},
|
||||||
|
|
||||||
ecosystem = {
|
ecosystem = {
|
||||||
no_update_news = true,
|
no_update_news = not prefs.get("hyprlandUpdateNews", false),
|
||||||
no_donation_nag = true,
|
no_donation_nag = not prefs.get("hyprlandDonationNag", false),
|
||||||
},
|
},
|
||||||
|
|
||||||
xwayland = {
|
xwayland = {
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
local prefs = require("prefs")
|
local prefs = require("prefs")
|
||||||
|
|
||||||
-- Per-output overrides written by Panama Settings, keyed by output name:
|
-- Per-output overrides written by Panama Settings, keyed by output name:
|
||||||
-- { ["DP-2"] = { mode = "3840x2160@60", scale = 2, transform = 0 } }
|
-- { ["DP-2"] = {
|
||||||
|
-- mode = "3840x2160@60", scale = 2, transform = 0,
|
||||||
|
-- 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. Colour 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
|
||||||
@@ -71,6 +74,26 @@ local function valid_transform(transform)
|
|||||||
and transform <= 3
|
and transform <= 3
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function valid_coordinate(value)
|
||||||
|
return type(value) == "number"
|
||||||
|
and value == value
|
||||||
|
and value == math.floor(value)
|
||||||
|
and value >= -100000
|
||||||
|
and value <= 100000
|
||||||
|
end
|
||||||
|
|
||||||
|
local function valid_position(entry)
|
||||||
|
return valid_coordinate(entry.x) and valid_coordinate(entry.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function valid_primary(entry)
|
||||||
|
return type(entry.primary) == "boolean"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_layout_fields(entry)
|
||||||
|
return entry.x ~= nil or entry.y ~= nil or entry.primary ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
local function display_entry(output)
|
local function display_entry(output)
|
||||||
if type(output) ~= "string" or output == ""
|
if type(output) ~= "string" or output == ""
|
||||||
or output:match("^[%w_.-]+$") == nil then
|
or output:match("^[%w_.-]+$") == nil then
|
||||||
@@ -85,9 +108,23 @@ local function display_entry(output)
|
|||||||
or not valid_transform(entry.transform) then
|
or not valid_transform(entry.transform) then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
-- Legacy records have none of the layout fields and keep automatic
|
||||||
|
-- placement. A partially written extended record is unsafe: accepting its
|
||||||
|
-- mode but guessing its position could overlap or strand another output.
|
||||||
|
if has_layout_fields(entry)
|
||||||
|
and (not valid_position(entry) or not valid_primary(entry)) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
return entry
|
return entry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function display_position(entry, fallback)
|
||||||
|
if entry ~= nil and has_layout_fields(entry) then
|
||||||
|
return string.format("%dx%d", entry.x, entry.y)
|
||||||
|
end
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
|
||||||
local shipped_mode = "4500x3000@60"
|
local shipped_mode = "4500x3000@60"
|
||||||
local shipped_scale = 1.5
|
local shipped_scale = 1.5
|
||||||
local shipped_transform = 0
|
local shipped_transform = 0
|
||||||
@@ -96,7 +133,7 @@ local dp2 = display_entry("DP-2")
|
|||||||
hl.monitor({
|
hl.monitor({
|
||||||
output = "DP-2",
|
output = "DP-2",
|
||||||
mode = dp2 and dp2.mode or shipped_mode,
|
mode = dp2 and dp2.mode or shipped_mode,
|
||||||
position = "0x0",
|
position = display_position(dp2, "0x0"),
|
||||||
scale = dp2 and dp2.scale or shipped_scale,
|
scale = dp2 and dp2.scale or shipped_scale,
|
||||||
transform = dp2 and dp2.transform or shipped_transform,
|
transform = dp2 and dp2.transform or shipped_transform,
|
||||||
|
|
||||||
@@ -119,7 +156,7 @@ for output, _ in pairs(displays) do
|
|||||||
hl.monitor({
|
hl.monitor({
|
||||||
output = output,
|
output = output,
|
||||||
mode = entry.mode,
|
mode = entry.mode,
|
||||||
position = "auto",
|
position = display_position(entry, "auto"),
|
||||||
scale = entry.scale,
|
scale = entry.scale,
|
||||||
transform = entry.transform,
|
transform = entry.transform,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ font_size 14.0
|
|||||||
|
|
||||||
#: Cursor customization {{{
|
#: Cursor customization {{{
|
||||||
|
|
||||||
cursor #c8d3f5
|
|
||||||
|
|
||||||
#: Default cursor color. If set to the special value none the cursor
|
#: Default cursor color. If set to the special value none the cursor
|
||||||
#: will be rendered with a "reverse video" effect. It's color will be
|
#: will be rendered with a "reverse video" effect. It's color will be
|
||||||
@@ -193,7 +192,6 @@ cursor #c8d3f5
|
|||||||
#: precedence. Also, the cursor colors are modified if the cell
|
#: precedence. Also, the cursor colors are modified if the cell
|
||||||
#: background and foreground colors have very low contrast.
|
#: background and foreground colors have very low contrast.
|
||||||
|
|
||||||
cursor_text_color #111325
|
|
||||||
|
|
||||||
#: The color of text under the cursor. If you want it rendered with
|
#: The color of text under the cursor. If you want it rendered with
|
||||||
#: the background color of the cell underneath instead, use the
|
#: the background color of the cell underneath instead, use the
|
||||||
@@ -312,7 +310,6 @@ cursor_shape block
|
|||||||
#: robustly with the ever-changing sea of bugs that is Cocoa is too
|
#: robustly with the ever-changing sea of bugs that is Cocoa is too
|
||||||
#: much effort.
|
#: much effort.
|
||||||
|
|
||||||
url_color #4fd6be
|
|
||||||
# url_style curly
|
# url_style curly
|
||||||
|
|
||||||
#: The color and style for highlighting URLs on mouse-over. url_style
|
#: The color and style for highlighting URLs on mouse-over. url_style
|
||||||
@@ -734,12 +731,10 @@ window_border_width 1pt
|
|||||||
#: placed centrally. A value of top-left means the padding will be
|
#: placed centrally. A value of top-left means the padding will be
|
||||||
#: only at the bottom and right edges.
|
#: only at the bottom and right edges.
|
||||||
|
|
||||||
active_border_color #82aaff
|
|
||||||
|
|
||||||
#: The color for the border of the active window. Set this to none to
|
#: The color for the border of the active window. Set this to none to
|
||||||
#: not draw borders around the active window.
|
#: not draw borders around the active window.
|
||||||
|
|
||||||
inactive_border_color #2f334d
|
|
||||||
|
|
||||||
#: The color for the border of inactive windows.
|
#: The color for the border of inactive windows.
|
||||||
|
|
||||||
@@ -960,16 +955,11 @@ tab_powerline_style round
|
|||||||
#: Template to use for active tabs. If not specified falls back to
|
#: Template to use for active tabs. If not specified falls back to
|
||||||
#: tab_title_template.
|
#: tab_title_template.
|
||||||
|
|
||||||
active_tab_foreground #1e2030
|
|
||||||
active_tab_background #82aaff
|
|
||||||
# active_tab_font_style bold-italic
|
# active_tab_font_style bold-italic
|
||||||
inactive_tab_foreground #c8d3f5
|
|
||||||
inactive_tab_background #42465a
|
|
||||||
# inactive_tab_font_style normal
|
# inactive_tab_font_style normal
|
||||||
|
|
||||||
#: Tab bar colors and styles.
|
#: Tab bar colors and styles.
|
||||||
|
|
||||||
tab_bar_background #222436
|
|
||||||
|
|
||||||
#: Background color for the tab bar. Defaults to using the terminal
|
#: Background color for the tab bar. Defaults to using the terminal
|
||||||
#: background color.
|
#: background color.
|
||||||
@@ -983,8 +973,8 @@ tab_bar_margin_color none
|
|||||||
|
|
||||||
#: Color scheme {{{
|
#: Color scheme {{{
|
||||||
|
|
||||||
foreground #c8d3f5
|
|
||||||
background #222436
|
include current-theme.conf
|
||||||
|
|
||||||
#: The foreground and background colors.
|
#: The foreground and background colors.
|
||||||
|
|
||||||
@@ -1043,8 +1033,6 @@ dim_opacity 1
|
|||||||
#: How much to dim text that has the DIM/FAINT attribute set. One
|
#: How much to dim text that has the DIM/FAINT attribute set. One
|
||||||
#: means no dimming and zero means fully dimmed (i.e. invisible).
|
#: means no dimming and zero means fully dimmed (i.e. invisible).
|
||||||
|
|
||||||
selection_foreground #2d3f76
|
|
||||||
selection_background #c8d3f5
|
|
||||||
|
|
||||||
#: The foreground and background colors for text selected with the
|
#: The foreground and background colors for text selected with the
|
||||||
#: mouse. Setting both of these to none will cause a "reverse video"
|
#: mouse. Setting both of these to none will cause a "reverse video"
|
||||||
@@ -1060,47 +1048,29 @@ selection_background #c8d3f5
|
|||||||
#: dull and bright version, for the first 16 colors. You can set the
|
#: dull and bright version, for the first 16 colors. You can set the
|
||||||
#: remaining 240 colors as color16 to color255.
|
#: remaining 240 colors as color16 to color255.
|
||||||
|
|
||||||
color0 #1b1d2b
|
|
||||||
color8 #444a73
|
|
||||||
|
|
||||||
#: black
|
#: black
|
||||||
|
|
||||||
color1 #ff757f
|
|
||||||
color9 #ff757f
|
|
||||||
|
|
||||||
#: red
|
#: red
|
||||||
|
|
||||||
color2 #a5e8b5
|
|
||||||
color10 #a5e8b5
|
|
||||||
|
|
||||||
#: green
|
#: green
|
||||||
color3 #ffc777
|
|
||||||
color11 #ffc777
|
|
||||||
|
|
||||||
#: yellow
|
#: yellow
|
||||||
|
|
||||||
color4 #82aaff
|
|
||||||
color12 #82aaff
|
|
||||||
|
|
||||||
#: blue
|
#: blue
|
||||||
|
|
||||||
color5 #c099ff
|
|
||||||
color13 #c099ff
|
|
||||||
|
|
||||||
#: magenta
|
#: magenta
|
||||||
|
|
||||||
color6 #86e1fc
|
|
||||||
color14 #86e1fc
|
|
||||||
|
|
||||||
#: cyan
|
#: cyan
|
||||||
|
|
||||||
color7 #828bb8
|
|
||||||
color15 #c8d3f5
|
|
||||||
|
|
||||||
#: white
|
#: white
|
||||||
|
|
||||||
color16 #ff966c
|
|
||||||
color17 #c53b53
|
|
||||||
|
|
||||||
# mark1_foreground black
|
# mark1_foreground black
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Tokyo Night Day — the palette's own light variant.
|
||||||
|
#
|
||||||
|
# The same hues as Moon at a different lightness, so a terminal in light mode
|
||||||
|
# still belongs to this desktop rather than looking like a different machine.
|
||||||
|
# Kept in the same order as tokyonight-moon.conf so the two can be diffed.
|
||||||
|
|
||||||
|
cursor #3760bf
|
||||||
|
cursor_text_color #e1e2e7
|
||||||
|
url_color #118c74
|
||||||
|
active_border_color #2e7de9
|
||||||
|
inactive_border_color #c4c8da
|
||||||
|
active_tab_foreground #e1e2e7
|
||||||
|
active_tab_background #2e7de9
|
||||||
|
inactive_tab_foreground #3760bf
|
||||||
|
inactive_tab_background #c4c8da
|
||||||
|
tab_bar_background #e1e2e7
|
||||||
|
foreground #3760bf
|
||||||
|
background #e1e2e7
|
||||||
|
selection_foreground #e1e2e7
|
||||||
|
selection_background #99a7df
|
||||||
|
color0 #e9e9ed
|
||||||
|
color8 #a1a6c5
|
||||||
|
color1 #f52a65
|
||||||
|
color9 #f52a65
|
||||||
|
color2 #587539
|
||||||
|
color10 #587539
|
||||||
|
color3 #8c6c3e
|
||||||
|
color11 #8c6c3e
|
||||||
|
color4 #2e7de9
|
||||||
|
color12 #2e7de9
|
||||||
|
color5 #9854f1
|
||||||
|
color13 #9854f1
|
||||||
|
color6 #007197
|
||||||
|
color14 #007197
|
||||||
|
color7 #6172b0
|
||||||
|
color15 #3760bf
|
||||||
|
color16 #b15c00
|
||||||
|
color17 #c64343
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Tokyo Night Moon — the dark theme Panama ships.
|
||||||
|
#
|
||||||
|
# Extracted from kitty.conf so the two schemes can be swapped. kitty.conf
|
||||||
|
# includes current-theme.conf, which Panama generates from the colour scheme
|
||||||
|
# setting; that generated file is gitignored because it is machine state.
|
||||||
|
#
|
||||||
|
# Live changes go through `kitty @ set-colors`, which is why kitty.conf enables
|
||||||
|
# remote control. Without it a scheme change would only reach terminals opened
|
||||||
|
# afterwards.
|
||||||
|
|
||||||
|
cursor #c8d3f5
|
||||||
|
cursor_text_color #111325
|
||||||
|
url_color #4fd6be
|
||||||
|
active_border_color #82aaff
|
||||||
|
inactive_border_color #2f334d
|
||||||
|
active_tab_foreground #1e2030
|
||||||
|
active_tab_background #82aaff
|
||||||
|
inactive_tab_foreground #c8d3f5
|
||||||
|
inactive_tab_background #42465a
|
||||||
|
tab_bar_background #222436
|
||||||
|
foreground #c8d3f5
|
||||||
|
background #222436
|
||||||
|
selection_foreground #2d3f76
|
||||||
|
selection_background #c8d3f5
|
||||||
|
color0 #1b1d2b
|
||||||
|
color8 #444a73
|
||||||
|
color1 #ff757f
|
||||||
|
color9 #ff757f
|
||||||
|
color2 #a5e8b5
|
||||||
|
color10 #a5e8b5
|
||||||
|
color3 #ffc777
|
||||||
|
color11 #ffc777
|
||||||
|
color4 #82aaff
|
||||||
|
color12 #82aaff
|
||||||
|
color5 #c099ff
|
||||||
|
color13 #c099ff
|
||||||
|
color6 #86e1fc
|
||||||
|
color14 #86e1fc
|
||||||
|
color7 #828bb8
|
||||||
|
color15 #c8d3f5
|
||||||
|
color16 #ff966c
|
||||||
|
color17 #c53b53
|
||||||
@@ -10,3 +10,6 @@ vim.api.nvim_create_autocmd("BufWritePre", {
|
|||||||
vim.fn.winrestview(view)
|
vim.fn.winrestview(view)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
-- Follow the desktop's light/dark setting while running, not only at startup.
|
||||||
|
require("config.panama").watch()
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- Panama desktop integration.
|
||||||
|
--
|
||||||
|
-- Neovim is the one application here that neither reads the desktop portal nor
|
||||||
|
-- has a control socket open by default, so it reads the shared settings file
|
||||||
|
-- directly -- the same ~/.config/panama/settings.json that the shell and the
|
||||||
|
-- Hyprland config read.
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
-- important than matching the desktop.
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local function settings_path()
|
||||||
|
local config_home = os.getenv("XDG_CONFIG_HOME")
|
||||||
|
if config_home == nil or config_home == "" then
|
||||||
|
local home = os.getenv("HOME")
|
||||||
|
if home == nil or home == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
config_home = home .. "/.config"
|
||||||
|
end
|
||||||
|
return config_home .. "/panama/settings.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- "dark" or "light". Defaults to dark, which is what Panama ships.
|
||||||
|
function M.color_scheme()
|
||||||
|
local path = settings_path()
|
||||||
|
if not path then
|
||||||
|
return "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, contents = pcall(function()
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local text = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
return text
|
||||||
|
end)
|
||||||
|
if not ok or not contents or contents == "" then
|
||||||
|
return "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
local decoded_ok, decoded = pcall(vim.json.decode, contents)
|
||||||
|
if not decoded_ok or type(decoded) ~= "table" then
|
||||||
|
return "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
return decoded.colorScheme == "light" and "light" or "dark"
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.is_light()
|
||||||
|
return M.color_scheme() == "light"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- tokyonight ships a light variant in the same family, so light mode stays the
|
||||||
|
-- same identity rather than becoming a different editor theme.
|
||||||
|
function M.tokyonight_style()
|
||||||
|
return M.is_light() and "day" or "moon"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Re-apply the scheme when the window regains focus.
|
||||||
|
--
|
||||||
|
-- Neovim reads the setting once at startup and has no control socket open by
|
||||||
|
-- default, so an editor already running when the desktop scheme flips would
|
||||||
|
-- otherwise stay on the old palette until it was restarted. FocusGained is
|
||||||
|
-- cheap, happens exactly when you would notice the mismatch, and does nothing
|
||||||
|
-- at all unless the scheme actually changed.
|
||||||
|
function M.watch()
|
||||||
|
local applied = M.color_scheme()
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("FocusGained", {
|
||||||
|
group = vim.api.nvim_create_augroup("PanamaColorScheme", { clear = true }),
|
||||||
|
callback = function()
|
||||||
|
local current = M.color_scheme()
|
||||||
|
if current == applied then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
applied = current
|
||||||
|
vim.o.background = current
|
||||||
|
pcall(vim.cmd.colorscheme, current == "light" and "tokyonight-day" or "tokyonight-moon")
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -1,31 +1,68 @@
|
|||||||
|
-- Tokyo Night, following the desktop's colour scheme.
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
-- becoming a different-looking application when the scheme flips.
|
||||||
|
--
|
||||||
|
-- The readability fixes below are deliberately dark-only. They were written
|
||||||
|
-- against Moon's palette -- a pale comment colour, a mid-grey gutter -- and
|
||||||
|
-- applying them to Day would put light grey text on a light background, which
|
||||||
|
-- is exactly the legibility problem they exist to solve, inverted.
|
||||||
|
|
||||||
|
local panama = require("config.panama")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
{
|
{
|
||||||
"folke/tokyonight.nvim",
|
"folke/tokyonight.nvim",
|
||||||
opts = {
|
opts = function()
|
||||||
style = "moon",
|
local light = panama.is_light()
|
||||||
transparent = true,
|
|
||||||
on_colors = function(colors)
|
return {
|
||||||
colors.comment = "#a0a7c5"
|
style = panama.tokyonight_style(),
|
||||||
colors.fg_gutter = "#787f93"
|
light_style = "day",
|
||||||
colors.terminal_black = "#828bb8"
|
transparent = true,
|
||||||
end,
|
|
||||||
on_highlights = function(highlights, colors)
|
on_colors = function(colors)
|
||||||
-- Fix inline code visibility in markdown
|
if light then
|
||||||
highlights["@markup.raw.markdown_inline"] = {
|
-- Day's defaults are tuned for a light ground and mostly need no
|
||||||
bg = colors.terminal_black,
|
-- help. Comments are the exception: the shipped #848cb5 measures
|
||||||
fg = colors.fg,
|
-- 2.54:1 against the #e1e2e7 background, well under the 3:1 floor
|
||||||
}
|
-- for secondary text. This is 3.25:1 -- readable, and still clearly
|
||||||
highlights["RenderMarkdownCodeInline"] = {
|
-- dimmer than Normal's 4.52:1 so it does not compete with code.
|
||||||
bg = colors.terminal_black,
|
colors.comment = "#7079a8"
|
||||||
fg = colors.fg,
|
return
|
||||||
}
|
end
|
||||||
-- Fix LspReference* readability: DiagnosticUnnecessary dims fg for unused
|
|
||||||
-- imports, making text nearly invisible against LspReferenceText's background
|
colors.comment = "#a0a7c5"
|
||||||
highlights["LspReferenceText"] = { bg = colors.fg_gutter, fg = colors.fg }
|
colors.fg_gutter = "#787f93"
|
||||||
highlights["LspReferenceRead"] = { bg = colors.fg_gutter, fg = colors.fg }
|
colors.terminal_black = "#828bb8"
|
||||||
highlights["LspReferenceWrite"] = { bg = colors.fg_gutter, fg = colors.fg }
|
end,
|
||||||
end,
|
|
||||||
},
|
on_highlights = function(highlights, colors)
|
||||||
|
-- Inline code in markdown is invisible at both lightnesses without
|
||||||
|
-- an explicit background, because the theme leaves it unset.
|
||||||
|
highlights["@markup.raw.markdown_inline"] = {
|
||||||
|
bg = light and colors.bg_highlight or colors.terminal_black,
|
||||||
|
fg = colors.fg,
|
||||||
|
}
|
||||||
|
highlights["RenderMarkdownCodeInline"] = {
|
||||||
|
bg = light and colors.bg_highlight or colors.terminal_black,
|
||||||
|
fg = colors.fg,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- LspReference* readability: DiagnosticUnnecessary dims fg for unused
|
||||||
|
-- imports, making text nearly invisible against LspReferenceText's
|
||||||
|
-- background.
|
||||||
|
local reference = {
|
||||||
|
bg = light and colors.bg_visual or colors.fg_gutter,
|
||||||
|
fg = colors.fg,
|
||||||
|
}
|
||||||
|
highlights["LspReferenceText"] = reference
|
||||||
|
highlights["LspReferenceRead"] = reference
|
||||||
|
highlights["LspReferenceWrite"] = reference
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"LazyVim/LazyVim",
|
"LazyVim/LazyVim",
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import "services/AudioStreams.js" as AudioStreams
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
readonly property int audioOutStreamFlag: 4
|
||||||
|
|
||||||
|
property var fixtureNodes: [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
ready: true,
|
||||||
|
type: audioOutStreamFlag,
|
||||||
|
properties: {
|
||||||
|
"application.id": "org.chromium.Chromium",
|
||||||
|
"application.name": "Chromium",
|
||||||
|
"application.icon_name": "chromium"
|
||||||
|
},
|
||||||
|
description: "Chromium audio",
|
||||||
|
audio: { volume: 0.4, muted: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
ready: true,
|
||||||
|
type: audioOutStreamFlag,
|
||||||
|
properties: {
|
||||||
|
"application.id": "org.chromium.Chromium",
|
||||||
|
"application.name": "Chromium",
|
||||||
|
"application.icon_name": "chromium"
|
||||||
|
},
|
||||||
|
description: "Chromium audio",
|
||||||
|
audio: { volume: 0.8, muted: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
ready: true,
|
||||||
|
type: audioOutStreamFlag,
|
||||||
|
properties: {
|
||||||
|
"application.process.binary": "spotify",
|
||||||
|
"application.name": "Spotify"
|
||||||
|
},
|
||||||
|
description: "Spotify",
|
||||||
|
audio: { volume: 0.25, muted: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30,
|
||||||
|
ready: true,
|
||||||
|
type: 2,
|
||||||
|
properties: { "application.name": "Microphone capture" },
|
||||||
|
description: "Input stream",
|
||||||
|
audio: { volume: 0.5, muted: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 40,
|
||||||
|
ready: false,
|
||||||
|
type: audioOutStreamFlag,
|
||||||
|
properties: { "application.name": "Not ready" },
|
||||||
|
description: "Unready stream",
|
||||||
|
audio: { volume: 0.5, muted: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 99,
|
||||||
|
ready: true,
|
||||||
|
type: audioOutStreamFlag,
|
||||||
|
properties: {},
|
||||||
|
description: "",
|
||||||
|
audio: { volume: 1, muted: false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function groups(): var {
|
||||||
|
return AudioStreams.group(fixtureNodes, audioOutStreamFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "application-volume-test"
|
||||||
|
|
||||||
|
function summary(): string {
|
||||||
|
const applications = groups();
|
||||||
|
const chromium = applications.find(application =>
|
||||||
|
application.key === "org.chromium.Chromium");
|
||||||
|
return JSON.stringify({
|
||||||
|
groups: applications.map(application => ({
|
||||||
|
key: application.key,
|
||||||
|
label: application.label,
|
||||||
|
icon: application.icon,
|
||||||
|
count: application.nodes.length
|
||||||
|
})),
|
||||||
|
chromiumVolume: AudioStreams.volume(chromium),
|
||||||
|
chromiumMuted: AudioStreams.muted(chromium)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutateVolume(): string {
|
||||||
|
const chromium = groups().find(application =>
|
||||||
|
application.key === "org.chromium.Chromium");
|
||||||
|
const changed = AudioStreams.setVolume(chromium, 0.7);
|
||||||
|
return JSON.stringify({
|
||||||
|
changed,
|
||||||
|
volumes: chromium.nodes.map(node => node.audio.volume),
|
||||||
|
muted: chromium.nodes.map(node => node.audio.muted)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutateMute(): string {
|
||||||
|
const chromium = groups().find(application =>
|
||||||
|
application.key === "org.chromium.Chromium");
|
||||||
|
const changed = AudioStreams.setMuted(chromium, true);
|
||||||
|
return JSON.stringify({
|
||||||
|
changed,
|
||||||
|
muted: chromium.nodes.map(node => node.audio.muted)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceSummary(): string {
|
||||||
|
const applications = AudioDevices.applications;
|
||||||
|
return JSON.stringify({
|
||||||
|
count: applications.length,
|
||||||
|
validTypes: applications.every(application =>
|
||||||
|
application.nodes.every(node =>
|
||||||
|
(node.type & PwNodeType.AudioOutStream)
|
||||||
|
=== PwNodeType.AudioOutStream))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidMutations(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
nullVolume: AudioDevices.setApplicationVolume(null, 0.5),
|
||||||
|
emptyMute: AudioDevices.setApplicationMuted({ nodes: [] }, true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,26 @@ Singleton {
|
|||||||
// back to shipped defaults and let the next write replace it.
|
// back to shipped defaults and let the next write replace it.
|
||||||
parsed = {};
|
parsed = {};
|
||||||
}
|
}
|
||||||
root.values = (parsed && typeof parsed === "object") ? parsed : {};
|
const raw = (parsed && typeof parsed === "object") ? parsed : {};
|
||||||
|
|
||||||
|
// Upgrade before anything reads a value. A stored key the current
|
||||||
|
// schema no longer recognises is carried through untouched and silently
|
||||||
|
// stops taking effect, so the conversion has to happen here rather than
|
||||||
|
// being noticed later by whoever owns that setting.
|
||||||
|
const result = Migrations.apply(raw);
|
||||||
|
root.values = result.values;
|
||||||
|
|
||||||
|
if (result.migrated)
|
||||||
|
console.info("Settings migrated from version", result.from, "to", result.to + ":",
|
||||||
|
result.applied.join("; "));
|
||||||
|
|
||||||
|
// Write whenever the version moved, which includes stamping a file
|
||||||
|
// written before versioning existed. Left unwritten, the stamp lives
|
||||||
|
// only in memory and is redone on every launch, and a migration that is
|
||||||
|
// not idempotent would compound.
|
||||||
|
if (result.changed)
|
||||||
|
persistTimer.restart();
|
||||||
|
|
||||||
root.revision++;
|
root.revision++;
|
||||||
root.loaded = true;
|
root.loaded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
// Versioned upgrades for the settings file.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
// untouched, so the user's choice simply stops taking effect with nothing to
|
||||||
|
// say why. That is the failure this exists to prevent.
|
||||||
|
//
|
||||||
|
// HOW IT WORKS
|
||||||
|
//
|
||||||
|
// settings.json carries a schemaVersion. On load, every migration with a
|
||||||
|
// version ABOVE the stored one runs in order, then the file is stamped with
|
||||||
|
// `current`. A file with no schemaVersion at all is a file written before this
|
||||||
|
// existed; it is stamped at `baseline` and NOT migrated, because those
|
||||||
|
// migrations were never written for it.
|
||||||
|
//
|
||||||
|
// WRITING ONE
|
||||||
|
//
|
||||||
|
// { version: 2, describe: "rename dockDelay to dockHideDelayMs",
|
||||||
|
// migrate: values => { ... return values; } }
|
||||||
|
//
|
||||||
|
// Rules that make this safe to run against a real user's file:
|
||||||
|
//
|
||||||
|
// * migrate() receives the whole values object and returns it. Mutating and
|
||||||
|
// returning the same object is fine.
|
||||||
|
// * NEVER delete a key you are not replacing. Unknown keys are deliberately
|
||||||
|
// preserved so that rolling back to an older Panama does not discard a
|
||||||
|
// newer version's settings, and a migration is the one place that promise
|
||||||
|
// could quietly be broken.
|
||||||
|
// * A migration must tolerate its input being absent or the wrong type. It
|
||||||
|
// runs against files written by every previous version, including ones
|
||||||
|
// that were hand-edited.
|
||||||
|
// * Migrations never run twice: the stored version only moves forward.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// What a file written today is stamped with. Bump this when adding a
|
||||||
|
// migration, to the version of the migration you added.
|
||||||
|
readonly property int current: 1
|
||||||
|
|
||||||
|
// Files predating versioning are stamped here without being migrated.
|
||||||
|
readonly property int baseline: 1
|
||||||
|
|
||||||
|
readonly property string versionKey: "schemaVersion"
|
||||||
|
|
||||||
|
// Ordered by version. Empty is the correct state until the first breaking
|
||||||
|
// schema change -- this exists so that change is a routine edit rather than
|
||||||
|
// an emergency.
|
||||||
|
readonly property var steps: []
|
||||||
|
|
||||||
|
function storedVersion(values: var): int {
|
||||||
|
const raw = values ? values[root.versionKey] : undefined;
|
||||||
|
return (typeof raw === "number" && isFinite(raw)) ? Math.floor(raw) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns { values, migrated, changed, from, to, applied }.
|
||||||
|
//
|
||||||
|
// `applied` names each step that ran, so the caller can log something
|
||||||
|
// meaningful rather than "settings changed somehow". `changed` is the one
|
||||||
|
// the caller should write on: stamping a pre-versioning file changes it
|
||||||
|
// without running any step, and left unwritten the stamp would live only in
|
||||||
|
// memory and be redone on every launch.
|
||||||
|
function apply(values: var): var {
|
||||||
|
return root.applyWith(values, root.steps, root.current, root.baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same logic with the step list injected, so the machinery can be
|
||||||
|
// tested against fixture migrations. The real list is empty until the first
|
||||||
|
// breaking schema change, and a mechanism that has never run against a
|
||||||
|
// failing step is not one to find out about during an upgrade.
|
||||||
|
function applyWith(values: var, steps: var, current: int, baseline: int): var {
|
||||||
|
const safe = (values && typeof values === "object") ? values : {};
|
||||||
|
const from = root.storedVersion(safe);
|
||||||
|
|
||||||
|
// No version: written before versioning existed. Stamp it and stop.
|
||||||
|
// Running the migration list against it would apply upgrades designed
|
||||||
|
// for schemas this file never had.
|
||||||
|
if (from === 0) {
|
||||||
|
safe[root.versionKey] = baseline;
|
||||||
|
return { values: safe, migrated: false, changed: true, from: 0, to: baseline, applied: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// A file from a NEWER Panama. Left completely alone: downgrading its
|
||||||
|
// keys is not something this can do correctly, and unknown keys are
|
||||||
|
// already preserved, so the older build simply ignores what it does not
|
||||||
|
// understand.
|
||||||
|
if (from > current)
|
||||||
|
return { values: safe, migrated: false, changed: false, from: from, to: from, applied: [] };
|
||||||
|
|
||||||
|
const applied = [];
|
||||||
|
let working = safe;
|
||||||
|
for (const step of steps) {
|
||||||
|
if (step.version <= from || step.version > current)
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const result = step.migrate(working);
|
||||||
|
if (result && typeof result === "object")
|
||||||
|
working = result;
|
||||||
|
applied.push(step.version + ": " + step.describe);
|
||||||
|
} catch (error) {
|
||||||
|
// One bad migration must not cost the user every setting. Stop
|
||||||
|
// at the last good version so the next launch retries from
|
||||||
|
// here rather than skipping the failed step forever.
|
||||||
|
console.warn("Migrations: step", step.version, "failed:", error);
|
||||||
|
working[root.versionKey] = step.version - 1;
|
||||||
|
return { values: working, migrated: applied.length > 0, changed: applied.length > 0,
|
||||||
|
from: from, to: step.version - 1, applied: applied };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
working[root.versionKey] = current;
|
||||||
|
return { values: working, migrated: applied.length > 0, changed: from !== current,
|
||||||
|
from: from, to: current, applied: applied };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,10 +128,21 @@ Singleton {
|
|||||||
{
|
{
|
||||||
key: "vrrPolicy", type: "enum", def: 3, group: "display",
|
key: "vrrPolicy", type: "enum", def: 3, group: "display",
|
||||||
label: "Variable refresh rate",
|
label: "Variable refresh rate",
|
||||||
detail: "Content-aware matches the display to what is on screen",
|
detail: "Matches the display's refresh rate to what is on screen",
|
||||||
|
// All four the compositor publishes, rather than the two that were
|
||||||
|
// here. Always-on VRR is a legitimate choice on a panel that
|
||||||
|
// handles it well, and it was simply unreachable -- as was
|
||||||
|
// fullscreen-only, which is what someone wanting VRR for video
|
||||||
|
// rather than games wants.
|
||||||
options: [
|
options: [
|
||||||
{ value: 0, label: "Off" },
|
{ value: 0, label: "Off",
|
||||||
{ value: 3, label: "Content-aware" }
|
detail: "The display runs at a fixed refresh rate" },
|
||||||
|
{ value: 1, label: "Always on",
|
||||||
|
detail: "Best on panels that handle low refresh rates without flicker" },
|
||||||
|
{ value: 2, label: "Fullscreen only",
|
||||||
|
detail: "Any fullscreen window, including video" },
|
||||||
|
{ value: 3, label: "Fullscreen games",
|
||||||
|
detail: "Only fullscreen games, which is the safest default" }
|
||||||
],
|
],
|
||||||
hypr: { path: ["misc", "vrr"], option: "misc:vrr", readAs: "int" }
|
hypr: { path: ["misc", "vrr"], option: "misc:vrr", readAs: "int" }
|
||||||
},
|
},
|
||||||
@@ -140,8 +151,12 @@ Singleton {
|
|||||||
label: "Direct scanout",
|
label: "Direct scanout",
|
||||||
detail: "Lets fullscreen content bypass compositing",
|
detail: "Lets fullscreen content bypass compositing",
|
||||||
options: [
|
options: [
|
||||||
{ value: 0, label: "Off" },
|
{ value: 0, label: "Off",
|
||||||
{ value: 2, label: "Automatic" }
|
detail: "Everything goes through the compositor" },
|
||||||
|
{ value: 1, label: "Always on",
|
||||||
|
detail: "Forced rather than decided per surface; can drop frames on some drivers" },
|
||||||
|
{ value: 2, label: "Automatic",
|
||||||
|
detail: "The compositor decides per surface, which is the safe default" }
|
||||||
],
|
],
|
||||||
hypr: { path: ["render", "direct_scanout"], option: "render:direct_scanout", readAs: "int" }
|
hypr: { path: ["render", "direct_scanout"], option: "render:direct_scanout", readAs: "int" }
|
||||||
},
|
},
|
||||||
@@ -191,6 +206,147 @@ Singleton {
|
|||||||
hypr: { path: ["decoration", "inactive_opacity"], option: "decoration:inactive_opacity", readAs: "float" }
|
hypr: { path: ["decoration", "inactive_opacity"], option: "decoration:inactive_opacity", readAs: "float" }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: "activeOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05,
|
||||||
|
group: "windows",
|
||||||
|
label: "Focused window opacity",
|
||||||
|
detail: "Fade even the focused window; 1.0 is fully opaque",
|
||||||
|
hypr: { path: ["decoration", "active_opacity"], option: "decoration:active_opacity", readAs: "float" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "fullscreenOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05,
|
||||||
|
group: "windows",
|
||||||
|
label: "Fullscreen opacity",
|
||||||
|
detail: "Applied instead of the focused opacity when a window is fullscreen",
|
||||||
|
hypr: { path: ["decoration", "fullscreen_opacity"], option: "decoration:fullscreen_opacity", readAs: "float" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "roundingPower", type: "real", def: 2.0, min: 2.0, max: 10.0, step: 0.5,
|
||||||
|
group: "windows",
|
||||||
|
label: "Corner shape",
|
||||||
|
detail: "2 is a circular corner; higher values approach a squircle",
|
||||||
|
hypr: { path: ["decoration", "rounding_power"], option: "decoration:rounding_power", readAs: "float" }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Window edges ────────────────────────────────────────────────────
|
||||||
|
// How the pointer interacts with a window's border, and how windows
|
||||||
|
// behave near each other. All shipped by looks.lua with no way to
|
||||||
|
// change any of it.
|
||||||
|
{
|
||||||
|
key: "resizeOnBorder", type: "bool", def: true, group: "edges",
|
||||||
|
label: "Resize by dragging the border",
|
||||||
|
detail: "Drag a window's edge to resize it, instead of only with the keyboard",
|
||||||
|
hypr: { path: ["general", "resize_on_border"], option: "general:resize_on_border", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "borderGrabArea", type: "int", def: 15, min: 0, max: 40, step: 1,
|
||||||
|
unit: "px",
|
||||||
|
group: "edges",
|
||||||
|
label: "Border grab area",
|
||||||
|
detail: "How far outside the border still counts as grabbing it. Larger is easier to hit",
|
||||||
|
hypr: { path: ["general", "extend_border_grab_area"], option: "general:extend_border_grab_area", readAs: "int" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hoverIconOnBorder", type: "bool", def: true, group: "edges",
|
||||||
|
label: "Show the resize cursor",
|
||||||
|
detail: "Change the pointer when it is over a resizable border",
|
||||||
|
hypr: { path: ["general", "hover_icon_on_border"], option: "general:hover_icon_on_border", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "snapWindowGap", type: "int", def: 10, min: 0, max: 60, step: 1,
|
||||||
|
unit: "px",
|
||||||
|
group: "edges",
|
||||||
|
label: "Snap distance between windows",
|
||||||
|
detail: "How close two floating windows must be before they snap together",
|
||||||
|
hypr: { path: ["general", "snap", "window_gap"], option: "general:snap:window_gap", readAs: "int" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "snapMonitorGap", type: "int", def: 10, min: 0, max: 60, step: 1,
|
||||||
|
unit: "px",
|
||||||
|
group: "edges",
|
||||||
|
label: "Snap distance to screen edges",
|
||||||
|
detail: "How close a floating window must be to an edge before it snaps to it",
|
||||||
|
hypr: { path: ["general", "snap", "monitor_gap"], option: "general:snap:monitor_gap", readAs: "int" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "snapRespectGaps", type: "bool", def: false, group: "edges",
|
||||||
|
label: "Snapping respects gaps",
|
||||||
|
detail: "Snapped windows keep the configured gap instead of touching",
|
||||||
|
hypr: { path: ["general", "snap", "respect_gaps"], option: "general:snap:respect_gaps", readAs: "bool" }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Master layout ───────────────────────────────────────────────────
|
||||||
|
// 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
|
||||||
|
// layout whose whole behaviour is in these settings.
|
||||||
|
{
|
||||||
|
key: "masterFactor", type: "real", def: 0.55, min: 0.1, max: 0.9, step: 0.05,
|
||||||
|
group: "master",
|
||||||
|
label: "Master area size",
|
||||||
|
detail: "How much of the screen the master window takes",
|
||||||
|
hypr: { path: ["master", "mfact"], option: "master:mfact", readAs: "float" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "masterOrientation", type: "enum", def: "left", group: "master",
|
||||||
|
label: "Master area position",
|
||||||
|
detail: "Which side of the screen the master window occupies",
|
||||||
|
options: [
|
||||||
|
{ value: "left", label: "Left" },
|
||||||
|
{ value: "right", label: "Right" },
|
||||||
|
{ value: "top", label: "Top" },
|
||||||
|
{ value: "bottom", label: "Bottom" },
|
||||||
|
{ value: "center", label: "Centre" }
|
||||||
|
],
|
||||||
|
hypr: { path: ["master", "orientation"], option: "master:orientation", readAs: "str" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "masterNewStatus", type: "enum", def: "slave", group: "master",
|
||||||
|
label: "New windows become",
|
||||||
|
detail: "Whether a new window takes the master area or joins the stack",
|
||||||
|
options: [
|
||||||
|
{ value: "master", label: "The master window" },
|
||||||
|
{ value: "slave", label: "Part of the stack" },
|
||||||
|
{ value: "inherit", label: "Whatever the focused window is" }
|
||||||
|
],
|
||||||
|
hypr: { path: ["master", "new_status"], option: "master:new_status", readAs: "str" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "masterNewOnTop", type: "bool", def: false, group: "master",
|
||||||
|
label: "Add new windows at the top",
|
||||||
|
detail: "New stack windows go above the others rather than below",
|
||||||
|
hypr: { path: ["master", "new_on_top"], option: "master:new_on_top", readAs: "bool" }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Hyprland's own notices ──────────────────────────────────────────
|
||||||
|
// Panama turns all four off on the user's behalf. That is a defensible
|
||||||
|
// default and was not a decision anyone could reverse without editing
|
||||||
|
// looks.lua, which is precisely the kind of thing this app exists to
|
||||||
|
// stop.
|
||||||
|
{
|
||||||
|
key: "hyprlandLogo", type: "bool", def: false, group: "notices",
|
||||||
|
label: "Hyprland wallpaper",
|
||||||
|
detail: "The stock background Hyprland draws when no wallpaper is set",
|
||||||
|
hypr: { path: ["misc", "disable_hyprland_logo"], option: "misc:disable_hyprland_logo", readAs: "bool", invert: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hyprlandSplash", type: "bool", def: false, group: "notices",
|
||||||
|
label: "Splash text",
|
||||||
|
detail: "The line of text Hyprland renders over the stock background",
|
||||||
|
hypr: { path: ["misc", "disable_splash_rendering"], option: "misc:disable_splash_rendering", readAs: "bool", invert: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hyprlandUpdateNews", type: "bool", def: false, group: "notices",
|
||||||
|
label: "Update announcements",
|
||||||
|
detail: "The window Hyprland opens after an update to describe what changed",
|
||||||
|
hypr: { path: ["ecosystem", "no_update_news"], option: "ecosystem:no_update_news", readAs: "bool", invert: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "hyprlandDonationNag", type: "bool", def: false, group: "notices",
|
||||||
|
label: "Donation reminders",
|
||||||
|
detail: "The prompt Hyprland shows twice a year asking for support",
|
||||||
|
hypr: { path: ["ecosystem", "no_donation_nag"], option: "ecosystem:no_donation_nag", readAs: "bool", invert: true }
|
||||||
|
},
|
||||||
|
|
||||||
// ── Effects ─────────────────────────────────────────────────────────
|
// ── Effects ─────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
key: "blurEnabled", type: "bool", def: true, group: "effects",
|
key: "blurEnabled", type: "bool", def: true, group: "effects",
|
||||||
@@ -226,6 +382,19 @@ Singleton {
|
|||||||
detail: "How far the shadow spreads from the window edge",
|
detail: "How far the shadow spreads from the window edge",
|
||||||
hypr: { path: ["decoration", "shadow", "range"], option: "decoration:shadow:range", readAs: "int" }
|
hypr: { path: ["decoration", "shadow", "range"], option: "decoration:shadow:range", readAs: "int" }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "shadowSharp", type: "bool", def: false, group: "effects",
|
||||||
|
label: "Hard-edged shadow",
|
||||||
|
detail: "A crisp shadow instead of a soft falloff",
|
||||||
|
hypr: { path: ["decoration", "shadow", "sharp"], option: "decoration:shadow:sharp", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "shadowRenderPower", type: "int", def: 3, min: 1, max: 4, step: 1,
|
||||||
|
group: "effects",
|
||||||
|
label: "Shadow falloff",
|
||||||
|
detail: "How sharply the shadow fades out. Higher is tighter to the window",
|
||||||
|
hypr: { path: ["decoration", "shadow", "render_power"], option: "decoration:shadow:render_power", readAs: "int" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "glowEnabled", type: "bool", def: true, group: "effects",
|
key: "glowEnabled", type: "bool", def: true, group: "effects",
|
||||||
label: "Focus glow",
|
label: "Focus glow",
|
||||||
@@ -257,6 +426,24 @@ Singleton {
|
|||||||
detail: "XKB layout name, or a comma-separated list to switch between",
|
detail: "XKB layout name, or a comma-separated list to switch between",
|
||||||
hypr: { path: ["input", "kb_layout"], option: "input:kb_layout", readAs: "str" }
|
hypr: { path: ["input", "kb_layout"], option: "input:kb_layout", readAs: "str" }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "keyboardVariant", type: "string", def: "", group: "input",
|
||||||
|
// Same shape as the layout list and for the same reason: this is
|
||||||
|
// serialised into an hl.config string.
|
||||||
|
pattern: "^$|^[a-z0-9_]{1,24}(,[a-z0-9_]{1,24})*$",
|
||||||
|
label: "Layout variant",
|
||||||
|
detail: "XKB variant, such as dvorak or colemak. Empty for the standard layout",
|
||||||
|
hypr: { path: ["input", "kb_variant"], option: "input:kb_variant", readAs: "str" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "keyboardOptions", type: "string", def: "caps:escape_shifted_capslock", group: "input",
|
||||||
|
// XKB option names are colon-separated pairs in a comma-separated
|
||||||
|
// list, e.g. "compose:ralt,caps:escape".
|
||||||
|
pattern: "^$|^[a-z0-9_]+:[a-z0-9_]+(,[a-z0-9_]+:[a-z0-9_]+)*$",
|
||||||
|
label: "Keyboard options",
|
||||||
|
detail: "XKB options, such as compose:ralt to make right Alt a compose key",
|
||||||
|
hypr: { path: ["input", "kb_options"], option: "input:kb_options", readAs: "str" }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "numlockByDefault", type: "bool", def: true, group: "input",
|
key: "numlockByDefault", type: "bool", def: true, group: "input",
|
||||||
label: "Num Lock on login",
|
label: "Num Lock on login",
|
||||||
@@ -280,19 +467,35 @@ Singleton {
|
|||||||
hypr: { path: ["input", "repeat_rate"], option: "input:repeat_rate", readAs: "int" }
|
hypr: { path: ["input", "repeat_rate"], option: "input:repeat_rate", readAs: "int" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "followMouse", type: "enum", def: 1, group: "input",
|
key: "followMouse", type: "enum", def: 1, group: "pointer",
|
||||||
label: "Focus follows pointer",
|
label: "Pointer focus",
|
||||||
detail: "Click to focus matches GNOME; sloppy focus follows the pointer",
|
detail: "What moving the pointer does to which window is focused",
|
||||||
|
// These labels were wrong, and wrong in the worst way: value 1 was
|
||||||
|
// shown as "Click to focus" while Hyprland's 1 means the opposite.
|
||||||
|
// The compositor publishes the authoritative mapping itself --
|
||||||
|
// `hyprctl descriptions` gives
|
||||||
|
// map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}]
|
||||||
|
// -- so a desktop labelled "Click to focus" was in fact following
|
||||||
|
// the pointer, and the way to actually get click-to-focus was to
|
||||||
|
// choose "Never". Value 3 was missing entirely.
|
||||||
|
//
|
||||||
|
// enum-hypr-map-contract now pins every mapped enum against that
|
||||||
|
// published map, so this cannot drift again.
|
||||||
options: [
|
options: [
|
||||||
{ value: 0, label: "Never" },
|
{ value: 0, label: "Click to focus",
|
||||||
{ value: 1, label: "Click to focus" },
|
detail: "Moving the pointer never changes focus" },
|
||||||
{ value: 2, label: "Sloppy focus" }
|
{ value: 1, label: "Focus follows pointer",
|
||||||
|
detail: "The window under the pointer takes focus as you move" },
|
||||||
|
{ value: 2, label: "Pointer detached",
|
||||||
|
detail: "The pointer highlights windows on its own; clicking moves keyboard focus" },
|
||||||
|
{ value: 3, label: "Pointer fully separate",
|
||||||
|
detail: "Clicking does not move keyboard focus at all" }
|
||||||
],
|
],
|
||||||
hypr: { path: ["input", "follow_mouse"], option: "input:follow_mouse", readAs: "int" }
|
hypr: { path: ["input", "follow_mouse"], option: "input:follow_mouse", readAs: "int" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "pointerSensitivity", type: "real", def: 0.0, min: -1.0, max: 1.0, step: 0.05,
|
key: "pointerSensitivity", type: "real", def: 0.0, min: -1.0, max: 1.0, step: 0.05,
|
||||||
group: "input",
|
group: "pointer",
|
||||||
label: "Pointer speed",
|
label: "Pointer speed",
|
||||||
detail: "Zero is flat, unaccelerated response",
|
detail: "Zero is flat, unaccelerated response",
|
||||||
hypr: { path: ["input", "sensitivity"], option: "input:sensitivity", readAs: "float" }
|
hypr: { path: ["input", "sensitivity"], option: "input:sensitivity", readAs: "float" }
|
||||||
@@ -300,7 +503,7 @@ Singleton {
|
|||||||
{
|
{
|
||||||
key: "cursorInactiveTimeout", type: "int", def: 4, min: 0, max: 60, step: 1,
|
key: "cursorInactiveTimeout", type: "int", def: 4, min: 0, max: 60, step: 1,
|
||||||
unit: "s",
|
unit: "s",
|
||||||
group: "input",
|
group: "pointer",
|
||||||
label: "Hide pointer after",
|
label: "Hide pointer after",
|
||||||
detail: "Seconds of stillness before the pointer fades out; 0 never hides it",
|
detail: "Seconds of stillness before the pointer fades out; 0 never hides it",
|
||||||
// Reported as a float even though it is only ever set to whole
|
// Reported as a float even though it is only ever set to whole
|
||||||
@@ -310,6 +513,209 @@ Singleton {
|
|||||||
hypr: { path: ["cursor", "inactive_timeout"], option: "cursor:inactive_timeout", readAs: "float" }
|
hypr: { path: ["cursor", "inactive_timeout"], option: "cursor:inactive_timeout", readAs: "float" }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Pointer ─────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Every `readAs` below was read back off the running compositor rather
|
||||||
|
// than guessed. Two are not what they look like: touchpad drag lock is
|
||||||
|
// an int with three states, not a switch, and scroll factors are floats
|
||||||
|
// even at their default of exactly 1.
|
||||||
|
{
|
||||||
|
key: "naturalScroll", type: "bool", def: false, group: "pointer",
|
||||||
|
label: "Natural scrolling",
|
||||||
|
detail: "Content follows the direction of your fingers, as on a phone",
|
||||||
|
hypr: { path: ["input", "natural_scroll"], option: "input:natural_scroll", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "accelProfile", type: "enum", def: "flat", group: "pointer",
|
||||||
|
label: "Acceleration",
|
||||||
|
detail: "Flat moves the pointer the same distance however fast you move",
|
||||||
|
options: [
|
||||||
|
{ value: "flat", label: "Flat" },
|
||||||
|
{ value: "adaptive", label: "Adaptive" }
|
||||||
|
],
|
||||||
|
hypr: { path: ["input", "accel_profile"], option: "input:accel_profile", readAs: "str" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "scrollFactor", type: "real", def: 1.0, min: 0.1, max: 4.0, step: 0.1,
|
||||||
|
group: "pointer",
|
||||||
|
label: "Scroll speed",
|
||||||
|
detail: "Multiplies how far one notch of the wheel scrolls",
|
||||||
|
hypr: { path: ["input", "scroll_factor"], option: "input:scroll_factor", readAs: "float" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "leftHanded", type: "bool", def: false, group: "pointer",
|
||||||
|
label: "Left-handed",
|
||||||
|
detail: "Swap the primary and secondary buttons",
|
||||||
|
hypr: { path: ["input", "left_handed"], option: "input:left_handed", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "middleClickPaste", type: "bool", def: true, group: "pointer",
|
||||||
|
label: "Middle-click paste",
|
||||||
|
detail: "Paste the primary selection in GTK and native Wayland applications",
|
||||||
|
hypr: { path: ["misc", "middle_click_paste"], option: "misc:middle_click_paste", readAs: "bool" }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Touchpad ────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Shown only on machines that have one. These are separate from the
|
||||||
|
// pointer settings above because libinput keeps them separate: a mouse
|
||||||
|
// and a touchpad on the same machine can scroll in opposite directions,
|
||||||
|
// and usually should.
|
||||||
|
{
|
||||||
|
key: "touchpadTapToClick", type: "bool", def: true, group: "touchpad",
|
||||||
|
label: "Tap to click",
|
||||||
|
detail: "A tap counts as a click without pressing down",
|
||||||
|
// The Lua config key and the hyprctl option name genuinely differ
|
||||||
|
// here: hl.config wants input.touchpad.tap_to_click, getoption
|
||||||
|
// answers to input:touchpad:tap-to-click. Using either spelling for
|
||||||
|
// both fails -- a hyphen is not a Lua identifier, and the
|
||||||
|
// underscored name is not a known option to getoption. This is what
|
||||||
|
// the two separate fields are for.
|
||||||
|
hypr: { path: ["input", "touchpad", "tap_to_click"], option: "input:touchpad:tap-to-click", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "touchpadNaturalScroll", type: "bool", def: true, group: "touchpad",
|
||||||
|
label: "Natural scrolling",
|
||||||
|
detail: "Content follows the direction of your fingers",
|
||||||
|
hypr: { path: ["input", "touchpad", "natural_scroll"], option: "input:touchpad:natural_scroll", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "touchpadDisableWhileTyping", type: "bool", def: true, group: "touchpad",
|
||||||
|
label: "Disable while typing",
|
||||||
|
detail: "Ignore the touchpad briefly after a keystroke, so a palm cannot move the pointer",
|
||||||
|
hypr: { path: ["input", "touchpad", "disable_while_typing"], option: "input:touchpad:disable_while_typing", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "touchpadScrollFactor", type: "real", def: 1.0, min: 0.1, max: 4.0, step: 0.1,
|
||||||
|
group: "touchpad",
|
||||||
|
label: "Scroll speed",
|
||||||
|
detail: "Multiplies how far a two-finger scroll travels",
|
||||||
|
hypr: { path: ["input", "touchpad", "scroll_factor"], option: "input:touchpad:scroll_factor", readAs: "float" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "touchpadDragLock", type: "enum", def: 0, group: "touchpad",
|
||||||
|
label: "Drag lock",
|
||||||
|
detail: "Keeps a tap-and-drag active when you lift a finger mid-drag",
|
||||||
|
// An int with three states rather than a switch, which is why this
|
||||||
|
// is an enum: reported as `int` by getoption, not `bool`.
|
||||||
|
options: [
|
||||||
|
{ value: 0, label: "Off" },
|
||||||
|
{ value: 1, label: "On" },
|
||||||
|
{ value: 2, label: "On, until you tap again" }
|
||||||
|
],
|
||||||
|
hypr: { path: ["input", "touchpad", "drag_lock"], option: "input:touchpad:drag_lock", readAs: "int" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "touchpadMiddleButtonEmulation", type: "bool", def: false, group: "touchpad",
|
||||||
|
label: "Middle-click by pressing both buttons",
|
||||||
|
detail: "Pressing left and right together acts as a middle click",
|
||||||
|
hypr: { path: ["input", "touchpad", "middle_button_emulation"], option: "input:touchpad:middle_button_emulation", readAs: "bool" }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Multitasking ────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// GNOME's Multitasking panel, in Hyprland's terms. The Desktop page
|
||||||
|
// described the first two of these as read-only facts ("Layout:
|
||||||
|
// Tiling"), which was never true -- they are ordinary settings that
|
||||||
|
// happened not to have controls.
|
||||||
|
//
|
||||||
|
// Defaults here are Panama's shipped values from hypr/looks.lua, not
|
||||||
|
// Hyprland's own, so restoring defaults returns the desktop to how it
|
||||||
|
// ships rather than to how Hyprland would behave with no config.
|
||||||
|
{
|
||||||
|
key: "windowLayout", type: "enum", def: "dwindle", group: "multitasking",
|
||||||
|
label: "Tiling layout",
|
||||||
|
detail: "Dwindle splits the focused window; master keeps one large window beside a stack",
|
||||||
|
options: [
|
||||||
|
{ value: "dwindle", label: "Dwindle" },
|
||||||
|
{ value: "master", label: "Master and stack" }
|
||||||
|
],
|
||||||
|
hypr: { path: ["general", "layout"], option: "general:layout", readAs: "str" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "preserveSplit", type: "bool", def: true, group: "multitasking",
|
||||||
|
label: "Keep split direction",
|
||||||
|
detail: "New windows follow the split of the window they replace, instead of always halving the longer side",
|
||||||
|
hypr: { path: ["dwindle", "preserve_split"], option: "dwindle:preserve_split", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "forceSplit", type: "enum", def: 0, group: "multitasking",
|
||||||
|
label: "New windows open",
|
||||||
|
detail: "Where a new window lands relative to the one that was focused",
|
||||||
|
options: [
|
||||||
|
{ value: 0, label: "Where the pointer is" },
|
||||||
|
{ value: 1, label: "Always left or above" },
|
||||||
|
{ value: 2, label: "Always right or below" }
|
||||||
|
],
|
||||||
|
hypr: { path: ["dwindle", "force_split"], option: "dwindle:force_split", readAs: "int" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "windowSnapping", type: "bool", def: true, group: "multitasking",
|
||||||
|
label: "Snap floating windows",
|
||||||
|
detail: "Floating windows stick to screen edges and to each other as you drag them",
|
||||||
|
hypr: { path: ["general", "snap", "enabled"], option: "general:snap:enabled", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "workspaceBackAndForth", type: "bool", def: false, group: "multitasking",
|
||||||
|
label: "Switch back and forth",
|
||||||
|
detail: "Selecting the workspace you are already on returns you to the previous one",
|
||||||
|
hypr: { path: ["binds", "workspace_back_and_forth"], option: "binds:workspace_back_and_forth", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "allowWorkspaceCycles", type: "bool", def: false, group: "multitasking",
|
||||||
|
label: "Wrap around at the ends",
|
||||||
|
detail: "Moving past the last workspace continues from the first",
|
||||||
|
hypr: { path: ["binds", "allow_workspace_cycles"], option: "binds:allow_workspace_cycles", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "focusOnActivate", type: "bool", def: false, group: "multitasking",
|
||||||
|
label: "Let applications take focus",
|
||||||
|
detail: "An application asking for attention is switched to, rather than only highlighted",
|
||||||
|
hypr: { path: ["misc", "focus_on_activate"], option: "misc:focus_on_activate", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mouseMoveFocusesMonitor", type: "bool", def: true, group: "multitasking",
|
||||||
|
label: "Pointer changes active display",
|
||||||
|
detail: "Moving the pointer to another display makes it the active one",
|
||||||
|
hypr: { path: ["misc", "mouse_move_focuses_monitor"], option: "misc:mouse_move_focuses_monitor", readAs: "bool" }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Accessibility ───────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Only what Hyprland can actually deliver. GNOME's sticky keys, slow
|
||||||
|
// keys, bounce keys and mouse keys are AccessX, an X11 server feature:
|
||||||
|
// XKB under Wayland has no accessx option group at all (verified
|
||||||
|
// against evdev.lst), and Hyprland does not implement it. The
|
||||||
|
// compositor will happily STORE "accessx:enable" as a keyboard option
|
||||||
|
// and nothing will ever act on it, which is exactly the kind of switch
|
||||||
|
// this app refuses to ship.
|
||||||
|
{
|
||||||
|
key: "magnifierFactor", type: "real", def: 1.0, min: 1.0, max: 5.0, step: 0.1,
|
||||||
|
group: "accessibility",
|
||||||
|
label: "Magnifier",
|
||||||
|
detail: "Magnifies the screen around the pointer. 1.0 is off",
|
||||||
|
hypr: { path: ["cursor", "zoom_factor"], option: "cursor:zoom_factor", readAs: "float" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "magnifierRigid", type: "bool", def: false, group: "accessibility",
|
||||||
|
label: "Magnifier follows in steps",
|
||||||
|
detail: "Moves the magnified view in increments rather than gliding with the pointer",
|
||||||
|
hypr: { path: ["cursor", "zoom_rigid"], option: "cursor:zoom_rigid", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dimInactive", type: "bool", def: false, group: "accessibility",
|
||||||
|
label: "Dim inactive windows",
|
||||||
|
detail: "Darkens every window except the focused one, so the active window is unmistakable",
|
||||||
|
hypr: { path: ["decoration", "dim_inactive"], option: "decoration:dim_inactive", readAs: "bool" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "dimStrength", type: "real", def: 0.5, min: 0.05, max: 0.9, step: 0.05,
|
||||||
|
group: "accessibility",
|
||||||
|
label: "Dim amount",
|
||||||
|
detail: "How much darker unfocused windows are",
|
||||||
|
hypr: { path: ["decoration", "dim_strength"], option: "decoration:dim_strength", readAs: "float" }
|
||||||
|
},
|
||||||
|
|
||||||
// ── Night light ─────────────────────────────────────────────────────
|
// ── Night light ─────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
key: "nightLightEnabled", type: "bool", def: false, group: "nightLight",
|
key: "nightLightEnabled", type: "bool", def: false, group: "nightLight",
|
||||||
@@ -341,6 +747,66 @@ Singleton {
|
|||||||
label: "Wallpaper",
|
label: "Wallpaper",
|
||||||
detail: "Shown on every output"
|
detail: "Shown on every output"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "wallpaperMode", type: "enum", def: "single", group: "wallpaper",
|
||||||
|
label: "Wallpaper mode", detail: "Use one image, rotate a collection, or choose per display",
|
||||||
|
options: [
|
||||||
|
{ value: "single", label: "Single" },
|
||||||
|
{ value: "slideshow", label: "Slideshow" },
|
||||||
|
{ value: "per-monitor", label: "Per display" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wallpaperSlideshowPaths", type: "json", def: ([]), group: "wallpaper", internal: true,
|
||||||
|
label: "Slideshow collection", detail: "Backgrounds selected for rotation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wallpaperIntervalMinutes", type: "int", def: 30, min: 5, max: 1440, step: 5,
|
||||||
|
unit: "min", group: "wallpaper", label: "Change background every",
|
||||||
|
detail: "Time between slideshow images"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wallpaperShuffle", type: "bool", def: true, group: "wallpaper",
|
||||||
|
label: "Shuffle", detail: "Show every selected image before repeating"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "wallpaperPerMonitor", type: "json", def: ({}), group: "wallpaper", internal: true,
|
||||||
|
label: "Per-display backgrounds", detail: "Background assigned to each connected display"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Lock-screen appearance ─────────────────────────────────────────
|
||||||
|
// scripts/panama-lock validates these again before generating a state
|
||||||
|
// config. The tracked hyprlock.conf remains the safe fallback.
|
||||||
|
{
|
||||||
|
key: "lockBackgroundMode", type: "enum", def: "screenshot", group: "lockAppearance",
|
||||||
|
label: "Background", detail: "What appears behind the lock screen",
|
||||||
|
options: [
|
||||||
|
{ value: "screenshot", label: "Blurred desktop" },
|
||||||
|
{ value: "wallpaper", label: "Current wallpaper" },
|
||||||
|
{ value: "solid", label: "Solid color" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lockBlurLevel", type: "int", def: 3, min: 0, max: 5, step: 1,
|
||||||
|
group: "lockAppearance", label: "Background blur",
|
||||||
|
detail: "Softens what is behind the password field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lockShowClock", type: "bool", def: true, group: "lockAppearance",
|
||||||
|
label: "Show clock", detail: "Use the desktop's 12 or 24-hour format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lockShowDate", type: "bool", def: true, group: "lockAppearance",
|
||||||
|
label: "Show date", detail: "Show the weekday and full date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lockShowUser", type: "bool", def: true, group: "lockAppearance",
|
||||||
|
label: "Show user name", detail: "Identify the signed-in account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "lockFadeOnEmpty", type: "bool", def: false, group: "lockAppearance",
|
||||||
|
label: "Hide password field until typing", detail: "Keep the empty field out of the way"
|
||||||
|
},
|
||||||
|
|
||||||
// ── Idle, lock, and sleep ───────────────────────────────────────────
|
// ── Idle, lock, and sleep ───────────────────────────────────────────
|
||||||
// Written into a generated hypridle config; see scripts/panama-idle.
|
// Written into a generated hypridle config; see scripts/panama-idle.
|
||||||
@@ -369,6 +835,176 @@ Singleton {
|
|||||||
detail: "Requires your password when the machine wakes"
|
detail: "Requires your password when the machine wakes"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Night light schedule ────────────────────────────────────────────
|
||||||
|
// Hours as decimals, so 17.5 is half past five. Wrapping past midnight
|
||||||
|
// is normal here and is what the shipped values do: on at 17:00, off at
|
||||||
|
// 10:00 the following morning.
|
||||||
|
{
|
||||||
|
key: "nightLightFrom", type: "real", def: 17.0, min: 0, max: 23.5, step: 0.5,
|
||||||
|
group: "nightLight",
|
||||||
|
label: "Turns on at",
|
||||||
|
detail: "Only used when Night Light follows a schedule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "nightLightTo", type: "real", def: 10.0, min: 0, max: 23.5, step: 0.5,
|
||||||
|
group: "nightLight",
|
||||||
|
label: "Turns off at",
|
||||||
|
detail: "A time earlier than the start simply means the next morning"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Colour scheme ───────────────────────────────────────────────────
|
||||||
|
// 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
|
||||||
|
// different lightness, which is what keeps the Prism identity intact
|
||||||
|
// across the switch.
|
||||||
|
//
|
||||||
|
// services/ColorScheme.qml pushes the choice to GTK and to the
|
||||||
|
// compositor's border colours, because an application toolbar or a
|
||||||
|
// window border still wearing the other scheme is more jarring than
|
||||||
|
// either scheme on its own.
|
||||||
|
{
|
||||||
|
key: "colorScheme", type: "enum", def: "dark", group: "appearance",
|
||||||
|
label: "Appearance",
|
||||||
|
detail: "Light and dark share one identity, not two themes",
|
||||||
|
options: [
|
||||||
|
{ value: "dark", label: "Dark" },
|
||||||
|
{ value: "light", label: "Light" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Application themes ─────────────────────────────────────────────
|
||||||
|
// ColorScheme owns GTK's light/dark theme. These are the two theme
|
||||||
|
// choices GNOME applications expose independently of that palette:
|
||||||
|
// their icons and pointer. DesktopStyle only accepts names found in
|
||||||
|
// the read-only XDG catalog before storing them.
|
||||||
|
{
|
||||||
|
key: "cursorTheme", type: "string", def: "oreo_blue_cursors", group: "themes",
|
||||||
|
pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$",
|
||||||
|
label: "Pointer theme",
|
||||||
|
detail: "The pointer design used by applications and Hyprland"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "iconTheme", type: "string", def: "Adwaita", group: "themes",
|
||||||
|
pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$",
|
||||||
|
label: "Application icons",
|
||||||
|
detail: "The icon set used by GTK applications"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Typography ──────────────────────────────────────────────────────
|
||||||
|
// The single largest thing in this desktop that used to be changeable
|
||||||
|
// only by editing Theme.qml.
|
||||||
|
//
|
||||||
|
// interfaceFont is every piece of text a person reads. iconFont is used
|
||||||
|
// ONLY to draw glyphs -- workspace pills, the status cluster, search
|
||||||
|
// icons -- so it must carry Nerd Font glyphs; a plain monospace family
|
||||||
|
// there replaces every icon in the shell with tofu, which is why the
|
||||||
|
// picker offers them as a separate, filtered list.
|
||||||
|
{
|
||||||
|
key: "interfaceFont", type: "string", def: "Adwaita Sans", group: "typography",
|
||||||
|
// A family name, matched by fontconfig. Constrained because it also
|
||||||
|
// reaches hl.config as a string in hypr/looks.lua.
|
||||||
|
pattern: "^[A-Za-z0-9 ._-]{1,64}$",
|
||||||
|
label: "Interface font",
|
||||||
|
detail: "Used for every piece of text in the shell"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "iconFont", type: "string", def: "VictorMono Nerd Font", group: "typography",
|
||||||
|
pattern: "^[A-Za-z0-9 ._-]{1,64}$",
|
||||||
|
label: "Icon font",
|
||||||
|
detail: "Draws the shell's glyphs, so it must be a Nerd Font"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "interfaceFontSize", type: "int", def: 13, min: 10, max: 18, step: 1,
|
||||||
|
unit: "px", group: "typography",
|
||||||
|
label: "Interface text size",
|
||||||
|
detail: "The base size the rest of the shell's type scales from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "applicationFont", type: "string", def: "Adwaita Sans", group: "typography",
|
||||||
|
pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$",
|
||||||
|
label: "Application font",
|
||||||
|
detail: "Used by menus, controls, and labels in applications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "applicationFontSize", type: "int", def: 11, min: 6, max: 32, step: 1,
|
||||||
|
unit: "pt", group: "typography",
|
||||||
|
label: "Application text size",
|
||||||
|
detail: "The base text size used by applications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "documentFont", type: "string", def: "Adwaita Sans", group: "typography",
|
||||||
|
pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$",
|
||||||
|
label: "Document font",
|
||||||
|
detail: "Used for document content when an application follows the system choice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "documentFontSize", type: "int", def: 12, min: 6, max: 32, step: 1,
|
||||||
|
unit: "pt", group: "typography",
|
||||||
|
label: "Document text size",
|
||||||
|
detail: "The default text size for document content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "monospaceFont", type: "string", def: "VictorMono Nerd Font", group: "typography",
|
||||||
|
pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$",
|
||||||
|
label: "Monospace font",
|
||||||
|
detail: "Used by terminals, editors, and code fields that follow the system choice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "monospaceFontSize", type: "int", def: 10, min: 6, max: 32, step: 1,
|
||||||
|
unit: "pt", group: "typography",
|
||||||
|
label: "Monospace text size",
|
||||||
|
detail: "The default text size for terminals and code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "fontHinting", type: "enum", def: "slight", group: "typography",
|
||||||
|
label: "Font hinting",
|
||||||
|
detail: "How strongly text aligns to the pixel grid",
|
||||||
|
options: [
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
{ value: "slight", label: "Slight" },
|
||||||
|
{ value: "medium", label: "Medium" },
|
||||||
|
{ value: "full", label: "Full" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "fontAntialiasing", type: "enum", def: "rgba", group: "typography",
|
||||||
|
label: "Text smoothing",
|
||||||
|
detail: "How application text softens its edges",
|
||||||
|
options: [
|
||||||
|
{ value: "none", label: "None" },
|
||||||
|
{ value: "grayscale", label: "Grayscale" },
|
||||||
|
{ value: "rgba", label: "Subpixel" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Application titlebars ──────────────────────────────────────────
|
||||||
|
// These affect applications that honour GNOME's window preferences.
|
||||||
|
// Hyprland itself has no server-side titlebar buttons, so minimize is
|
||||||
|
// deliberately absent rather than presented as a switch that lies.
|
||||||
|
{
|
||||||
|
key: "titlebarButtonSide", type: "enum", def: "right", group: "titlebar",
|
||||||
|
label: "Button side",
|
||||||
|
detail: "Place application titlebar buttons on the left or right",
|
||||||
|
options: [
|
||||||
|
{ value: "left", label: "Left" },
|
||||||
|
{ value: "right", label: "Right" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "titlebarMaximizeButton", type: "bool", def: false, group: "titlebar",
|
||||||
|
label: "Maximize button",
|
||||||
|
detail: "Show a maximize button in application titlebars that support it"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "titlebarDoubleClick", type: "enum", def: "toggle-maximize", group: "titlebar",
|
||||||
|
label: "Double-click titlebar",
|
||||||
|
detail: "Choose what a double-click on an application titlebar does",
|
||||||
|
options: [
|
||||||
|
{ value: "toggle-maximize", label: "Toggle maximize" },
|
||||||
|
{ value: "none", label: "Do nothing" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// ── Accessibility ───────────────────────────────────────────────────
|
// ── Accessibility ───────────────────────────────────────────────────
|
||||||
// Backed by gsettings so GTK applications agree with the shell, and
|
// Backed by gsettings so GTK applications agree with the shell, and
|
||||||
// pushed to the compositor as well where it has its own notion.
|
// pushed to the compositor as well where it has its own notion.
|
||||||
@@ -402,6 +1038,47 @@ Singleton {
|
|||||||
detail: "How often Panama updates the current conditions"
|
detail: "How often Panama updates the current conditions"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Which GPU the vitals readout tracks ─────────────────────────────
|
||||||
|
// A sysfs path rather than a card number, because the number is neither
|
||||||
|
// stable across machines nor meaningful. Constrained to the one shape
|
||||||
|
// that can be read for utilisation; VitalsWidget hides itself when the
|
||||||
|
// path is unreadable, so a stale value degrades to no readout rather
|
||||||
|
// than a wrong one.
|
||||||
|
{
|
||||||
|
key: "gpuBusyPath", type: "string",
|
||||||
|
def: "/sys/class/drm/card1/device/gpu_busy_percent",
|
||||||
|
group: "vitals", internal: true,
|
||||||
|
pattern: "^/sys/class/drm/card[0-9]+/device/gpu_busy_percent$",
|
||||||
|
label: "Graphics device",
|
||||||
|
detail: "Which GPU the graphics readout in the bar measures"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Weather location ────────────────────────────────────────────────
|
||||||
|
// 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
|
||||||
|
// in the UI; it is never sent anywhere, so it can say whatever makes the
|
||||||
|
// reading recognisable.
|
||||||
|
{
|
||||||
|
key: "weatherLatitude", type: "real", def: 27.7375, min: -90, max: 90, step: 0.0001,
|
||||||
|
group: "weather", internal: true,
|
||||||
|
label: "Latitude",
|
||||||
|
detail: "Set by choosing a location"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "weatherLongitude", type: "real", def: -82.6861, min: -180, max: 180, step: 0.0001,
|
||||||
|
group: "weather", internal: true,
|
||||||
|
label: "Longitude",
|
||||||
|
detail: "Set by choosing a location"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "weatherLocation", type: "string", def: "Local weather", group: "weather",
|
||||||
|
internal: true,
|
||||||
|
// Display only -- never sent to the weather service.
|
||||||
|
pattern: "^[^\\n]{1,64}$",
|
||||||
|
label: "Weather location",
|
||||||
|
detail: "The place the weather reading is for"
|
||||||
|
},
|
||||||
|
|
||||||
// ── Vitals refresh ──────────────────────────────────────────────────
|
// ── Vitals refresh ──────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
key: "vitalsIntervalMs", type: "int", def: 2000, min: 500, max: 10000, step: 500,
|
key: "vitalsIntervalMs", type: "int", def: 2000, min: 500, max: 10000, step: 500,
|
||||||
@@ -476,13 +1153,18 @@ Singleton {
|
|||||||
// A "json" value: the ordered list of desktop entry ids pinned to the
|
// A "json" value: the ordered list of desktop entry ids pinned to the
|
||||||
// dock. Kept in the shared store so that reordering the dock is covered
|
// dock. Kept in the shared store so that reordering the dock is covered
|
||||||
// by Restore defaults like everything else, rather than living in its
|
// by Restore defaults like everything else, rather than living in its
|
||||||
// own file. The shipped order is the GNOME dash it replaced.
|
// own file. The shipped order is the GNOME dash it replaced, with one
|
||||||
|
// deliberate substitution: Panama Settings takes the first slot rather
|
||||||
|
// than GNOME Settings. Panama now covers what GNOME Settings did for
|
||||||
|
// this desktop and delegates the remainder to it by name, so pinning
|
||||||
|
// the thing it delegates TO put the fallback in front of the real one.
|
||||||
|
// GNOME Settings stays installed and searchable in the launcher.
|
||||||
{
|
{
|
||||||
key: "dockPinned", type: "json", group: "dock",
|
key: "dockPinned", type: "json", group: "dock",
|
||||||
label: "Pinned applications",
|
label: "Pinned applications",
|
||||||
detail: "Applications that stay in the Dock whether or not they are running",
|
detail: "Applications that stay in the Dock whether or not they are running",
|
||||||
def: [
|
def: [
|
||||||
"org.gnome.Settings", "kitty", "org.gnome.Nautilus",
|
"panama-settings", "kitty", "org.gnome.Nautilus",
|
||||||
"com.bitwarden.desktop", "org.gnome.Software", "helium",
|
"com.bitwarden.desktop", "org.gnome.Software", "helium",
|
||||||
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
||||||
"app.bluebubbles.BlueBubbles", "rustdesk",
|
"app.bluebubbles.BlueBubbles", "rustdesk",
|
||||||
@@ -509,7 +1191,7 @@ Singleton {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── Display configuration ───────────────────────────────────────────
|
// ── Display configuration ───────────────────────────────────────────
|
||||||
// { "<output>": { mode, scale, transform } }, 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. Colour 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
|
||||||
@@ -518,7 +1200,22 @@ Singleton {
|
|||||||
key: "displays", type: "json", def: ({}), group: "display",
|
key: "displays", type: "json", def: ({}), group: "display",
|
||||||
internal: true,
|
internal: true,
|
||||||
label: "Display configuration",
|
label: "Display configuration",
|
||||||
detail: "Resolution, scale, and rotation per connected display"
|
detail: "Resolution, scale, rotation, position, and primary display"
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Per-application notification rules ──────────────────────────────
|
||||||
|
// { "<appId>": { enabled, showOnLockScreen, showContentOnLockScreen } }
|
||||||
|
//
|
||||||
|
// Absent means "no rule", which is not the same as a rule that allows
|
||||||
|
// everything: a new application must be able to notify without needing
|
||||||
|
// an entry written for it first. services/Notifs.qml treats a missing
|
||||||
|
// entry as permissive and Do Not Disturb remains an override on top,
|
||||||
|
// rather than being duplicated per application.
|
||||||
|
{
|
||||||
|
key: "notificationAppRules", type: "json", def: ({}), group: "notifications",
|
||||||
|
internal: true,
|
||||||
|
label: "Application notification rules",
|
||||||
|
detail: "Per-application notification and lock-screen visibility preferences"
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Internal ────────────────────────────────────────────────────────
|
// ── Internal ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ Singleton {
|
|||||||
// ── Weather ─────────────────────────────────────────────────────────────
|
// ── Weather ─────────────────────────────────────────────────────────────
|
||||||
// Coordinates taken from the GNOME night-light setting, which had already
|
// Coordinates taken from the GNOME night-light setting, which had already
|
||||||
// resolved the location. Uses Open-Meteo, which needs no API key.
|
// resolved the location. Uses Open-Meteo, which needs no API key.
|
||||||
readonly property real latitude: 27.7375
|
readonly property real latitude: DesktopPreferences.get("weatherLatitude")
|
||||||
readonly property real longitude: -82.6861
|
readonly property real longitude: DesktopPreferences.get("weatherLongitude")
|
||||||
// Open-Meteo returns coordinates but no friendly place name. Keep the
|
// Open-Meteo returns coordinates but no friendly place name. Keep the
|
||||||
// label deliberately general rather than exposing precise coordinates in
|
// label deliberately general rather than exposing precise coordinates in
|
||||||
// the UI or guessing at a city from them.
|
// the UI or guessing at a city from them.
|
||||||
readonly property string weatherLocation: "Local weather"
|
readonly property string weatherLocation: DesktopPreferences.get("weatherLocation")
|
||||||
readonly property string temperatureUnit: DesktopPreferences.get("temperatureUnit")
|
readonly property string temperatureUnit: DesktopPreferences.get("temperatureUnit")
|
||||||
readonly property int weatherRefreshMinutes: DesktopPreferences.get("weatherRefreshMinutes")
|
readonly property int weatherRefreshMinutes: DesktopPreferences.get("weatherRefreshMinutes")
|
||||||
|
|
||||||
@@ -41,13 +41,13 @@ Singleton {
|
|||||||
|
|
||||||
// amdgpu exposes utilisation here. Verified present on this machine; the
|
// amdgpu exposes utilisation here. Verified present on this machine; the
|
||||||
// widget hides itself if the path is missing rather than showing zeros.
|
// widget hides itself if the path is missing rather than showing zeros.
|
||||||
readonly property string gpuBusyPath: "/sys/class/drm/card1/device/gpu_busy_percent"
|
readonly property string gpuBusyPath: DesktopPreferences.get("gpuBusyPath")
|
||||||
|
|
||||||
// ── Night light ─────────────────────────────────────────────────────────
|
// ── Night light ─────────────────────────────────────────────────────────
|
||||||
// Matches the (disabled) GNOME schedule: 3500K from 17:00 to 10:00.
|
// Matches the (disabled) GNOME schedule: 3500K from 17:00 to 10:00.
|
||||||
readonly property int nightLightTemperature: DesktopPreferences.get("nightLightTemperature")
|
readonly property int nightLightTemperature: DesktopPreferences.get("nightLightTemperature")
|
||||||
readonly property real nightLightFrom: 17.0
|
readonly property real nightLightFrom: DesktopPreferences.get("nightLightFrom")
|
||||||
readonly property real nightLightTo: 10.0
|
readonly property real nightLightTo: DesktopPreferences.get("nightLightTo")
|
||||||
readonly property bool nightLightEnabledByDefault: DesktopPreferences.get("nightLightEnabled")
|
readonly property bool nightLightEnabledByDefault: DesktopPreferences.get("nightLightEnabled")
|
||||||
|
|
||||||
// ── Notifications ───────────────────────────────────────────────────────
|
// ── Notifications ───────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -18,36 +18,47 @@ import QtQuick
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// ── Colour scheme ───────────────────────────────────────────────────────
|
||||||
|
// 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
|
||||||
|
// same hues at different lightness, which is what lets the Prism identity
|
||||||
|
// survive the switch: blue still leads into orchid, it is simply a darker
|
||||||
|
// blue on a lighter ground.
|
||||||
|
//
|
||||||
|
// Every token below is a binding on this, so flipping it repaints the whole
|
||||||
|
// shell without anything needing to know it happened.
|
||||||
|
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
|
||||||
|
|
||||||
// ── Palette ─────────────────────────────────────────────────────────────
|
// ── Palette ─────────────────────────────────────────────────────────────
|
||||||
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
|
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
|
||||||
readonly property color bg: "#222436"
|
readonly property color bg: root.dark ? "#222436" : "#e1e2e7"
|
||||||
readonly property color bgDark: "#1e2030"
|
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de"
|
||||||
readonly property color bgHighlight: "#2f334d"
|
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da"
|
||||||
readonly property color bgPanel: "#2e2f3d" // dock glass surface
|
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface
|
||||||
readonly property color bgPopover: "#21212f" // Openbar submenu background
|
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background
|
||||||
|
|
||||||
readonly property color fg: "#c8d3f5"
|
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf"
|
||||||
readonly property color fgDim: "#828bb8"
|
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0"
|
||||||
readonly property color fgMuted: "#636da6"
|
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
||||||
readonly property color gutter: "#3b4261"
|
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
||||||
|
|
||||||
// The pair. `accent` is the primary and carries every state meaning
|
// The pair. `accent` is the primary and carries every state meaning
|
||||||
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
||||||
// theme — it never appears alone, only as the far end of a gradient. That
|
// theme — it never appears alone, only as the far end of a gradient. That
|
||||||
// restraint is the whole point: the two colours meeting is the signature,
|
// restraint is the whole point: the two colours meeting is the signature,
|
||||||
// so the pink stops being special the moment it's used as a flat fill.
|
// so the pink stops being special the moment it's used as a flat fill.
|
||||||
readonly property color accent: "#82aaff" // blue
|
readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue
|
||||||
readonly property color accentSecondary: "#b172b0" // orchid, from tmux
|
readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux
|
||||||
readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
|
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
||||||
readonly property color cyan: "#86e1fc"
|
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
||||||
readonly property color teal: "#4fd6be"
|
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
||||||
readonly property color green: "#c3e88d"
|
readonly property color green: root.dark ? "#c3e88d" : "#587539"
|
||||||
readonly property color yellow: "#ffc777"
|
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e"
|
||||||
readonly property color orange: "#ff966c"
|
readonly property color orange: root.dark ? "#ff966c" : "#b15c00"
|
||||||
readonly property color red: "#ff757f"
|
readonly property color red: root.dark ? "#ff757f" : "#f52a65"
|
||||||
readonly property color redDeep: "#c53b53"
|
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343"
|
||||||
readonly property color magenta: "#c099ff"
|
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1"
|
||||||
readonly property color pink: "#fca7ea"
|
readonly property color pink: root.dark ? "#fca7ea" : "#d20065"
|
||||||
|
|
||||||
// Semantic aliases — prefer these in widgets so intent survives a repaint.
|
// Semantic aliases — prefer these in widgets so intent survives a repaint.
|
||||||
readonly property color ok: green
|
readonly property color ok: green
|
||||||
@@ -59,11 +70,14 @@ Singleton {
|
|||||||
// The dock sits on compositor blur, so its fill is deliberately very
|
// The dock sits on compositor blur, so its fill is deliberately very
|
||||||
// translucent. Popovers are near-opaque because they carry text that must
|
// translucent. Popovers are near-opaque because they carry text that must
|
||||||
// stay legible. The bar itself has no surface or alpha token.
|
// stay legible. The bar itself has no surface or alpha token.
|
||||||
readonly property real dockAlpha: 0.34
|
// Light surfaces need more opacity for the same sense of a solid panel: the
|
||||||
readonly property real popoverAlpha: 0.92
|
// same 0.34 that reads as glass over a dark desktop reads as haze over a
|
||||||
readonly property real overlayAlpha: 0.55
|
// light one, and text on it stops being legible.
|
||||||
readonly property real hoverAlpha: 0.14
|
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
|
||||||
readonly property real activeAlpha: 0.24
|
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97
|
||||||
|
readonly property real overlayAlpha: root.dark ? 0.55 : 0.40
|
||||||
|
readonly property real hoverAlpha: root.dark ? 0.14 : 0.10
|
||||||
|
readonly property real activeAlpha: root.dark ? 0.24 : 0.18
|
||||||
|
|
||||||
// ── Geometry ────────────────────────────────────────────────────────────
|
// ── Geometry ────────────────────────────────────────────────────────────
|
||||||
readonly property int barHeight: 36
|
readonly property int barHeight: 36
|
||||||
@@ -90,12 +104,12 @@ Singleton {
|
|||||||
// Adwaita Sans for everything the user reads as text. No exceptions: a
|
// Adwaita Sans for everything the user reads as text. No exceptions: a
|
||||||
// monospaced clock or percentage reads as a terminal readout pasted into a
|
// monospaced clock or percentage reads as a terminal readout pasted into a
|
||||||
// UI, which is the opposite of the intent here.
|
// UI, which is the opposite of the intent here.
|
||||||
readonly property string fontFamily: "Adwaita Sans"
|
readonly property string fontFamily: DesktopPreferences.get("interfaceFont")
|
||||||
|
|
||||||
// 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 recolour (see widgets/ThemedIcon.qml).
|
||||||
readonly property string fontMono: "VictorMono Nerd Font"
|
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,
|
||||||
// elapsed timers, dimension readouts. Tabular figures all share one
|
// elapsed timers, dimension readouts. Tabular figures all share one
|
||||||
@@ -107,10 +121,13 @@ Singleton {
|
|||||||
readonly property var tabularFigures: ({
|
readonly property var tabularFigures: ({
|
||||||
"tnum": 1
|
"tnum": 1
|
||||||
})
|
})
|
||||||
readonly property int fontSize: 13
|
// The four sizes are a scale, not four independent numbers: they move
|
||||||
readonly property int fontSizeSmall: 11
|
// together so the relationship between body, caption, heading and title
|
||||||
readonly property int fontSizeLarge: 16
|
// survives a change to the base rather than drifting apart.
|
||||||
readonly property int fontSizeTitle: 20
|
readonly property int fontSize: DesktopPreferences.get("interfaceFontSize")
|
||||||
|
readonly property int fontSizeSmall: Math.max(9, root.fontSize - 2)
|
||||||
|
readonly property int fontSizeLarge: root.fontSize + 3
|
||||||
|
readonly property int fontSizeTitle: root.fontSize + 7
|
||||||
|
|
||||||
// ── Motion ──────────────────────────────────────────────────────────────
|
// ── Motion ──────────────────────────────────────────────────────────────
|
||||||
// Event-driven only. Nothing in this shell animates while idle — no pulse,
|
// Event-driven only. Nothing in this shell animates while idle — no pulse,
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ singleton PreferenceSchema 1.0 PreferenceSchema.qml
|
|||||||
singleton HomePreferences 1.0 HomePreferences.qml
|
singleton HomePreferences 1.0 HomePreferences.qml
|
||||||
singleton Settings 1.0 Settings.qml
|
singleton Settings 1.0 Settings.qml
|
||||||
singleton Theme 1.0 Theme.qml
|
singleton Theme 1.0 Theme.qml
|
||||||
|
singleton Migrations 1.0 Migrations.qml
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
IpcHandler {
|
||||||
|
target: "connectivity-test"
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
wifiDevice: Connectivity.wifiDevice ? Connectivity.wifiDevice.name : "",
|
||||||
|
wiredDevice: Connectivity.wiredDevice ? Connectivity.wiredDevice.name : "",
|
||||||
|
wiredConnected: !!(Connectivity.wiredDevice && Connectivity.wiredDevice.connected),
|
||||||
|
networks: Connectivity.networks.length,
|
||||||
|
activeSsid: Connectivity.activeNetwork ? Connectivity.activeNetwork.name : "",
|
||||||
|
activeStrength: Connectivity.activeNetwork ? Connectivity.activeNetwork.signalStrength : -1,
|
||||||
|
activeLabel: Connectivity.activeNetwork ? Connectivity.signalLabel(Connectivity.activeNetwork.signalStrength) : "",
|
||||||
|
adapter: Connectivity.adapter ? true : false,
|
||||||
|
btDevices: Connectivity.bluetoothDevices.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelFor(strength: real): string { return Connectivity.signalLabel(strength); }
|
||||||
|
function setActive(on: bool): void { Connectivity.active = on; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.modules.settings
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
DisplayIdentify {}
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: fixtureService
|
||||||
|
|
||||||
|
property var monitors: [
|
||||||
|
{
|
||||||
|
name: "DP-2", description: "Primary display", width: 4500, height: 3000,
|
||||||
|
refreshRate: 60, mode: "[email protected]", scale: 1.5,
|
||||||
|
transform: 0, x: 0, y: 0, primary: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HDMI-A-1", description: "Second display", width: 2560, height: 1440,
|
||||||
|
refreshRate: 60, mode: "[email protected]", scale: 1,
|
||||||
|
transform: 0, x: 3000, y: 0, primary: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
property var applied: []
|
||||||
|
|
||||||
|
function currentLayout(): var {
|
||||||
|
return monitors.map(record => Object.assign({}, record));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLayout(layout: var): bool {
|
||||||
|
applied = layout.map(record => Object.assign({}, record));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayArrangement {
|
||||||
|
id: arrangement
|
||||||
|
width: 800
|
||||||
|
displayService: fixtureService
|
||||||
|
selectedOutput: "HDMI-A-1"
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "display-arrangement-test"
|
||||||
|
|
||||||
|
function status(width: int): string {
|
||||||
|
arrangement.width = width;
|
||||||
|
arrangement.resetDraft();
|
||||||
|
return JSON.stringify(arrangement.canvasSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragFixture(): string {
|
||||||
|
arrangement.resetDraft();
|
||||||
|
arrangement.setDraftPosition("HDMI-A-1", 3016, 0, true);
|
||||||
|
arrangement.applyDraft();
|
||||||
|
return JSON.stringify(fixtureService.applied);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyboardFixture(): string {
|
||||||
|
arrangement.resetDraft();
|
||||||
|
arrangement.nudge("HDMI-A-1", -10, 0);
|
||||||
|
const afterArrow = arrangement.draftLayout.find(record => record.name === "HDMI-A-1").x;
|
||||||
|
arrangement.nudge("HDMI-A-1", -100, 0);
|
||||||
|
const afterShiftArrow = arrangement.draftLayout.find(record => record.name === "HDMI-A-1").x;
|
||||||
|
return JSON.stringify({ afterArrow, afterShiftArrow });
|
||||||
|
}
|
||||||
|
|
||||||
|
function primaryFixture(): string {
|
||||||
|
arrangement.resetDraft();
|
||||||
|
arrangement.makePrimary("HDMI-A-1");
|
||||||
|
return JSON.stringify(fixtureService.applied.map(record => ({
|
||||||
|
name: record.name, x: record.x, y: record.y, primary: record.primary
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
function identify(): void { Displays.identify(); }
|
||||||
|
function identifying(): bool { return Displays.identifying; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import "services/DisplayLayout.js" as DisplayLayout
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
readonly property var fixture: [
|
||||||
|
{ name: "DP-2", width: 4500, height: 3000, scale: 1.5, transform: 0, x: 140, y: 80, primary: true },
|
||||||
|
{ name: "HDMI-A-1", width: 2560, height: 1440, scale: 1, transform: 1, x: 3140, y: 80, primary: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "display-layout-test"
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
const normalized = DisplayLayout.normalize(fixture);
|
||||||
|
const canvas = DisplayLayout.canvasRects(normalized, 800, 500, 20);
|
||||||
|
const near = normalized.map(record => Object.assign({}, record));
|
||||||
|
near[1].x = 3016;
|
||||||
|
const far = normalized.map(record => Object.assign({}, record));
|
||||||
|
far[1].x = 3017;
|
||||||
|
return JSON.stringify({
|
||||||
|
valid: DisplayLayout.validate(fixture),
|
||||||
|
sizes: fixture.map(DisplayLayout.logicalSize),
|
||||||
|
normalized: normalized.map(record => ({ name: record.name, x: record.x, y: record.y, primary: record.primary })),
|
||||||
|
bounds: canvas.bounds,
|
||||||
|
canvasScale: canvas.scale,
|
||||||
|
canvasRects: canvas.rects,
|
||||||
|
near: DisplayLayout.snap(near, "HDMI-A-1", 16).find(record => record.name === "HDMI-A-1").x,
|
||||||
|
far: DisplayLayout.snap(far, "HDMI-A-1", 16).find(record => record.name === "HDMI-A-1").x
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalid(): string {
|
||||||
|
const base = fixture.map(record => Object.assign({}, record));
|
||||||
|
const cases = [];
|
||||||
|
const add = layout => cases.push(DisplayLayout.validate(layout));
|
||||||
|
add([base[0], Object.assign({}, base[1], { name: "DP-2" })]);
|
||||||
|
add(base.map(record => Object.assign({}, record, { primary: false })));
|
||||||
|
add(base.map(record => Object.assign({}, record, { primary: true })));
|
||||||
|
add([Object.assign({}, base[0], { x: 0.5 }), base[1]]);
|
||||||
|
add([Object.assign({}, base[0], { scale: 0 }), base[1]]);
|
||||||
|
add([Object.assign({}, base[0], { transform: 4 }), base[1]]);
|
||||||
|
add([Object.assign({}, base[0], { width: Infinity }), base[1]]);
|
||||||
|
add([Object.assign({}, base[0], { width: 0 }), base[1]]);
|
||||||
|
return JSON.stringify(cases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ ShellRoot {
|
|||||||
mode: monitor ? monitor.mode : "",
|
mode: monitor ? monitor.mode : "",
|
||||||
scale: monitor ? monitor.scale : 0,
|
scale: monitor ? monitor.scale : 0,
|
||||||
transform: monitor ? monitor.transform : -1,
|
transform: monitor ? monitor.transform : -1,
|
||||||
|
x: monitor ? monitor.x : 0,
|
||||||
|
y: monitor ? monitor.y : 0,
|
||||||
|
primary: monitor ? monitor.primary : false,
|
||||||
modes: monitor ? monitor.modes.length : 0,
|
modes: monitor ? monitor.modes.length : 0,
|
||||||
awaiting: Displays.awaitingConfirmation,
|
awaiting: Displays.awaitingConfirmation,
|
||||||
canConfirm: Displays.canConfirm,
|
canConfirm: Displays.canConfirm,
|
||||||
@@ -36,6 +39,49 @@ ShellRoot {
|
|||||||
return Displays.apply(monitor.name, mode, scale, monitor.transform);
|
return Displays.apply(monitor.name, mode, scale, monitor.transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transactionStatus(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
layout: Displays.currentLayout(),
|
||||||
|
pending: Displays.pendingRequestedLayout,
|
||||||
|
previous: Displays.pendingPreviousLayout,
|
||||||
|
reverting: Displays.revertExpectedLayout,
|
||||||
|
awaiting: Displays.awaitingConfirmation,
|
||||||
|
canConfirm: Displays.canConfirm,
|
||||||
|
busy: Displays.busy,
|
||||||
|
generation: Displays.operationGeneration,
|
||||||
|
revertGeneration: Displays.revertGeneration,
|
||||||
|
lastError: Displays.lastError
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLayoutFixture(secondX: int, secondY: int): bool {
|
||||||
|
const layout = Displays.currentLayout();
|
||||||
|
if (layout.length !== 2) return false;
|
||||||
|
layout[0].x = 0;
|
||||||
|
layout[0].y = 0;
|
||||||
|
layout[0].primary = true;
|
||||||
|
layout[1].x = secondX;
|
||||||
|
layout[1].y = secondY;
|
||||||
|
layout[1].primary = false;
|
||||||
|
return Displays.applyLayout(layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePrimaryFixture(output: string): bool {
|
||||||
|
return Displays.makePrimary(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectReadback(text: string, generation: int): void {
|
||||||
|
Displays.parse(text, generation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expireApplyVerification(): void {
|
||||||
|
Displays.verificationTimedOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
function expireRevertVerification(): void {
|
||||||
|
Displays.revertVerificationTimedOut();
|
||||||
|
}
|
||||||
|
|
||||||
function refreshIdentityFixture(): string {
|
function refreshIdentityFixture(): string {
|
||||||
const modes = Displays.normaliseModes([
|
const modes = Displays.normaliseModes([
|
||||||
"[email protected]",
|
"[email protected]",
|
||||||
@@ -49,6 +95,30 @@ ShellRoot {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positionFixture(): string {
|
||||||
|
const previous = Displays.monitors;
|
||||||
|
Displays.parse(JSON.stringify([
|
||||||
|
{
|
||||||
|
name: "DP-2", description: "Primary", width: 4500, height: 3000,
|
||||||
|
refreshRate: 60, scale: 1.5, transform: 0, x: 140, y: 80,
|
||||||
|
availableModes: ["[email protected]"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HDMI-A-1", description: "Second", width: 2560, height: 1440,
|
||||||
|
refreshRate: 60, scale: 1, transform: 0, x: 3140, y: 80,
|
||||||
|
availableModes: ["[email protected]"]
|
||||||
|
}
|
||||||
|
]), Displays.operationGeneration);
|
||||||
|
const result = JSON.stringify(Displays.monitors.map(monitor => ({
|
||||||
|
name: monitor.name,
|
||||||
|
x: monitor.x,
|
||||||
|
y: monitor.y,
|
||||||
|
primary: monitor.primary
|
||||||
|
})));
|
||||||
|
Displays.monitors = previous;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function applyBad(kind: string): bool {
|
function applyBad(kind: string): bool {
|
||||||
const monitor = Displays.monitors[0];
|
const monitor = Displays.monitors[0];
|
||||||
if (!monitor) return false;
|
if (!monitor) return false;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
// DesktopEntries populates asynchronously. Reading `applications.values`
|
||||||
|
// here rather than calling byId() blind is the documented way to wait for
|
||||||
|
// it -- and is exactly the trap noted in modules/settings/README.md.
|
||||||
|
readonly property var ids: {
|
||||||
|
const out = {};
|
||||||
|
for (const entry of DesktopEntries.applications.values)
|
||||||
|
out[entry.id] = entry;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "dock-pin-test"
|
||||||
|
|
||||||
|
function count(): int { return DesktopEntries.applications.values.length; }
|
||||||
|
|
||||||
|
function resolve(): string {
|
||||||
|
const pinned = DesktopPreferences.get("dockPinned");
|
||||||
|
const missing = [];
|
||||||
|
let first = null;
|
||||||
|
for (const id of pinned) {
|
||||||
|
const entry = DesktopEntries.byId(id);
|
||||||
|
if (!entry) missing.push(id);
|
||||||
|
if (first === null)
|
||||||
|
first = { id: id, found: !!entry, name: entry ? entry.name : "", icon: entry ? entry.icon : "" };
|
||||||
|
}
|
||||||
|
return JSON.stringify({ total: pinned.length, first: first, missing: missing });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
// The production singleton still performs its delayed startup scan. The
|
||||||
|
// harness turns it off before its 2200 ms deadline so each fixture drives
|
||||||
|
// only the state transition it is asserting.
|
||||||
|
Component.onCompleted: Health.startupScanEnabled = false
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "health-test"
|
||||||
|
|
||||||
|
function accept(text: string, generation: int): bool { return Health.consumeSnapshot(text, generation); }
|
||||||
|
function queue(): void { Health.refresh(); Health.refresh(); }
|
||||||
|
function status(): string { return JSON.stringify(Health.diagnostics()); }
|
||||||
|
function repair(id: string): bool { return Health.repair(id, false); }
|
||||||
|
function report(): string { return JSON.stringify(Health.snapshot, null, 2); }
|
||||||
|
function copy(): bool { return Health.copyReport(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
IpcHandler {
|
||||||
|
target: "lock-screen-test"
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
generated: LockScreen.generated,
|
||||||
|
path: LockScreen.path,
|
||||||
|
fallback: LockScreen.fallback,
|
||||||
|
lastError: LockScreen.lastError,
|
||||||
|
busy: LockScreen.busy
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function burst(): void {
|
||||||
|
DesktopPreferences.set("lockBlurLevel", 1);
|
||||||
|
DesktopPreferences.set("lockBlurLevel", 2);
|
||||||
|
DesktopPreferences.set("lockBlurLevel", 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh(): void {
|
||||||
|
LockScreen.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.modules.settings
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string fixtureWallpaper: `${Quickshell.env("HOME")}/Pictures/Wallpapers/faroe_islands.jpg`
|
||||||
|
|
||||||
|
LockScreenPreview {
|
||||||
|
id: screenshotPreview
|
||||||
|
width: 620
|
||||||
|
backgroundMode: "screenshot"
|
||||||
|
blurLevel: 4
|
||||||
|
showClock: true
|
||||||
|
showDate: true
|
||||||
|
showUser: true
|
||||||
|
fadeOnEmpty: false
|
||||||
|
wallpaperPath: root.fixtureWallpaper
|
||||||
|
}
|
||||||
|
|
||||||
|
LockScreenPreview {
|
||||||
|
id: wallpaperPreview
|
||||||
|
width: 620
|
||||||
|
backgroundMode: "wallpaper"
|
||||||
|
blurLevel: 2
|
||||||
|
showClock: false
|
||||||
|
showDate: true
|
||||||
|
showUser: false
|
||||||
|
fadeOnEmpty: true
|
||||||
|
wallpaperPath: root.fixtureWallpaper
|
||||||
|
}
|
||||||
|
|
||||||
|
LockScreenPreview {
|
||||||
|
id: solidPreview
|
||||||
|
width: 620
|
||||||
|
backgroundMode: "solid"
|
||||||
|
blurLevel: 0
|
||||||
|
showClock: true
|
||||||
|
showDate: false
|
||||||
|
showUser: true
|
||||||
|
fadeOnEmpty: false
|
||||||
|
wallpaperPath: root.fixtureWallpaper
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "lock-screen-settings-test"
|
||||||
|
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
screenshot: {
|
||||||
|
mode: screenshotPreview.previewMode,
|
||||||
|
wallpaperVisible: screenshotPreview.wallpaperVisible,
|
||||||
|
blurStrength: screenshotPreview.blurStrength,
|
||||||
|
clockVisible: screenshotPreview.clockVisible,
|
||||||
|
dateVisible: screenshotPreview.dateVisible,
|
||||||
|
userVisible: screenshotPreview.userVisible,
|
||||||
|
passwordVisible: screenshotPreview.passwordVisible
|
||||||
|
},
|
||||||
|
wallpaper: {
|
||||||
|
mode: wallpaperPreview.previewMode,
|
||||||
|
wallpaperVisible: wallpaperPreview.wallpaperVisible,
|
||||||
|
blurStrength: wallpaperPreview.blurStrength,
|
||||||
|
clockVisible: wallpaperPreview.clockVisible,
|
||||||
|
dateVisible: wallpaperPreview.dateVisible,
|
||||||
|
userVisible: wallpaperPreview.userVisible,
|
||||||
|
passwordVisible: wallpaperPreview.passwordVisible
|
||||||
|
},
|
||||||
|
solid: {
|
||||||
|
mode: solidPreview.previewMode,
|
||||||
|
wallpaperVisible: solidPreview.wallpaperVisible,
|
||||||
|
blurStrength: solidPreview.blurStrength,
|
||||||
|
clockVisible: solidPreview.clockVisible,
|
||||||
|
dateVisible: solidPreview.dateVisible,
|
||||||
|
userVisible: solidPreview.userVisible,
|
||||||
|
passwordVisible: solidPreview.passwordVisible
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Exercises Migrations.applyWith against fixture steps and prints a verdict per
|
||||||
|
// case. Run by tests/quickshell/migrations-contract.sh.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// a failing step is not one to discover the behaviour of during an upgrade.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
|
||||||
|
ShellRoot {
|
||||||
|
Component.onCompleted: {
|
||||||
|
const steps = [
|
||||||
|
{ version: 2, describe: "add b", migrate: v => { v.b = (v.a ?? 0) + 1; return v; } },
|
||||||
|
{ version: 3, describe: "add c", migrate: v => { v.c = "three"; return v; } },
|
||||||
|
{ version: 4, describe: "explode", migrate: v => { throw new Error("boom"); } }
|
||||||
|
];
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
// No version at all: a file written before versioning. Stamped, never
|
||||||
|
// migrated.
|
||||||
|
let r = Migrations.applyWith({ a: 1 }, steps, 3, 1);
|
||||||
|
results.unversioned = { v: r.values.schemaVersion, migrated: r.migrated,
|
||||||
|
changed: r.changed, untouched: r.values.b === undefined };
|
||||||
|
|
||||||
|
// Older file: every step above its version runs, in order.
|
||||||
|
r = Migrations.applyWith({ a: 1, schemaVersion: 1 }, steps, 3, 1);
|
||||||
|
results.upgrade = { v: r.values.schemaVersion, b: r.values.b, c: r.values.c,
|
||||||
|
count: r.applied.length, migrated: r.migrated };
|
||||||
|
|
||||||
|
// Already current: nothing runs.
|
||||||
|
r = Migrations.applyWith({ schemaVersion: 3, keep: "me" }, steps, 3, 1);
|
||||||
|
results.current = { v: r.values.schemaVersion, migrated: r.migrated,
|
||||||
|
kept: r.values.keep === "me", b: r.values.b === undefined };
|
||||||
|
|
||||||
|
// From the future: left completely alone, including its unknown keys.
|
||||||
|
r = Migrations.applyWith({ schemaVersion: 9, futureKey: "x" }, steps, 3, 1);
|
||||||
|
results.future = { v: r.values.schemaVersion, migrated: r.migrated,
|
||||||
|
changed: r.changed, kept: r.values.futureKey === "x" };
|
||||||
|
|
||||||
|
// A failing step stops at the last good version rather than losing the
|
||||||
|
// file or skipping past the failure forever.
|
||||||
|
r = Migrations.applyWith({ schemaVersion: 1, a: 5 }, steps, 4, 1);
|
||||||
|
results.failure = { v: r.values.schemaVersion, b: r.values.b, c: r.values.c,
|
||||||
|
kept: r.values.a === 5, count: r.applied.length };
|
||||||
|
|
||||||
|
// Unknown keys survive a migration: rolling back to an older Panama
|
||||||
|
// must not discard a newer version's settings.
|
||||||
|
r = Migrations.applyWith({ schemaVersion: 1, unknownFromFuture: true }, steps, 3, 1);
|
||||||
|
results.preserved = { kept: r.values.unknownFromFuture === true };
|
||||||
|
|
||||||
|
console.info("PANAMA-MIGRATIONS " + JSON.stringify(results));
|
||||||
|
Qt.callLater(() => Qt.quit());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ Pill {
|
|||||||
horizontalPadding: 8
|
horizontalPadding: 8
|
||||||
onActivated: ShellState.toggle("activity")
|
onActivated: ShellState.toggle("activity")
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. Camera, microphone and screen-sharing state is a privacy readout.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("privacy")
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ PanelWindow {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HealthIndicator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
ActivityIndicator {
|
ActivityIndicator {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ Pill {
|
|||||||
horizontalPadding: 8
|
horizontalPadding: 8
|
||||||
onActivated: ShellState.openDateMenu("agenda")
|
onActivated: ShellState.openDateMenu("agenda")
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. The same place the clock leads, since this is the calendar's own reminder.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("datetime")
|
||||||
|
|
||||||
ToolTip.visible: root.hovered && root.visible
|
ToolTip.visible: root.hovered && root.visible
|
||||||
ToolTip.delay: 500
|
ToolTip.delay: 500
|
||||||
ToolTip.text: CalendarAgenda.nextEvent?.summary ?? "Upcoming event"
|
ToolTip.text: CalendarAgenda.nextEvent?.summary ?? "Upcoming event"
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ Pill {
|
|||||||
|
|
||||||
onActivated: ShellState.toggleDateMenu("agenda")
|
onActivated: ShellState.toggleDateMenu("agenda")
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. Timezone and clock format live on Date & Time. The
|
||||||
|
// format toggles are mirrored on Appearance, but someone right-clicking a
|
||||||
|
// clock is far more often after the time itself than its typography.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("datetime")
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: Qt.formatDateTime(clock.date, root.format)
|
text: Qt.formatDateTime(clock.date, root.format)
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// A deliberately absent-until-needed health affordance. Healthy and merely
|
||||||
|
// unconfigured systems leave no ornament or layout residue in the bar.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal activated
|
||||||
|
|
||||||
|
readonly property int issueCount: Health.summary.warnings + Health.summary.errors
|
||||||
|
readonly property color tone: Health.status === "error"
|
||||||
|
? Theme.danger
|
||||||
|
: (Health.status === "warning" ? Theme.warn : "transparent")
|
||||||
|
readonly property string statusText: root.issueCount === 1
|
||||||
|
? "1 system health issue"
|
||||||
|
: root.issueCount + " system health issues"
|
||||||
|
readonly property string accessibleLabel: Health.status === "error"
|
||||||
|
? "System Health: " + root.issueCount + (root.issueCount === 1 ? " issue requires action" : " issues require action")
|
||||||
|
: "System Health: " + root.issueCount + (root.issueCount === 1 ? " issue needs attention" : " issues need attention")
|
||||||
|
readonly property string tooltipText: root.accessibleLabel
|
||||||
|
|
||||||
|
visible: Health.actionable
|
||||||
|
implicitWidth: visible ? content.implicitWidth + 16 : 0
|
||||||
|
implicitHeight: visible ? 24 : 0
|
||||||
|
width: implicitWidth
|
||||||
|
height: implicitHeight
|
||||||
|
radius: 9
|
||||||
|
color: visible ? Theme.alpha(root.tone, 0.09) : "transparent"
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: visible ? Theme.alpha(root.tone, activeFocus ? 0.72 : 0.20) : "transparent"
|
||||||
|
|
||||||
|
activeFocusOnTab: visible
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: root.accessibleLabel
|
||||||
|
Accessible.description: "Open System Health"
|
||||||
|
Accessible.onPressAction: root.activated()
|
||||||
|
|
||||||
|
onActivated: {
|
||||||
|
ShellState.openSettings("services");
|
||||||
|
Health.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: root.activated()
|
||||||
|
Keys.onEnterPressed: root.activated()
|
||||||
|
Keys.onSpacePressed: root.activated()
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: content
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "\u{F0ECD}" // md-shield-alert-outline
|
||||||
|
color: root.tone
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: String(root.issueCount)
|
||||||
|
color: root.tone
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pointer
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.visible
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.activated()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.visible: pointer.containsMouse && root.visible
|
||||||
|
ToolTip.delay: 500
|
||||||
|
ToolTip.text: root.tooltipText
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.services
|
||||||
import qs.widgets
|
import qs.widgets
|
||||||
|
|
||||||
Pill {
|
Pill {
|
||||||
@@ -34,6 +35,9 @@ Pill {
|
|||||||
onActivated: if (root.player?.canTogglePlaying)
|
onActivated: if (root.player?.canTogglePlaying)
|
||||||
root.player.togglePlaying()
|
root.player.togglePlaying()
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. Output device and per-application volume.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("sound")
|
||||||
|
|
||||||
// Scroll up = previous, down = next — the same direction as the workspace
|
// Scroll up = previous, down = next — the same direction as the workspace
|
||||||
// switcher, so the whole bar scrolls consistently.
|
// switcher, so the whole bar scrolls consistently.
|
||||||
onScrolled: delta => {
|
onScrolled: delta => {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ Pill {
|
|||||||
|
|
||||||
onActivated: root.requestQuickSettings()
|
onActivated: root.requestQuickSettings()
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. Network and Bluetooth, which is most of what these glyphs report.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("connectivity")
|
||||||
|
|
||||||
// ── Audio ───────────────────────────────────────────────────────────────
|
// ── Audio ───────────────────────────────────────────────────────────────
|
||||||
// Without a tracker, volume and muted silently read as zero/false.
|
// Without a tracker, volume and muted silently read as zero/false.
|
||||||
PwObjectTracker {
|
PwObjectTracker {
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import qs.widgets
|
|||||||
Pill {
|
Pill {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. Which readouts
|
||||||
|
// appear in the bar, and how often they update.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("appearance")
|
||||||
|
|
||||||
interactive: false
|
interactive: false
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import qs.widgets
|
|||||||
Pill {
|
Pill {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Right-click opens the settings that govern this widget. Location, units and refresh interval are all on Home.
|
||||||
|
onSecondaryActivated: ShellState.openSettings("home")
|
||||||
|
|
||||||
interactive: false
|
interactive: false
|
||||||
visible: Weather.available
|
visible: Weather.available
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Compact, click-through system feedback. Repeated hardware-key presses update
|
||||||
|
// this one surface in place, avoiding a stack of notification cards.
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var modelData: null
|
||||||
|
readonly property var presentation: OsdState.state
|
||||||
|
readonly property bool belongsHere: !OsdState.monitorName || !root.modelData
|
||||||
|
|| root.modelData.name === OsdState.monitorName
|
||||||
|
readonly property bool requestedVisible: OsdState.active && root.belongsHere
|
||||||
|
readonly property int horizontalPadding: 18
|
||||||
|
readonly property int contentGap: 14
|
||||||
|
readonly property int progressWidth: 156
|
||||||
|
readonly property int progressLabelWidth: 48
|
||||||
|
readonly property int messageWidth: Math.min(300, Math.max(52, Math.ceil(labelMetrics.advanceWidth)))
|
||||||
|
readonly property int desiredWidth: root.horizontalPadding * 2 + 24 + root.contentGap
|
||||||
|
+ (root.presentation.progress
|
||||||
|
? root.progressWidth + root.contentGap + root.progressLabelWidth
|
||||||
|
: root.messageWidth)
|
||||||
|
|
||||||
|
screen: root.modelData
|
||||||
|
anchors.bottom: true
|
||||||
|
margins.bottom: Theme.dockIconSize + Theme.dockPadding * 2 + Theme.barGap * 3
|
||||||
|
exclusiveZone: 0
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
implicitWidth: root.desiredWidth
|
||||||
|
implicitHeight: 64
|
||||||
|
color: "transparent"
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "qs-popover-osd"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
property bool mapped: false
|
||||||
|
visible: root.mapped
|
||||||
|
|
||||||
|
onRequestedVisibleChanged: {
|
||||||
|
if (root.requestedVisible) {
|
||||||
|
unmapTimer.stop();
|
||||||
|
root.mapped = true;
|
||||||
|
entrance.restart();
|
||||||
|
} else if (root.mapped) {
|
||||||
|
unmapTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (root.requestedVisible)
|
||||||
|
root.mapped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: unmapTimer
|
||||||
|
interval: Theme.durNormal
|
||||||
|
onTriggered: root.mapped = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: surface
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.popoverRadius
|
||||||
|
color: Theme.alpha(Theme.bgPopover, 0.9)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.1)
|
||||||
|
opacity: root.requestedVisible ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
|
||||||
|
transform: Translate {
|
||||||
|
id: slide
|
||||||
|
y: root.requestedVisible ? 0 : 8
|
||||||
|
Behavior on y {
|
||||||
|
NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrismEdge {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 1
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
inset: parent.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: root.contentGap
|
||||||
|
|
||||||
|
ThemedIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
size: 24
|
||||||
|
icon: root.presentation.icon
|
||||||
|
iconFallback: "dialog-information-symbolic"
|
||||||
|
tint: Theme.fg
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: root.presentation.progress
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: visible ? root.progressWidth : 0
|
||||||
|
height: 8
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.alpha(Theme.fg, 0.14)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: parent.width * root.presentation.ratio
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.accent
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: Theme.durFast; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: root.presentation.progress ? root.progressLabelWidth : root.messageWidth
|
||||||
|
text: root.presentation.label
|
||||||
|
color: Theme.fg
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
horizontalAlignment: root.presentation.progress ? Text.AlignRight : Text.AlignLeft
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: labelMetrics
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
text: root.presentation.label
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: entrance
|
||||||
|
NumberAnimation {
|
||||||
|
target: surface
|
||||||
|
property: "opacity"
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
duration: Theme.durNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: slide
|
||||||
|
property: "y"
|
||||||
|
from: 8
|
||||||
|
to: 0
|
||||||
|
duration: Theme.durNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
function clamp(value, minimum, maximum) {
|
||||||
|
return Math.max(minimum, Math.min(maximum, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function finiteNumber(value, fallback) {
|
||||||
|
var parsed = Number(value);
|
||||||
|
return isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconFor(kind, ratio) {
|
||||||
|
var name = String(kind || "").toLowerCase();
|
||||||
|
if (name === "volume-muted")
|
||||||
|
return "audio-volume-muted-symbolic";
|
||||||
|
if (name === "volume") {
|
||||||
|
if (ratio <= 0)
|
||||||
|
return "audio-volume-muted-symbolic";
|
||||||
|
if (ratio < 0.34)
|
||||||
|
return "audio-volume-low-symbolic";
|
||||||
|
if (ratio < 0.67)
|
||||||
|
return "audio-volume-medium-symbolic";
|
||||||
|
return "audio-volume-high-symbolic";
|
||||||
|
}
|
||||||
|
if (name === "microphone-muted")
|
||||||
|
return "microphone-sensitivity-muted-symbolic";
|
||||||
|
if (name === "microphone")
|
||||||
|
return "audio-input-microphone-symbolic";
|
||||||
|
if (name === "brightness")
|
||||||
|
return "display-brightness-symbolic";
|
||||||
|
if (name === "media-play" || name === "media-playing")
|
||||||
|
return "media-playback-start-symbolic";
|
||||||
|
if (name === "media-pause" || name === "media-paused")
|
||||||
|
return "media-playback-pause-symbolic";
|
||||||
|
if (name === "media-next")
|
||||||
|
return "media-skip-forward-symbolic";
|
||||||
|
if (name === "media-previous")
|
||||||
|
return "media-skip-backward-symbolic";
|
||||||
|
if (name === "media-stop")
|
||||||
|
return "media-playback-stop-symbolic";
|
||||||
|
return name || "dialog-information-symbolic";
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedDuration(value) {
|
||||||
|
var parsed = finiteNumber(value, 1400);
|
||||||
|
return Math.max(0, Math.round(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressState(kind, rawValue, rawMaximum, rawLabel, rawDuration) {
|
||||||
|
var maximum = Math.max(1, finiteNumber(rawMaximum, 100));
|
||||||
|
var value = clamp(finiteNumber(rawValue, 0), 0, maximum);
|
||||||
|
var ratio = value / maximum;
|
||||||
|
var label = String(rawLabel || "");
|
||||||
|
if (!label)
|
||||||
|
label = Math.round(ratio * 100) + "%";
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: String(kind || ""),
|
||||||
|
value: value,
|
||||||
|
maximum: maximum,
|
||||||
|
ratio: ratio,
|
||||||
|
label: label,
|
||||||
|
icon: iconFor(kind, ratio),
|
||||||
|
duration: normalizedDuration(rawDuration),
|
||||||
|
progress: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageState(kind, rawLabel, rawDuration) {
|
||||||
|
return {
|
||||||
|
kind: String(kind || ""),
|
||||||
|
value: 0,
|
||||||
|
maximum: 100,
|
||||||
|
ratio: 0,
|
||||||
|
label: String(rawLabel || ""),
|
||||||
|
icon: iconFor(kind, 0),
|
||||||
|
duration: normalizedDuration(rawDuration),
|
||||||
|
progress: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== "undefined") {
|
||||||
|
module.exports = {
|
||||||
|
clamp: clamp,
|
||||||
|
iconFor: iconFor,
|
||||||
|
progressState: progressState,
|
||||||
|
messageState: messageState
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
module qs.modules.osd
|
||||||
|
Osd 1.0 Osd.qml
|
||||||
@@ -6,6 +6,7 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -16,23 +17,12 @@ Item {
|
|||||||
|
|
||||||
implicitHeight: list.implicitHeight
|
implicitHeight: list.implicitHeight
|
||||||
|
|
||||||
readonly property var nodes: {
|
readonly property var nodes: root.output ? AudioDevices.outputs : AudioDevices.inputs
|
||||||
return Pipewire.nodes.values.filter(n => {
|
|
||||||
if (n.isStream)
|
|
||||||
return false;
|
|
||||||
// Sources have to be filtered on the type flags: !isSink also
|
|
||||||
// matches video nodes (webcams show up here otherwise).
|
|
||||||
return root.output ? n.isSink : (n.type & PwNodeType.AudioSource) === PwNodeType.AudioSource;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property var current: root.output ? Pipewire.defaultAudioSink : Pipewire.defaultAudioSource
|
readonly property var current: AudioDevices.current(root.output)
|
||||||
|
|
||||||
function select(node): void {
|
function select(node): void {
|
||||||
if (root.output)
|
AudioDevices.select(root.output, node)
|
||||||
Pipewire.preferredDefaultAudioSink = node;
|
|
||||||
else
|
|
||||||
Pipewire.preferredDefaultAudioSource = node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollColumn {
|
ScrollColumn {
|
||||||
@@ -52,7 +42,7 @@ Item {
|
|||||||
implicitHeight: 38
|
implicitHeight: 38
|
||||||
icon: root.output ? "audio-speakers-symbolic" : "audio-input-microphone-symbolic"
|
icon: root.output ? "audio-speakers-symbolic" : "audio-input-microphone-symbolic"
|
||||||
iconFallback: "audio-card-symbolic"
|
iconFallback: "audio-card-symbolic"
|
||||||
label: nodeRow.modelData.description || nodeRow.modelData.nickname || nodeRow.modelData.name
|
label: AudioDevices.label(nodeRow.modelData)
|
||||||
selected: nodeRow.modelData === root.current
|
selected: nodeRow.modelData === root.current
|
||||||
onClicked: root.select(nodeRow.modelData)
|
onClicked: root.select(nodeRow.modelData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,44 @@
|
|||||||
// Backlight slider, via brightnessctl.
|
// Brightness, from whichever source this machine actually has.
|
||||||
//
|
//
|
||||||
// This machine drives an external DisplayPort monitor and has no backlight
|
// Two exist and they are not interchangeable:
|
||||||
// class device at all (brightnessctl only reports keyboard/NIC LEDs), so the
|
//
|
||||||
// row removes itself rather than sitting there as a dead control. Probed once
|
// The kernel backlight class, driven by brightnessctl. Laptop panels have it;
|
||||||
// at startup — backlight devices do not appear and disappear.
|
// this desktop does not -- brightnessctl reports only keyboard and NIC LEDs.
|
||||||
|
//
|
||||||
|
// DDC/CI, the channel the buttons on a monitor's bezel drive. That is the
|
||||||
|
// only brightness an external display has, and it is per-monitor.
|
||||||
|
//
|
||||||
|
// A machine may have neither, either, or both, so this renders a row per source
|
||||||
|
// found and removes itself entirely when there are none, rather than sitting
|
||||||
|
// there as a dead control.
|
||||||
|
//
|
||||||
|
// Connector labels appear only when there is more than one row. A single
|
||||||
|
// slider needs no explanation of which screen it dims.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.widgets
|
import qs.widgets
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool available: false
|
property bool backlightAvailable: false
|
||||||
property real value: 0
|
property real backlightValue: 0
|
||||||
|
|
||||||
visible: root.available
|
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
|
||||||
implicitHeight: root.available ? 32 : 0
|
readonly property bool labelled: root.rowCount > 1
|
||||||
|
|
||||||
|
visible: root.rowCount > 0
|
||||||
|
implicitHeight: rows.implicitHeight
|
||||||
|
|
||||||
|
// Probing I2C takes on the order of a second, so it waits until the panel
|
||||||
|
// is actually on screen rather than running at shell startup. Monitors do
|
||||||
|
// not come and go, so once is enough.
|
||||||
|
onVisibleChanged: if (visible && !Brightness.scanned) Brightness.refresh()
|
||||||
|
Component.onCompleted: if (root.visible && !Brightness.scanned) Brightness.refresh()
|
||||||
|
|
||||||
// `-m` is the machine-readable form: name,class,current,percent,max
|
// `-m` is the machine-readable form: name,class,current,percent,max
|
||||||
Process {
|
Process {
|
||||||
@@ -34,36 +54,105 @@ Item {
|
|||||||
const fields = line.split(",");
|
const fields = line.split(",");
|
||||||
if (fields.length < 5 || fields[1] !== "backlight")
|
if (fields.length < 5 || fields[1] !== "backlight")
|
||||||
continue;
|
continue;
|
||||||
root.available = true;
|
root.backlightAvailable = true;
|
||||||
root.value = parseInt(fields[3]) / 100;
|
root.backlightValue = parseInt(fields[3]) / 100;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function apply(v: real): void {
|
function applyBacklight(v: real): void {
|
||||||
root.value = v;
|
root.backlightValue = v;
|
||||||
// Never go fully dark: a 0% backlight looks like a broken shell.
|
// Never go fully dark: a 0% backlight looks like a broken shell.
|
||||||
Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
|
Quickshell.execDetached(["brightnessctl", "-c", "backlight", "-q", "set", Math.max(1, Math.round(v * 100)) + "%"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValueSlider draws its own leading icon, but symbolic icons need
|
Column {
|
||||||
// recolouring to be visible — see ThemedIcon.
|
id: rows
|
||||||
ThemedIcon {
|
|
||||||
id: glyph
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 6
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
spacing: 4
|
||||||
size: 17
|
|
||||||
icon: "display-brightness-symbolic"
|
BrightnessRow {
|
||||||
|
width: rows.width
|
||||||
|
visible: root.backlightAvailable
|
||||||
|
label: "Built-in"
|
||||||
|
value: root.backlightValue
|
||||||
|
onMoved: v => root.applyBacklight(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Brightness.displays
|
||||||
|
|
||||||
|
BrightnessRow {
|
||||||
|
required property var modelData
|
||||||
|
width: rows.width
|
||||||
|
// Hyprland already knows what each output is called, so the
|
||||||
|
// name comes from there rather than from a second source that
|
||||||
|
// could disagree with the Displays page. The connector is the
|
||||||
|
// fallback, so a display is never an unlabelled slider.
|
||||||
|
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
||||||
|
value: modelData.value / 100
|
||||||
|
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ValueSlider {
|
component BrightnessRow: Item {
|
||||||
anchors.left: glyph.right
|
id: row
|
||||||
anchors.leftMargin: 12
|
|
||||||
anchors.right: parent.right
|
property string label: ""
|
||||||
anchors.rightMargin: 32
|
property real value: 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
signal moved(real value)
|
||||||
value: root.value
|
|
||||||
onMoved: v => root.apply(v)
|
implicitHeight: caption.height + control.height
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: caption
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
anchors.top: parent.top
|
||||||
|
visible: root.labelled
|
||||||
|
height: visible ? implicitHeight + 2 : 0
|
||||||
|
text: row.label
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// both a caption and a centre line instead would conflict, and an
|
||||||
|
// anchor set to undefined is not released.
|
||||||
|
Item {
|
||||||
|
id: control
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: caption.bottom
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
// ValueSlider draws its own leading icon, but symbolic icons need
|
||||||
|
// recolouring to be visible — see ThemedIcon.
|
||||||
|
ThemedIcon {
|
||||||
|
id: glyph
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
size: 17
|
||||||
|
icon: "display-brightness-symbolic"
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueSlider {
|
||||||
|
anchors.left: glyph.right
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 32
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
value: row.value
|
||||||
|
onMoved: v => row.moved(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,20 @@ import qs.config
|
|||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
SettingsPage {
|
SettingsPage {
|
||||||
title: "About Panama"
|
id: root
|
||||||
|
|
||||||
|
title: "About"
|
||||||
lede: "A curated Hyprland desktop built around focus, speed, and good taste."
|
lede: "A curated Hyprland desktop built around focus, speed, and good taste."
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!MachineInfo.scanned)
|
||||||
|
MachineInfo.refresh();
|
||||||
|
if (GraphicsDevices.devices.length === 0)
|
||||||
|
GraphicsDevices.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Panama Desktop"
|
title: "Desktop"
|
||||||
subtitle: "Tokyo Night Moon · Prism glass · native tiling"
|
subtitle: "Tokyo Night Moon · Prism glass · native tiling"
|
||||||
|
|
||||||
TextRow {
|
TextRow {
|
||||||
@@ -31,6 +40,59 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What GNOME's About panel answers and this page did not: what am I running
|
||||||
|
// on. Rows come from MachineInfo, except graphics, which is joined from
|
||||||
|
// GraphicsDevices rather than read a second time -- two readouts of the same
|
||||||
|
// hardware are two things that can disagree.
|
||||||
|
//
|
||||||
|
// The join splices the GPU rows in directly after Processor rather than
|
||||||
|
// appending them, so the card reads the way fastfetch does: what the system
|
||||||
|
// is, then what is installed on it, then the hardware underneath. A GPU
|
||||||
|
// listed after "Disk" reads as an afterthought.
|
||||||
|
readonly property var machineRows: {
|
||||||
|
const rows = (MachineInfo.facts ?? []).slice();
|
||||||
|
const gpus = GraphicsDevices.devices ?? [];
|
||||||
|
if (gpus.length === 0)
|
||||||
|
return rows;
|
||||||
|
|
||||||
|
const graphics = gpus.map((device, index) => ({
|
||||||
|
label: gpus.length > 1 ? "Graphics " + (index + 1) : "Graphics",
|
||||||
|
value: device.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
const after = rows.findIndex(row => row.label === "Processor");
|
||||||
|
if (after < 0)
|
||||||
|
return rows.concat(graphics);
|
||||||
|
return rows.slice(0, after + 1).concat(graphics, rows.slice(after + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "This machine"
|
||||||
|
subtitle: "Hardware and system, as the kernel reports it."
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.machineRows
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
id: machineRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: machineRow.modelData.label
|
||||||
|
value: machineRow.modelData.value
|
||||||
|
divider: machineRow.index < root.machineRows.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
visible: MachineInfo.scanned && root.machineRows.length === 0
|
||||||
|
label: "Hardware"
|
||||||
|
detail: "The system did not report anything readable"
|
||||||
|
value: "Unavailable"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Design principles"
|
title: "Design principles"
|
||||||
|
|
||||||
|
|||||||
@@ -25,32 +25,61 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Text"
|
title: "Text"
|
||||||
subtitle: "Scales text in applications. Panama's own panels are drawn at their design size, so the shell is unaffected."
|
subtitle: "Scales text in applications. The shell's own panels are drawn at their design size, so they are unaffected."
|
||||||
|
|
||||||
SliderRow { setting: "textScale"; divider: false }
|
SliderRow { setting: "textScale"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Motion"
|
title: "Motion"
|
||||||
subtitle: "Panama never animates while idle. This affects motion you asked for — windows opening, workspaces sliding, panels appearing."
|
subtitle: "Nothing animates while idle. This affects motion you asked for — windows opening, workspaces sliding, panels appearing."
|
||||||
|
|
||||||
ToggleRow { setting: "animationsEnabled"; divider: false }
|
ToggleRow { setting: "animationsEnabled"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zoom, done by the compositor rather than handed to GNOME. Hyprland has a
|
||||||
|
// real magnifier (cursor:zoom_factor) that follows the pointer, so there is
|
||||||
|
// no reason to send someone to another application for it.
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Contrast"
|
title: "Magnifier"
|
||||||
subtitle: "Unfocused windows can be faded to make the focused one obvious, or left at full strength if that is harder to read."
|
subtitle: "Magnifies the screen around the pointer. Set the magnification to 1× to turn it off."
|
||||||
|
|
||||||
SliderRow { setting: "inactiveOpacity"; divider: false }
|
SliderRow { setting: "magnifierFactor"; zeroLabel: "Off" }
|
||||||
|
ToggleRow { setting: "magnifierRigid"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "System accessibility"
|
title: "Contrast"
|
||||||
subtitle: "Screen reader, zoom, and on-screen keyboard are provided by GNOME's accessibility stack."
|
subtitle: "Unfocused windows can be faded or darkened to make the focused one obvious, or left alone if that is harder to read."
|
||||||
|
|
||||||
|
SliderRow { setting: "inactiveOpacity" }
|
||||||
|
ToggleRow { setting: "dimInactive" }
|
||||||
|
SliderRow { setting: "dimStrength"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// What this session genuinely cannot do, said plainly.
|
||||||
|
//
|
||||||
|
// Sticky keys, slow keys, bounce keys and mouse keys are AccessX, which is
|
||||||
|
// an X11 SERVER feature. XKB under Wayland has no accessx option group at
|
||||||
|
// all, and Hyprland does not implement one. The compositor will accept
|
||||||
|
// "accessx:enable" as a keyboard option and store it, and nothing will ever
|
||||||
|
// act on it -- so there is no switch here, and pointing at GNOME's panel
|
||||||
|
// would be no better, since the daemon that applies those keys is not
|
||||||
|
// running either.
|
||||||
|
SettingsCard {
|
||||||
|
title: "Keyboard accessibility"
|
||||||
|
subtitle: "Sticky, slow and bounce keys are an X11 feature with no Wayland equivalent, so they are unavailable in this session. Offering them here would store a preference that nothing acts on."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Screen reader"
|
||||||
|
detail: "Orca reads the screen aloud and works over the accessibility bus, which does run here"
|
||||||
|
action: "Start Orca"
|
||||||
|
onTriggered: SystemSettings.openApplication("orca")
|
||||||
|
}
|
||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
label: "GNOME accessibility settings"
|
label: "GNOME accessibility settings"
|
||||||
detail: "Opens in GNOME Settings"
|
detail: "For the parts GNOME's own stack still owns"
|
||||||
action: "Open"
|
action: "Open"
|
||||||
divider: false
|
divider: false
|
||||||
onTriggered: SystemSettings.openGnomePanel("universal-access")
|
onTriggered: SystemSettings.openGnomePanel("universal-access")
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import qs.services
|
|||||||
SettingsPage {
|
SettingsPage {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property string expandedPicker: ""
|
||||||
|
|
||||||
title: "Appearance"
|
title: "Appearance"
|
||||||
lede: "Drag anything below. The preview above is your real geometry, to scale."
|
lede: "Tune the Prism shell and the applications that live inside it. The preview above is your real geometry, to scale."
|
||||||
|
|
||||||
header: Component {
|
header: Component {
|
||||||
Column {
|
Column {
|
||||||
@@ -46,10 +48,26 @@ SettingsPage {
|
|||||||
title: "Background"
|
title: "Background"
|
||||||
subtitle: Wallpaper.lastError !== ""
|
subtitle: Wallpaper.lastError !== ""
|
||||||
? Wallpaper.lastError
|
? Wallpaper.lastError
|
||||||
: "Applied to every display. Panama looks in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds."
|
: "Applied to every display. Looked for in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds."
|
||||||
|
|
||||||
|
WallpaperControls {
|
||||||
|
id: wallpaperControls
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
WallpaperPicker {
|
WallpaperPicker {
|
||||||
|
id: wallpapers
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
mode: wallpaperControls.mode
|
||||||
|
selectedOutput: wallpaperControls.selectedOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
visible: wallpapers.hidden > 0 || wallpapers.expanded
|
||||||
|
label: wallpapers.expanded ? "Showing every image" : wallpapers.hidden + " more available"
|
||||||
|
detail: "The grid is kept short so the rest of this page stays reachable"
|
||||||
|
action: wallpapers.expanded ? "Show fewer" : "Show all"
|
||||||
|
onTriggered: wallpapers.expanded = !wallpapers.expanded
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
@@ -64,6 +82,205 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Lock screen"
|
||||||
|
subtitle: LockScreen.lastError !== ""
|
||||||
|
? LockScreen.lastError
|
||||||
|
: "A representative preview of the screen shown before authentication."
|
||||||
|
|
||||||
|
LockScreenPreview {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
ChoiceRow { setting: "lockBackgroundMode" }
|
||||||
|
SliderRow { setting: "lockBlurLevel"; zeroLabel: "Off" }
|
||||||
|
ToggleRow { setting: "lockShowClock" }
|
||||||
|
ToggleRow { setting: "lockShowDate" }
|
||||||
|
ToggleRow { setting: "lockShowUser" }
|
||||||
|
ToggleRow { setting: "lockFadeOnEmpty"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Colour scheme"
|
||||||
|
subtitle: 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."
|
||||||
|
|
||||||
|
ChoiceRow { setting: "colorScheme"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Shell typography"
|
||||||
|
subtitle: Fonts.lastError !== ""
|
||||||
|
? Fonts.lastError
|
||||||
|
: "Every piece of text in the shell. Samples are drawn in the font they name."
|
||||||
|
|
||||||
|
SliderRow { setting: "interfaceFontSize" }
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Interface font"
|
||||||
|
detail: Fonts.interfaceMissing
|
||||||
|
? "Not installed on this machine — fontconfig is substituting something else"
|
||||||
|
: "Used for all shell text"
|
||||||
|
value: Fonts.interfaceFont
|
||||||
|
}
|
||||||
|
|
||||||
|
FontPicker {
|
||||||
|
width: parent.width
|
||||||
|
families: Fonts.interfaceFonts
|
||||||
|
current: Fonts.interfaceFont
|
||||||
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No interface fonts found"
|
||||||
|
onPicked: family => Fonts.setInterface(family)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Icon font"
|
||||||
|
detail: Fonts.iconMissing
|
||||||
|
? "Not installed — the shell's glyphs will not draw correctly"
|
||||||
|
: "Draws the shell's glyphs, so only Nerd Fonts are offered"
|
||||||
|
value: Fonts.iconFont
|
||||||
|
}
|
||||||
|
|
||||||
|
FontPicker {
|
||||||
|
width: parent.width
|
||||||
|
families: Fonts.iconFonts
|
||||||
|
current: Fonts.iconFont
|
||||||
|
emptyText: "No Nerd Fonts installed"
|
||||||
|
onPicked: family => Fonts.setIcon(family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Application typography"
|
||||||
|
subtitle: DesktopStyle.lastError !== ""
|
||||||
|
? DesktopStyle.lastError
|
||||||
|
: "Fonts used by applications that follow the desktop defaults. Open one family at a time to keep the page calm."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Application font"
|
||||||
|
detail: DesktopStyle.applicationFont
|
||||||
|
action: root.expandedPicker === "application-font" ? "Close" : "Choose"
|
||||||
|
onTriggered: root.expandedPicker = root.expandedPicker === "application-font" ? "" : "application-font"
|
||||||
|
}
|
||||||
|
|
||||||
|
FontPicker {
|
||||||
|
visible: root.expandedPicker === "application-font"
|
||||||
|
width: parent.width
|
||||||
|
families: Fonts.interfaceFonts
|
||||||
|
current: DesktopStyle.applicationFont
|
||||||
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No application fonts found"
|
||||||
|
onPicked: family => {
|
||||||
|
if (DesktopStyle.setApplicationFont(family))
|
||||||
|
root.expandedPicker = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderRow { setting: "applicationFontSize" }
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Document font"
|
||||||
|
detail: DesktopStyle.documentFont
|
||||||
|
action: root.expandedPicker === "document-font" ? "Close" : "Choose"
|
||||||
|
onTriggered: root.expandedPicker = root.expandedPicker === "document-font" ? "" : "document-font"
|
||||||
|
}
|
||||||
|
|
||||||
|
FontPicker {
|
||||||
|
visible: root.expandedPicker === "document-font"
|
||||||
|
width: parent.width
|
||||||
|
families: Fonts.interfaceFonts
|
||||||
|
current: DesktopStyle.documentFont
|
||||||
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No document fonts found"
|
||||||
|
onPicked: family => {
|
||||||
|
if (DesktopStyle.setDocumentFont(family))
|
||||||
|
root.expandedPicker = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderRow { setting: "documentFontSize" }
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Monospace font"
|
||||||
|
detail: DesktopStyle.monospaceFont
|
||||||
|
action: root.expandedPicker === "monospace-font" ? "Close" : "Choose"
|
||||||
|
onTriggered: root.expandedPicker = root.expandedPicker === "monospace-font" ? "" : "monospace-font"
|
||||||
|
}
|
||||||
|
|
||||||
|
FontPicker {
|
||||||
|
visible: root.expandedPicker === "monospace-font"
|
||||||
|
width: parent.width
|
||||||
|
families: Fonts.monospaceFonts
|
||||||
|
current: DesktopStyle.monospaceFont
|
||||||
|
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No monospace fonts found"
|
||||||
|
onPicked: family => {
|
||||||
|
if (DesktopStyle.setMonospaceFont(family))
|
||||||
|
root.expandedPicker = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SliderRow { setting: "monospaceFontSize" }
|
||||||
|
ChoiceRow { setting: "fontHinting" }
|
||||||
|
ChoiceRow { setting: "fontAntialiasing"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Icons & pointer"
|
||||||
|
subtitle: DesktopStyle.lastError !== ""
|
||||||
|
? DesktopStyle.lastError
|
||||||
|
: "Installed themes only. The pointer updates in applications and Hyprland together."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Application icons"
|
||||||
|
detail: DesktopStyle.iconTheme
|
||||||
|
action: root.expandedPicker === "icon-theme" ? "Close" : "Choose"
|
||||||
|
enabled: DesktopStyle.catalogLoaded
|
||||||
|
onTriggered: root.expandedPicker = root.expandedPicker === "icon-theme" ? "" : "icon-theme"
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPicker {
|
||||||
|
visible: root.expandedPicker === "icon-theme"
|
||||||
|
width: parent.width
|
||||||
|
items: DesktopStyle.iconThemes
|
||||||
|
current: DesktopStyle.iconTheme
|
||||||
|
placeholder: "Search icon themes"
|
||||||
|
emptyText: DesktopStyle.scanning ? "Reading installed icon themes…" : "No icon themes found"
|
||||||
|
onPicked: value => {
|
||||||
|
if (DesktopStyle.setIconTheme(value))
|
||||||
|
root.expandedPicker = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Pointer theme"
|
||||||
|
detail: DesktopStyle.cursorTheme
|
||||||
|
action: root.expandedPicker === "cursor-theme" ? "Close" : "Choose"
|
||||||
|
enabled: DesktopStyle.catalogLoaded
|
||||||
|
divider: false
|
||||||
|
onTriggered: root.expandedPicker = root.expandedPicker === "cursor-theme" ? "" : "cursor-theme"
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPicker {
|
||||||
|
visible: root.expandedPicker === "cursor-theme"
|
||||||
|
width: parent.width
|
||||||
|
items: DesktopStyle.cursorThemes
|
||||||
|
current: DesktopStyle.cursorTheme
|
||||||
|
placeholder: "Search pointer themes"
|
||||||
|
emptyText: DesktopStyle.scanning ? "Reading installed pointer themes…" : "No pointer themes found"
|
||||||
|
onPicked: value => {
|
||||||
|
if (DesktopStyle.setCursorTheme(value))
|
||||||
|
root.expandedPicker = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Titlebars"
|
||||||
|
subtitle: "For applications that draw GNOME-compatible titlebars. Hyprland itself does not add titlebar buttons to tiled windows."
|
||||||
|
|
||||||
|
ChoiceRow { setting: "titlebarButtonSide" }
|
||||||
|
ToggleRow { setting: "titlebarMaximizeButton" }
|
||||||
|
ChoiceRow { setting: "titlebarDoubleClick"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Windows"
|
title: "Windows"
|
||||||
subtitle: "Spacing and shape of tiled windows. Each change is applied to the compositor and confirmed before it is saved."
|
subtitle: "Spacing and shape of tiled windows. Each change is applied to the compositor and confirmed before it is saved."
|
||||||
@@ -72,7 +289,10 @@ SettingsPage {
|
|||||||
SliderRow { setting: "gapsIn" }
|
SliderRow { setting: "gapsIn" }
|
||||||
SliderRow { setting: "gapsOut" }
|
SliderRow { setting: "gapsOut" }
|
||||||
SliderRow { setting: "borderSize"; zeroLabel: "None" }
|
SliderRow { setting: "borderSize"; zeroLabel: "None" }
|
||||||
SliderRow { setting: "inactiveOpacity"; divider: false }
|
SliderRow { setting: "roundingPower" }
|
||||||
|
SliderRow { setting: "inactiveOpacity" }
|
||||||
|
SliderRow { setting: "activeOpacity" }
|
||||||
|
SliderRow { setting: "fullscreenOpacity"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
@@ -84,6 +304,8 @@ SettingsPage {
|
|||||||
SliderRow { setting: "blurPasses" }
|
SliderRow { setting: "blurPasses" }
|
||||||
ToggleRow { setting: "shadowEnabled" }
|
ToggleRow { setting: "shadowEnabled" }
|
||||||
SliderRow { setting: "shadowRange"; zeroLabel: "None" }
|
SliderRow { setting: "shadowRange"; zeroLabel: "None" }
|
||||||
|
SliderRow { setting: "shadowRenderPower" }
|
||||||
|
ToggleRow { setting: "shadowSharp" }
|
||||||
ToggleRow { setting: "glowEnabled" }
|
ToggleRow { setting: "glowEnabled" }
|
||||||
SliderRow { setting: "glowRange"; zeroLabel: "None" }
|
SliderRow { setting: "glowRange"; zeroLabel: "None" }
|
||||||
ToggleRow { setting: "animationsEnabled"; divider: false }
|
ToggleRow { setting: "animationsEnabled"; divider: false }
|
||||||
@@ -103,14 +325,27 @@ SettingsPage {
|
|||||||
|
|
||||||
ToggleRow { setting: "showCpu" }
|
ToggleRow { setting: "showCpu" }
|
||||||
ToggleRow { setting: "showMemory" }
|
ToggleRow { setting: "showMemory" }
|
||||||
ToggleRow { setting: "showGpu"; divider: false }
|
ToggleRow { setting: "showGpu"; divider: true }
|
||||||
|
// Refresh interval was on the Home page, which split one concept across
|
||||||
|
// two pages -- what the vitals show here, how often they update there.
|
||||||
|
SliderRow { setting: "vitalsIntervalMs"; divider: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing }
|
||||||
|
|
||||||
|
// Only worth asking when there is a choice to make.
|
||||||
|
ChoiceGrid {
|
||||||
|
visible: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing
|
||||||
|
width: parent.width
|
||||||
|
label: "Graphics device"
|
||||||
|
detail: GraphicsDevices.selectionMissing
|
||||||
|
? "The stored device is not present on this machine, so the graphics readout is hidden. Choose one below."
|
||||||
|
: "Which GPU the graphics readout measures."
|
||||||
|
options: GraphicsDevices.devices.map(device => ({
|
||||||
|
value: device.path,
|
||||||
|
label: GraphicsDevices.shortName(device.name)
|
||||||
|
}))
|
||||||
|
current: GraphicsDevices.selectedPath
|
||||||
|
divider: false
|
||||||
|
onPicked: value => GraphicsDevices.select(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Theme"
|
|
||||||
subtitle: "Panama has one curated visual identity rather than a matrix of partially compatible themes. The controls above adjust its parameters — how much space, how soft, how much motion — without replacing it."
|
|
||||||
|
|
||||||
TextRow { label: "Color palette"; detail: "Tokyo Night Moon"; value: "Prism" }
|
|
||||||
TextRow { label: "Interface type"; detail: "Adwaita Sans"; value: "System"; divider: false }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var applications: AudioDevices.applications
|
||||||
|
property bool pipewireReady: Pipewire.ready
|
||||||
|
|
||||||
|
readonly property int rowCount: applicationRepeater.count
|
||||||
|
readonly property string statusText: {
|
||||||
|
if (!root.pipewireReady)
|
||||||
|
return "PipeWire is unavailable";
|
||||||
|
if (root.applications.length === 0)
|
||||||
|
return "Applications playing sound will appear here";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent ? parent.width : 620
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: applicationRepeater
|
||||||
|
model: root.applications
|
||||||
|
|
||||||
|
ApplicationVolumeRow {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: root.width
|
||||||
|
application: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.statusText !== ""
|
||||||
|
text: root.statusText
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
topPadding: 18
|
||||||
|
bottomPadding: 18
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.config
|
||||||
|
import qs.modules.quicksettings
|
||||||
|
import qs.services
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var application
|
||||||
|
|
||||||
|
readonly property real volume: AudioDevices.applicationVolume(root.application)
|
||||||
|
readonly property bool muted: AudioDevices.applicationMuted(root.application)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 620
|
||||||
|
implicitHeight: 78
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
color: Theme.alpha(Theme.fg, 0.025)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.07)
|
||||||
|
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: root.application?.nodes ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemedIcon {
|
||||||
|
id: applicationIcon
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 11
|
||||||
|
size: 20
|
||||||
|
icon: root.application?.icon ?? "audio-x-generic-symbolic"
|
||||||
|
iconFallback: "audio-x-generic-symbolic"
|
||||||
|
tint: Theme.fg
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: applicationIcon.right
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: applicationIcon.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.application?.label ?? "Unknown application"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: (root.application?.nodes?.length ?? 0) > 1
|
||||||
|
text: `${root.application?.nodes?.length ?? 0} audio streams`
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: muteButton
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 6
|
||||||
|
size: 30
|
||||||
|
iconSize: 17
|
||||||
|
icon: root.muted || root.volume <= 0.001
|
||||||
|
? "audio-volume-muted-symbolic"
|
||||||
|
: "audio-volume-high-symbolic"
|
||||||
|
iconFallback: "audio-volume-high-symbolic"
|
||||||
|
onClicked: AudioDevices.setApplicationMuted(root.application, !root.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueSlider {
|
||||||
|
anchors.left: muteButton.right
|
||||||
|
anchors.leftMargin: 7
|
||||||
|
anchors.right: volumeText.left
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.verticalCenter: muteButton.verticalCenter
|
||||||
|
value: root.muted ? 0 : root.volume
|
||||||
|
onMoved: value => AudioDevices.setApplicationVolume(root.application, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: volumeText
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: muteButton.verticalCenter
|
||||||
|
width: 38
|
||||||
|
text: Math.round(root.volume * 100) + "%"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ SettingsPage {
|
|||||||
lede: "Choose what opens your files and links, and what starts with your session."
|
lede: "Choose what opens your files and links, and what starts with your session."
|
||||||
|
|
||||||
property string expandedRole: ""
|
property string expandedRole: ""
|
||||||
|
property bool addingAutostart: false
|
||||||
readonly property var applications: DesktopEntries.applications.values
|
readonly property var applications: DesktopEntries.applications.values
|
||||||
readonly property var roles: [
|
readonly property var roles: [
|
||||||
{ key: "browser", label: "Browser", detail: "Web links and HTML pages", categorySets: [["webbrowser"]], terms: ["web browser", "browser"] },
|
{ key: "browser", label: "Browser", detail: "Web links and HTML pages", categorySets: [["webbrowser"]], terms: ["web browser", "browser"] },
|
||||||
@@ -142,9 +143,64 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GNOME's Search panel, answered honestly.
|
||||||
|
//
|
||||||
|
// It configures which applications provide results in gnome-shell's
|
||||||
|
// overview and which folders are indexed. gnome-shell does not run here,
|
||||||
|
// so those settings would do nothing. Under Panama searching is the
|
||||||
|
// launcher's job, and Vicinae carries its own preferences -- reimplementing
|
||||||
|
// them here would give two places to change one thing.
|
||||||
|
SettingsCard {
|
||||||
|
title: "Search"
|
||||||
|
subtitle: "Applications, files, the calculator, clipboard history, emoji, and open windows are all searched from the launcher."
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Launcher"
|
||||||
|
detail: SystemSettings.vicinaeActive
|
||||||
|
? "Running as a user service"
|
||||||
|
: "Not running — Super+Shift+R opens the fallback launcher"
|
||||||
|
value: "Vicinae"
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Open search"
|
||||||
|
detail: "Three keys open it, because Super+A and Super+R were GNOME's app grid and run dialog"
|
||||||
|
value: "Super+Space"
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Change these shortcuts"
|
||||||
|
detail: "Every launcher chord is rebindable, including clipboard history and emoji"
|
||||||
|
action: "Open keyboard"
|
||||||
|
divider: false
|
||||||
|
onTriggered: ShellState.openSettings("shortcuts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "User autostart"
|
title: "User autostart"
|
||||||
subtitle: "These desktop entries live in your user configuration. Select a row to toggle it."
|
subtitle: "Choose what starts with your session. Entries live in your user configuration, not the compositor."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Add an application"
|
||||||
|
detail: root.addingAutostart
|
||||||
|
? "Search the applications installed on this machine"
|
||||||
|
: "Start another installed application when you sign in"
|
||||||
|
action: root.addingAutostart ? "Close" : "Choose"
|
||||||
|
divider: !root.addingAutostart || DefaultApps.autostartEntries.length > 0
|
||||||
|
enabled: !DefaultApps.busy
|
||||||
|
onTriggered: root.addingAutostart = !root.addingAutostart
|
||||||
|
}
|
||||||
|
|
||||||
|
AutostartAppPicker {
|
||||||
|
visible: root.addingAutostart
|
||||||
|
width: parent.width
|
||||||
|
existing: DefaultApps.autostartEntries.map(entry => entry.id)
|
||||||
|
onPicked: id => {
|
||||||
|
DefaultApps.addAutostart(id);
|
||||||
|
root.addingAutostart = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextRow {
|
TextRow {
|
||||||
visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
|
visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
|
||||||
@@ -175,7 +231,7 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Compositor autostart"
|
title: "Compositor autostart"
|
||||||
subtitle: "Panama starts these from Hyprland configuration. They are read-only here."
|
subtitle: "These are started from the Hyprland configuration. They are read-only here."
|
||||||
|
|
||||||
TextRow {
|
TextRow {
|
||||||
visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0
|
visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.config
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var node: null
|
||||||
|
|
||||||
|
label: "Balance"
|
||||||
|
detail: "Adjust the left and right channels"
|
||||||
|
controlWidth: 270
|
||||||
|
visible: root.available
|
||||||
|
divider: false
|
||||||
|
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: root.node ? [root.node] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelIndex(channel): int {
|
||||||
|
if (!root.node?.audio)
|
||||||
|
return -1;
|
||||||
|
const channels = root.node.audio.channels;
|
||||||
|
for (let index = 0; index < channels.length; index++) {
|
||||||
|
if (channels[index] === channel)
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int leftIndex: root.channelIndex(PwAudioChannel.FrontLeft)
|
||||||
|
readonly property int rightIndex: root.channelIndex(PwAudioChannel.FrontRight)
|
||||||
|
readonly property bool available: !!root.node?.audio
|
||||||
|
&& root.leftIndex >= 0 && root.rightIndex >= 0
|
||||||
|
&& root.node.audio.volumes.length > Math.max(root.leftIndex, root.rightIndex)
|
||||||
|
|
||||||
|
readonly property real position: {
|
||||||
|
if (!root.available)
|
||||||
|
return 0.5;
|
||||||
|
const left = root.node.audio.volumes[root.leftIndex];
|
||||||
|
const right = root.node.audio.volumes[root.rightIndex];
|
||||||
|
const level = Math.max(left, right);
|
||||||
|
if (level <= 0.001)
|
||||||
|
return 0.5;
|
||||||
|
return right >= left ? 0.5 + (1 - left / level) * 0.5
|
||||||
|
: 0.5 - (1 - right / level) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBalance(value: real): void {
|
||||||
|
if (!root.available)
|
||||||
|
return;
|
||||||
|
const next = Array.from(root.node.audio.volumes);
|
||||||
|
const level = Math.max(next[root.leftIndex], next[root.rightIndex], 0.001);
|
||||||
|
if (value < 0.5) {
|
||||||
|
next[root.leftIndex] = level;
|
||||||
|
next[root.rightIndex] = level * value * 2;
|
||||||
|
} else {
|
||||||
|
next[root.leftIndex] = level * (1 - value) * 2;
|
||||||
|
next[root.rightIndex] = level;
|
||||||
|
}
|
||||||
|
root.node.audio.volumes = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueSlider {
|
||||||
|
anchors.fill: parent
|
||||||
|
value: root.position
|
||||||
|
icon: "audio-speakers-symbolic"
|
||||||
|
onMoved: value => root.setBalance(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Adds an installed application to the user's freedesktop autostart directory.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.config
|
||||||
|
import qs.modules.clipboard
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var existing
|
||||||
|
signal picked(string id)
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
function desktopId(entry: var): string {
|
||||||
|
const id = String(entry?.id ?? "");
|
||||||
|
return id.endsWith(".desktop") ? id : id + ".desktop";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var matches: {
|
||||||
|
const needle = search.text.trim().toLowerCase();
|
||||||
|
if (needle === "")
|
||||||
|
return [];
|
||||||
|
const out = [];
|
||||||
|
for (const entry of DesktopEntries.applications.values) {
|
||||||
|
const desktopId = root.desktopId(entry);
|
||||||
|
if (entry.noDisplay || root.existing.indexOf(desktopId) >= 0)
|
||||||
|
continue;
|
||||||
|
const haystack = `${entry.name ?? ""} ${entry.genericName ?? ""} ${desktopId}`.toLowerCase();
|
||||||
|
if (haystack.indexOf(needle) >= 0)
|
||||||
|
out.push(entry);
|
||||||
|
if (out.length >= 8)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchField {
|
||||||
|
id: search
|
||||||
|
width: parent.width
|
||||||
|
placeholder: "Search installed applications"
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.matches
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: candidate
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: String(candidate.modelData.name || root.desktopId(candidate.modelData))
|
||||||
|
detail: String(candidate.modelData.genericName || root.desktopId(candidate.modelData))
|
||||||
|
divider: candidate.index < root.matches.length - 1
|
||||||
|
controlWidth: 86
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Add"
|
||||||
|
onClicked: {
|
||||||
|
root.picked(root.desktopId(candidate.modelData));
|
||||||
|
search.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
visible: search.text.trim() !== "" && root.matches.length === 0
|
||||||
|
label: "No matching applications"
|
||||||
|
detail: "Only installed desktop applications can start with the session"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Bluetooth, at page size.
|
||||||
|
//
|
||||||
|
// Paired devices first, because reconnecting to something you already own is
|
||||||
|
// what you are here for nine times out of ten; discovered devices follow.
|
||||||
|
// Battery is shown where BlueZ reports it, which is the one thing people
|
||||||
|
// routinely open a terminal for.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
function primaryAction(device: var): void {
|
||||||
|
if (device.connected) {
|
||||||
|
device.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (device.paired) {
|
||||||
|
device.connect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device.pair();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateLabel(device: var): string {
|
||||||
|
if (device.pairing)
|
||||||
|
return "Pairing…";
|
||||||
|
if (device.connected)
|
||||||
|
return device.batteryAvailable
|
||||||
|
? `Connected · ${Math.round(device.battery * 100)}% battery`
|
||||||
|
: "Connected";
|
||||||
|
if (device.paired)
|
||||||
|
return "Paired";
|
||||||
|
return device.address || "Not paired";
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Connectivity.bluetoothDevices
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: entry
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
label: entry.modelData.name || entry.modelData.address || "Unknown device"
|
||||||
|
detail: root.stateLabel(entry.modelData)
|
||||||
|
divider: entry.index < Connectivity.bluetoothDevices.length - 1
|
||||||
|
controlWidth: 200
|
||||||
|
activatable: !entry.modelData.pairing
|
||||||
|
onActivated: root.primaryAction(entry.modelData)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 7
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
enabled: !entry.modelData.pairing
|
||||||
|
text: entry.modelData.connected
|
||||||
|
? "Disconnect"
|
||||||
|
: (entry.modelData.paired ? "Connect" : "Pair")
|
||||||
|
onClicked: root.primaryAction(entry.modelData)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: entry.modelData.paired
|
||||||
|
text: "Forget"
|
||||||
|
onClicked: entry.modelData.forget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
width: parent.width
|
||||||
|
visible: Connectivity.bluetoothDevices.length === 0
|
||||||
|
label: !Connectivity.adapter
|
||||||
|
? "No Bluetooth adapter"
|
||||||
|
: (Connectivity.adapter.enabled ? "Looking for devices…" : "Bluetooth is off")
|
||||||
|
detail: Connectivity.adapter && !Connectivity.adapter.enabled
|
||||||
|
? "Turn it on above to discover devices"
|
||||||
|
: "Put the device into pairing mode to make it appear"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,92 +1,192 @@
|
|||||||
|
// Network & Devices.
|
||||||
|
//
|
||||||
|
// Wi-Fi and Bluetooth are handled here rather than delegated. Everything goes
|
||||||
|
// through Quickshell.Networking and Quickshell.Bluetooth -- NetworkManager and
|
||||||
|
// BlueZ over DBus -- and nothing shells out to nmcli or bluetoothctl. That was
|
||||||
|
// the founding requirement for this desktop: never having to drop to a terminal
|
||||||
|
// to join a network.
|
||||||
|
//
|
||||||
|
// Scanning follows this page being on screen. Wi-Fi scanning and especially
|
||||||
|
// Bluetooth discovery hold the radio, and doing either for a list nobody is
|
||||||
|
// looking at is battery and airtime spent on nothing.
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import Quickshell.Networking
|
import Quickshell.Networking
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.modules.quicksettings
|
|
||||||
|
|
||||||
SettingsPage {
|
SettingsPage {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
title: "Network & Devices"
|
|
||||||
lede: "Connect graphically—no terminal workflow required."
|
|
||||||
|
|
||||||
readonly property var wifiDevice: {
|
title: "Network & Devices"
|
||||||
for (const device of Networking.devices.values) {
|
lede: Connectivity.activeNetwork
|
||||||
if (device.type === DeviceType.Wifi)
|
? "Connected to " + Connectivity.activeNetwork.name
|
||||||
return device;
|
: "Wi-Fi, Bluetooth, and the things Fedora owns."
|
||||||
}
|
|
||||||
return null;
|
// Drive the scanners only while this page is the one being shown.
|
||||||
|
Component.onCompleted: {
|
||||||
|
Connectivity.active = true;
|
||||||
|
if (!WifiShare.scanned)
|
||||||
|
WifiShare.refresh();
|
||||||
}
|
}
|
||||||
readonly property var bluetoothAdapter: Bluetooth.defaultAdapter
|
Component.onDestruction: Connectivity.active = false
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Wi‑Fi"
|
title: "Wired"
|
||||||
subtitle: Networking.wifiEnabled ? "Available networks" : "Wireless networking is off"
|
visible: Connectivity.wiredDevice !== null
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Ethernet"
|
||||||
|
detail: Connectivity.wiredDevice ? Connectivity.wiredDevice.name : ""
|
||||||
|
value: Connectivity.wiredDevice && Connectivity.wiredDevice.connected ? "Connected" : "Not connected"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Wi-Fi"
|
||||||
|
// A Wi-Fi switch reading "On" above the words "No Wi-Fi adapter" is a
|
||||||
|
// contradiction; with no radio the card simply does not belong.
|
||||||
|
visible: Connectivity.wifiDevice !== null
|
||||||
|
subtitle: "Networks are re-scanned while this page is open."
|
||||||
|
|
||||||
SettingRow {
|
SettingRow {
|
||||||
label: "Wi‑Fi"
|
label: "Wi-Fi"
|
||||||
detail: root.wifiDevice ? "Managed by NetworkManager" : "No wireless adapter found"
|
detail: Connectivity.wifiEnabled ? "On" : "Off"
|
||||||
controlWidth: 48
|
controlWidth: 48
|
||||||
|
divider: Connectivity.wifiEnabled
|
||||||
|
|
||||||
SettingsToggle {
|
SettingsToggle {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
checked: Networking.wifiEnabled
|
checked: Connectivity.wifiEnabled
|
||||||
enabled: Networking.wifiHardwareEnabled
|
enabled: Connectivity.wifiAvailable
|
||||||
onToggled: value => Networking.wifiEnabled = value
|
onToggled: value => Networking.wifiEnabled = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WifiList {
|
WifiPanel {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
device: root.wifiDevice
|
visible: Connectivity.wifiEnabled
|
||||||
active: true
|
}
|
||||||
maxHeight: 240
|
}
|
||||||
|
|
||||||
|
// Sharing a network by QR, the way GNOME's Wi-Fi panel does. The
|
||||||
|
// alternative is reading a passphrase out loud.
|
||||||
|
//
|
||||||
|
// The image holds the password in machine-readable form, so it is generated
|
||||||
|
// on demand rather than up front, and the helper writes it to tmpfs under
|
||||||
|
// XDG_RUNTIME_DIR instead of anywhere persistent.
|
||||||
|
SettingsCard {
|
||||||
|
visible: Connectivity.wifiDevice !== null && WifiShare.shareable.length > 0
|
||||||
|
title: "Share a network"
|
||||||
|
subtitle: WifiShare.sharing !== ""
|
||||||
|
? "Point a phone's camera at the code to join " + WifiShare.sharing + "."
|
||||||
|
: "Shows a QR code a phone can scan to join, without reading the password out."
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: WifiShare.shareable
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
id: shareRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: shareRow.modelData.ssid
|
||||||
|
detail: WifiShare.sharing === shareRow.modelData.name
|
||||||
|
? "Showing a code below — anyone who can see the screen can join"
|
||||||
|
: "Saved network"
|
||||||
|
action: WifiShare.sharing === shareRow.modelData.name ? "Hide" : "Show code"
|
||||||
|
divider: shareRow.index < WifiShare.shareable.length - 1 || WifiShare.sharing !== ""
|
||||||
|
onTriggered: WifiShare.sharing === shareRow.modelData.name
|
||||||
|
? WifiShare.stopSharing()
|
||||||
|
: WifiShare.share(shareRow.modelData.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionRow {
|
// Drawn at its natural size on a white plate: a QR code inverted or
|
||||||
label: "Advanced network settings"
|
// tinted to match a dark theme is unreliable to scan, and this one has
|
||||||
detail: "VPN, wired profiles, DNS, and connection details"
|
// exactly one job.
|
||||||
divider: false
|
Item {
|
||||||
action: "Open panel"
|
width: parent.width
|
||||||
onTriggered: SystemSettings.openGnomePanel("network")
|
visible: WifiShare.sharing !== "" && WifiShare.imagePath !== ""
|
||||||
|
implicitHeight: visible ? plate.height + 20 : 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: plate
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
y: 10
|
||||||
|
width: 208
|
||||||
|
height: 208
|
||||||
|
radius: 10
|
||||||
|
color: "white"
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 184
|
||||||
|
height: 184
|
||||||
|
smooth: false
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
cache: false
|
||||||
|
source: WifiShare.imagePath !== "" ? "file://" + WifiShare.imagePath : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Bluetooth"
|
title: "Bluetooth"
|
||||||
subtitle: root.bluetoothAdapter?.enabled ? "Nearby and paired devices" : "Bluetooth is off"
|
visible: Connectivity.adapter !== null
|
||||||
|
subtitle: "Discovery runs while this page is open."
|
||||||
|
|
||||||
SettingRow {
|
SettingRow {
|
||||||
label: "Bluetooth"
|
label: "Bluetooth"
|
||||||
detail: root.bluetoothAdapter ? "Pair and reconnect without leaving Settings" : "No Bluetooth adapter found"
|
detail: Connectivity.adapter
|
||||||
|
? (Connectivity.adapter.enabled ? "On" : "Off")
|
||||||
|
: "Unavailable"
|
||||||
controlWidth: 48
|
controlWidth: 48
|
||||||
|
divider: !!(Connectivity.adapter && Connectivity.adapter.enabled)
|
||||||
|
|
||||||
SettingsToggle {
|
SettingsToggle {
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
checked: root.bluetoothAdapter?.enabled ?? false
|
checked: !!(Connectivity.adapter && Connectivity.adapter.enabled)
|
||||||
enabled: root.bluetoothAdapter !== null
|
enabled: Connectivity.adapter !== null
|
||||||
onToggled: value => {
|
onToggled: value => {
|
||||||
if (root.bluetoothAdapter)
|
if (Connectivity.adapter)
|
||||||
root.bluetoothAdapter.enabled = value;
|
Connectivity.adapter.enabled = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BluetoothList {
|
BluetoothPanel {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
active: true
|
visible: !!(Connectivity.adapter && Connectivity.adapter.enabled)
|
||||||
maxHeight: 220
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Owned by Fedora"
|
||||||
|
subtitle: "VPNs, per-connection routing, printers, and online accounts are configured by GNOME's panels, which are installed and searchable."
|
||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
label: "Advanced Bluetooth settings"
|
label: "Network connections"
|
||||||
detail: "Device details and system-level options"
|
detail: "VPN, proxies, and per-connection settings"
|
||||||
|
action: "Open"
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("network")
|
||||||
|
}
|
||||||
|
ActionRow {
|
||||||
|
label: "Printers"
|
||||||
|
action: "Open"
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("printers")
|
||||||
|
}
|
||||||
|
ActionRow {
|
||||||
|
label: "Online accounts"
|
||||||
|
action: "Open"
|
||||||
divider: false
|
divider: false
|
||||||
action: "Open panel"
|
onTriggered: SystemSettings.openGnomePanel("online-accounts")
|
||||||
onTriggered: SystemSettings.openGnomePanel("bluetooth")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,18 +47,16 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Window layout"
|
title: "Window layout"
|
||||||
subtitle: "Panama follows the Forge mental model with native Hyprland tiling."
|
subtitle: "Follows the Forge mental model, with native Hyprland tiling."
|
||||||
|
|
||||||
TextRow {
|
// These were two read-only rows reporting "Tiling" and "Dynamic", which
|
||||||
label: "Layout"
|
// described settings rather than facts -- both are ordinary Hyprland
|
||||||
detail: "Dwindle with preserved split direction"
|
// options that simply had no controls. TextRow's own documentation says
|
||||||
value: "Tiling"
|
// a setting the user could reasonably change does not belong in it.
|
||||||
}
|
ChoiceRow { setting: "windowLayout" }
|
||||||
TextRow {
|
ToggleRow { setting: "preserveSplit" }
|
||||||
label: "Workspace movement"
|
ChoiceRow { setting: "forceSplit" }
|
||||||
detail: "Alt+H / Alt+L, add Shift to move a window"
|
ToggleRow { setting: "windowSnapping" }
|
||||||
value: "Dynamic"
|
|
||||||
}
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
label: "Gaps, corners, and effects"
|
label: "Gaps, corners, and effects"
|
||||||
detail: "Adjusted on the Appearance page, beside a live preview"
|
detail: "Adjusted on the Appearance page, beside a live preview"
|
||||||
@@ -68,6 +66,56 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GNOME's Multitasking panel, in Hyprland's terms.
|
||||||
|
// Only meaningful when the layout above is Master and stack. Hidden
|
||||||
|
// otherwise, because a card of settings that do nothing under the layout
|
||||||
|
// you are actually running is worse than not offering the layout at all.
|
||||||
|
SettingsCard {
|
||||||
|
visible: DesktopPreferences.get("windowLayout") === "master"
|
||||||
|
title: "Master and stack"
|
||||||
|
subtitle: "How the master area behaves. These apply only while the tiling layout above is Master and stack."
|
||||||
|
|
||||||
|
SliderRow { setting: "masterFactor" }
|
||||||
|
ChoiceRow { setting: "masterOrientation" }
|
||||||
|
ChoiceRow { setting: "masterNewStatus" }
|
||||||
|
ToggleRow { setting: "masterNewOnTop"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Window edges"
|
||||||
|
subtitle: "How the pointer grabs a window's border, and how floating windows behave near each other and the screen edge."
|
||||||
|
|
||||||
|
ToggleRow { setting: "resizeOnBorder" }
|
||||||
|
SliderRow { setting: "borderGrabArea"; zeroLabel: "Border only" }
|
||||||
|
ToggleRow { setting: "hoverIconOnBorder" }
|
||||||
|
SliderRow { setting: "snapWindowGap"; zeroLabel: "Touching" }
|
||||||
|
SliderRow { setting: "snapMonitorGap"; zeroLabel: "Touching" }
|
||||||
|
ToggleRow { setting: "snapRespectGaps"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hyprland's own interruptions. Panama turns all four off, which is a
|
||||||
|
// defensible default and was not previously a decision anyone could
|
||||||
|
// reverse without editing looks.lua.
|
||||||
|
SettingsCard {
|
||||||
|
title: "Hyprland notices"
|
||||||
|
subtitle: "Panama hides all of these by default. They are the compositor's own, not Panama's."
|
||||||
|
|
||||||
|
ToggleRow { setting: "hyprlandLogo" }
|
||||||
|
ToggleRow { setting: "hyprlandSplash" }
|
||||||
|
ToggleRow { setting: "hyprlandUpdateNews" }
|
||||||
|
ToggleRow { setting: "hyprlandDonationNag"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Workspaces & focus"
|
||||||
|
subtitle: "Hyprland's workspaces are created and destroyed as you use them, so there is no fixed count to set."
|
||||||
|
|
||||||
|
ToggleRow { setting: "workspaceBackAndForth" }
|
||||||
|
ToggleRow { setting: "allowWorkspaceCycles" }
|
||||||
|
ToggleRow { setting: "focusOnActivate" }
|
||||||
|
ToggleRow { setting: "mouseMoveFocusesMonitor"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Focus"
|
title: "Focus"
|
||||||
|
|
||||||
@@ -111,10 +159,10 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Reset"
|
title: "Reset"
|
||||||
subtitle: "Restores Panama's appearance, dock, clock, focus, and display policy, and clears your Home accessory arrangement. Pinned applications, files, and paired devices are not changed."
|
subtitle: "Restores the appearance, dock, clock, focus, and display policy, and clears your Home accessory arrangement. Pinned applications, files, and paired devices are not changed."
|
||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
label: "Restore Panama defaults"
|
label: "Restore defaults"
|
||||||
detail: "Applies immediately, including to the compositor"
|
detail: "Applies immediately, including to the compositor"
|
||||||
action: "Restore defaults"
|
action: "Restore defaults"
|
||||||
divider: false
|
divider: false
|
||||||
|
|||||||
@@ -50,10 +50,13 @@ 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
|
||||||
|
// around it is light does not read as "your desktop" -- it reads as a
|
||||||
|
// screenshot of someone else's.
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
orientation: Gradient.Vertical
|
orientation: Gradient.Vertical
|
||||||
GradientStop { position: 0.0; color: "#2b3050" }
|
GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" }
|
||||||
GradientStop { position: 1.0; color: "#241f33" }
|
GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bar, so the preview reads as this desktop and not a generic one.
|
// The bar, so the preview reads as this desktop and not a generic one.
|
||||||
@@ -86,9 +89,12 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The real bar carries a clock here, not a name. Standing in with the
|
||||||
|
// actual time keeps the preview a picture of this desktop rather than
|
||||||
|
// a picture of a product.
|
||||||
Text {
|
Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "Panama"
|
text: Qt.formatTime(new Date(), "h:mm")
|
||||||
color: Theme.alpha(Theme.fg, 0.45)
|
color: Theme.alpha(Theme.fg, 0.45)
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: 9
|
font.pixelSize: 9
|
||||||
@@ -203,7 +209,7 @@ Rectangle {
|
|||||||
shadowEnabled: true
|
shadowEnabled: true
|
||||||
shadowColor: win.focused && root.glowOn
|
shadowColor: win.focused && root.glowOn
|
||||||
? Theme.alpha(Theme.accent, 0.5)
|
? Theme.alpha(Theme.accent, 0.5)
|
||||||
: Theme.alpha("#15161e", 0.85)
|
: Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45)
|
||||||
shadowBlur: win.focused && root.glowOn
|
shadowBlur: win.focused && root.glowOn
|
||||||
? Math.min(1.0, root.px(root.glowRange) / 12)
|
? Math.min(1.0, root.px(root.glowRange) / 12)
|
||||||
: Math.min(1.0, root.px(root.shadowRange) / 24)
|
: Math.min(1.0, root.px(root.shadowRange) / 24)
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.widgets
|
||||||
|
import "../../services/DisplayLayout.js" as DisplayLayout
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var displayService
|
||||||
|
property string selectedOutput: ""
|
||||||
|
property var draftLayout: []
|
||||||
|
property bool interactionEnabled: true
|
||||||
|
|
||||||
|
signal selectionRequested(string output)
|
||||||
|
|
||||||
|
implicitHeight: content.implicitHeight
|
||||||
|
readonly property var canvasData: DisplayLayout.canvasRects(
|
||||||
|
root.draftLayout, canvas.width, canvas.height, 18)
|
||||||
|
|
||||||
|
function copied(layout): var {
|
||||||
|
return (layout || []).map(record => Object.assign({}, record));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDraft(): void {
|
||||||
|
root.draftLayout = root.copied(root.displayService.currentLayout());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDraftPosition(output: string, x: real, y: real, snapToEdges: bool): bool {
|
||||||
|
const next = root.copied(root.draftLayout);
|
||||||
|
const record = next.find(candidate => candidate.name === output);
|
||||||
|
if (!record)
|
||||||
|
return false;
|
||||||
|
record.x = Math.round(x);
|
||||||
|
record.y = Math.round(y);
|
||||||
|
root.draftLayout = snapToEdges ? DisplayLayout.snap(next, output, 16) : next;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nudge(output: string, dx: int, dy: int): bool {
|
||||||
|
const record = root.draftLayout.find(candidate => candidate.name === output);
|
||||||
|
return !!record && root.setDraftPosition(output, record.x + dx, record.y + dy, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDraft(): bool {
|
||||||
|
return root.displayService.applyLayout(root.copied(root.draftLayout));
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePrimary(output: string): bool {
|
||||||
|
const next = root.copied(root.draftLayout);
|
||||||
|
if (!next.some(record => record.name === output))
|
||||||
|
return false;
|
||||||
|
for (const record of next)
|
||||||
|
record.primary = record.name === output;
|
||||||
|
root.draftLayout = DisplayLayout.normalize(next);
|
||||||
|
return root.applyDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasSnapshot(): var {
|
||||||
|
return {
|
||||||
|
bounds: root.canvasData.bounds,
|
||||||
|
scale: root.canvasData.scale,
|
||||||
|
rects: root.canvasData.rects.map(record => Object.assign({}, record))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.resetDraft()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.displayService
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onMonitorsChanged(): void {
|
||||||
|
if (!root.displayService.awaitingConfirmation)
|
||||||
|
root.resetDraft();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: content
|
||||||
|
width: parent.width
|
||||||
|
spacing: 11
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: canvas
|
||||||
|
width: parent.width
|
||||||
|
height: root.width >= 620 ? 232 : 190
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
color: Theme.alpha(Theme.bgDark, 0.76)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.07)
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
// A restrained coordinate field makes the topology feel like a
|
||||||
|
// precision instrument without turning it into a technical graph.
|
||||||
|
Repeater {
|
||||||
|
model: 4
|
||||||
|
Rectangle {
|
||||||
|
required property int index
|
||||||
|
y: (index + 1) * canvas.height / 5
|
||||||
|
width: canvas.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.alpha(Theme.fg, 0.025)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.canvasData.rects
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: tile
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
readonly property bool selected: root.selectedOutput === modelData.name
|
||||||
|
readonly property var draft: root.draftLayout.find(
|
||||||
|
record => record.name === modelData.name)
|
||||||
|
|
||||||
|
x: modelData.x
|
||||||
|
y: modelData.y
|
||||||
|
width: Math.max(64, modelData.width)
|
||||||
|
height: Math.max(48, modelData.height)
|
||||||
|
radius: 11
|
||||||
|
color: tile.selected
|
||||||
|
? Theme.alpha(Theme.bgHighlight, 0.92)
|
||||||
|
: Theme.alpha(Theme.bgPanel, hover.hovered ? 0.94 : 0.78)
|
||||||
|
border.width: tile.selected || activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus
|
||||||
|
? Theme.accentSecondary
|
||||||
|
: (tile.selected ? Theme.accent : Theme.alpha(Theme.fg, 0.14))
|
||||||
|
opacity: root.interactionEnabled ? 1 : 0.5
|
||||||
|
activeFocusOnTab: root.interactionEnabled
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: "Move " + tile.modelData.name
|
||||||
|
Accessible.description: tile.draft && tile.draft.primary
|
||||||
|
? "Primary display. Drag or use the arrow keys to move it."
|
||||||
|
: "Drag or use the arrow keys to move this display."
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 2
|
||||||
|
radius: parent.radius - 2
|
||||||
|
visible: tile.selected
|
||||||
|
opacity: 0.24
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
GradientStop { position: 0; color: Theme.accent }
|
||||||
|
GradientStop { position: 1; color: Theme.accentSecondary }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: 11
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: tile.modelData.name
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: tile.draft
|
||||||
|
? `${Math.round(tile.draft.width / tile.draft.scale)} × ${Math.round(tile.draft.height / tile.draft.scale)}`
|
||||||
|
: ""
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 7
|
||||||
|
width: primaryText.implicitWidth + 12
|
||||||
|
height: 20
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
visible: tile.draft && tile.draft.primary
|
||||||
|
color: Theme.alpha(Theme.accent, 0.2)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: primaryText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Primary"
|
||||||
|
color: Theme.accentAlt
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hover
|
||||||
|
enabled: root.interactionEnabled
|
||||||
|
cursorShape: Qt.OpenHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
enabled: root.interactionEnabled
|
||||||
|
onTapped: {
|
||||||
|
root.selectionRequested(tile.modelData.name);
|
||||||
|
tile.forceActiveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: drag
|
||||||
|
target: null
|
||||||
|
enabled: root.interactionEnabled
|
||||||
|
property real initialX: 0
|
||||||
|
property real initialY: 0
|
||||||
|
property bool moved: false
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
const record = root.draftLayout.find(
|
||||||
|
candidate => candidate.name === tile.modelData.name);
|
||||||
|
initialX = record ? record.x : 0;
|
||||||
|
initialY = record ? record.y : 0;
|
||||||
|
moved = false;
|
||||||
|
root.selectionRequested(tile.modelData.name);
|
||||||
|
tile.forceActiveFocus();
|
||||||
|
} else if (moved) {
|
||||||
|
const record = root.draftLayout.find(
|
||||||
|
candidate => candidate.name === tile.modelData.name);
|
||||||
|
if (record) {
|
||||||
|
root.setDraftPosition(tile.modelData.name, record.x, record.y, true);
|
||||||
|
root.applyDraft();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onTranslationChanged: {
|
||||||
|
if (!active || root.canvasData.scale <= 0)
|
||||||
|
return;
|
||||||
|
moved = true;
|
||||||
|
root.setDraftPosition(
|
||||||
|
tile.modelData.name,
|
||||||
|
initialX + translation.x / root.canvasData.scale,
|
||||||
|
initialY + translation.y / root.canvasData.scale,
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (!root.interactionEnabled)
|
||||||
|
return;
|
||||||
|
const step = event.modifiers & Qt.ShiftModifier ? 100 : 10;
|
||||||
|
let handled = true;
|
||||||
|
if (event.key === Qt.Key_Left)
|
||||||
|
root.nudge(tile.modelData.name, -step, 0);
|
||||||
|
else if (event.key === Qt.Key_Right)
|
||||||
|
root.nudge(tile.modelData.name, step, 0);
|
||||||
|
else if (event.key === Qt.Key_Up)
|
||||||
|
root.nudge(tile.modelData.name, 0, -step);
|
||||||
|
else if (event.key === Qt.Key_Down)
|
||||||
|
root.nudge(tile.modelData.name, 0, step);
|
||||||
|
else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter)
|
||||||
|
root.applyDraft();
|
||||||
|
else if (event.key === Qt.Key_Escape)
|
||||||
|
root.resetDraft();
|
||||||
|
else
|
||||||
|
handled = false;
|
||||||
|
event.accepted = handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: Math.max(0, parent.width - identifyButton.width
|
||||||
|
- primaryButton.width - applyButton.width - 24)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.width >= 600
|
||||||
|
? "Drag to arrange · arrows move 10 px · Shift moves 100 px"
|
||||||
|
: "Drag or use the arrow keys"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: identifyButton
|
||||||
|
text: "Identify"
|
||||||
|
enabled: root.interactionEnabled
|
||||||
|
onClicked: root.displayService.identify()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: primaryButton
|
||||||
|
text: "Make primary"
|
||||||
|
enabled: root.interactionEnabled && root.selectedOutput !== ""
|
||||||
|
&& !root.draftLayout.find(record =>
|
||||||
|
record.name === root.selectedOutput)?.primary
|
||||||
|
onClicked: root.makePrimary(root.selectedOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: applyButton
|
||||||
|
text: "Apply"
|
||||||
|
enabled: root.interactionEnabled
|
||||||
|
onClicked: root.applyDraft()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
property var modelData: null
|
||||||
|
readonly property string connector: win.modelData?.name ?? "Display"
|
||||||
|
readonly property int number: Math.max(1,
|
||||||
|
Displays.monitors.findIndex(monitor => monitor.name === win.connector) + 1)
|
||||||
|
readonly property string description: Displays.monitorNamed(win.connector)?.description ?? "Connected display"
|
||||||
|
|
||||||
|
screen: win.modelData
|
||||||
|
visible: Displays.identifying
|
||||||
|
implicitWidth: 260
|
||||||
|
implicitHeight: 172
|
||||||
|
color: "transparent"
|
||||||
|
exclusiveZone: 0
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "qs-display-identify"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.popoverRadius
|
||||||
|
color: Theme.alpha(Theme.bgPopover, 0.96)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.14)
|
||||||
|
|
||||||
|
PrismEdge {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
inset: parent.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - 32
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: String(win.number)
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 68
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: win.connector
|
||||||
|
color: Theme.accentAlt
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: win.description
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Displays.
|
// Displays.
|
||||||
//
|
//
|
||||||
// Resolution, refresh rate, scale, and rotation, plus the gaming display
|
// Resolution, refresh rate, scale, and rotation, plus panel brightness and the
|
||||||
// policy that was already here.
|
// gaming display policy that was already here.
|
||||||
//
|
//
|
||||||
// Every geometry change goes through an apply-then-confirm countdown. This is
|
// Every geometry change goes through an apply-then-confirm countdown. This is
|
||||||
// the one page where a wrong value can leave the screen unreadable or blank,
|
// the one page where a wrong value can leave the screen unreadable or blank,
|
||||||
@@ -32,7 +32,14 @@ SettingsPage {
|
|||||||
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
|
root.selectedOutput = Displays.monitors.length > 0 ? Displays.monitors[0].name : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: root.syncSelectedOutput()
|
// Probing I2C for DDC-capable monitors takes on the order of a second, so
|
||||||
|
// it runs when this page is opened rather than at shell startup. Monitors
|
||||||
|
// do not appear while you are looking at a settings page, so once is enough.
|
||||||
|
Component.onCompleted: {
|
||||||
|
root.syncSelectedOutput();
|
||||||
|
if (!Brightness.scanned)
|
||||||
|
Brightness.refresh();
|
||||||
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: Displays
|
target: Displays
|
||||||
function onMonitorsChanged(): void { root.syncSelectedOutput(); }
|
function onMonitorsChanged(): void { root.syncSelectedOutput(); }
|
||||||
@@ -99,6 +106,20 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: Displays.monitors.length > 1
|
||||||
|
title: "Arrange displays"
|
||||||
|
subtitle: "Drag the screens into place. The primary display anchors the desktop at 0,0."
|
||||||
|
|
||||||
|
DisplayArrangement {
|
||||||
|
width: parent.width
|
||||||
|
displayService: Displays
|
||||||
|
selectedOutput: root.selectedOutput
|
||||||
|
interactionEnabled: !Displays.awaitingConfirmation && !Displays.busy
|
||||||
|
onSelectionRequested: output => root.selectedOutput = output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
visible: Displays.monitors.length > 1
|
visible: Displays.monitors.length > 1
|
||||||
title: "Connected display"
|
title: "Connected display"
|
||||||
@@ -142,7 +163,7 @@ SettingsPage {
|
|||||||
ActionRow {
|
ActionRow {
|
||||||
visible: Displays.isOverridden(root.monitor ? root.monitor.name : "")
|
visible: Displays.isOverridden(root.monitor ? root.monitor.name : "")
|
||||||
label: "Using a custom display setting"
|
label: "Using a custom display setting"
|
||||||
detail: "Forget it to go back to the resolution and scale Panama ships"
|
detail: "Forget it to go back to the shipped resolution and scale"
|
||||||
action: "Forget"
|
action: "Forget"
|
||||||
divider: false
|
divider: false
|
||||||
onTriggered: Displays.forget(root.monitor.name)
|
onTriggered: Displays.forget(root.monitor.name)
|
||||||
@@ -187,9 +208,61 @@ SettingsPage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Night Light"
|
||||||
|
subtitle: NightLight.active
|
||||||
|
? "On now, warming the display to reduce blue light."
|
||||||
|
: "Warms the display in the evening to reduce blue light."
|
||||||
|
|
||||||
|
ToggleRow { setting: "nightLightEnabled" }
|
||||||
|
ToggleRow { setting: "nightLightAutomatic" }
|
||||||
|
TimeOfDayRow { setting: "nightLightFrom" }
|
||||||
|
TimeOfDayRow { setting: "nightLightTo" }
|
||||||
|
SliderRow { setting: "nightLightTemperature"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panel brightness, over DDC/CI.
|
||||||
|
//
|
||||||
|
// This is hardware state rather than a stored preference: the monitor
|
||||||
|
// remembers it, the bezel buttons change it behind Panama's back, and
|
||||||
|
// writing it into settings.json would mean restoring a value the panel had
|
||||||
|
// already moved on from. So there is no schema key here and no SliderRow --
|
||||||
|
// the rows read and write the display directly.
|
||||||
|
SettingsCard {
|
||||||
|
visible: Brightness.available || Brightness.lastError !== ""
|
||||||
|
title: "Brightness"
|
||||||
|
subtitle: Brightness.available
|
||||||
|
? "Sent to the monitor over DDC/CI, the same channel its buttons use."
|
||||||
|
: Brightness.lastError
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Brightness.displays
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: brightnessRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
||||||
|
detail: modelData.connector ? modelData.connector + " · " + modelData.value + "%"
|
||||||
|
: modelData.value + "%"
|
||||||
|
divider: brightnessRow.index < Brightness.displays.length - 1
|
||||||
|
controlWidth: 190
|
||||||
|
|
||||||
|
ValueSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
width: parent.width
|
||||||
|
value: brightnessRow.modelData.value / 100
|
||||||
|
onMoved: v => Brightness.set(brightnessRow.modelData.bus, Math.round(v * 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Gaming display policy"
|
title: "Gaming display policy"
|
||||||
subtitle: "Applied immediately and restored when Panama starts."
|
subtitle: "Applied immediately and restored when the session starts."
|
||||||
|
|
||||||
ToggleRow { setting: "autoHdr" }
|
ToggleRow { setting: "autoHdr" }
|
||||||
ChoiceRow { setting: "vrrPolicy" }
|
ChoiceRow { setting: "vrrPolicy" }
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Choosing a font family.
|
||||||
|
//
|
||||||
|
// Each candidate is rendered IN the font it names. A list of family names set
|
||||||
|
// in the current font tells you nothing about what you are choosing, and the
|
||||||
|
// whole point of picking a typeface is seeing it.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.modules.clipboard
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
property var families: []
|
||||||
|
property string current: ""
|
||||||
|
property string emptyText: "No fonts found"
|
||||||
|
|
||||||
|
signal picked(string family)
|
||||||
|
|
||||||
|
// Filtered rather than listing all 139: a scrolling wall of family names
|
||||||
|
// inside a page that is already scrolling is worse than a search box.
|
||||||
|
readonly property var matches: {
|
||||||
|
const needle = filter.text.trim().toLowerCase();
|
||||||
|
const list = root.families.filter(family =>
|
||||||
|
needle === "" ? true : family.toLowerCase().indexOf(needle) >= 0);
|
||||||
|
// Always show what is currently selected, even when it does not match.
|
||||||
|
if (needle === "" && list.indexOf(root.current) >= 0)
|
||||||
|
return [root.current].concat(list.filter(f => f !== root.current)).slice(0, 12);
|
||||||
|
return list.slice(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchField {
|
||||||
|
id: filter
|
||||||
|
width: parent.width
|
||||||
|
placeholder: "Search installed fonts"
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.matches
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: candidate
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool selected: candidate.modelData === root.current
|
||||||
|
|
||||||
|
label: candidate.modelData
|
||||||
|
detail: candidate.selected ? "Currently in use" : ""
|
||||||
|
controlWidth: 210
|
||||||
|
divider: candidate.index < root.matches.length - 1
|
||||||
|
activatable: !candidate.selected
|
||||||
|
onActivated: root.picked(candidate.modelData)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
elide: Text.ElideRight
|
||||||
|
// The sample is drawn in the candidate family, which is the
|
||||||
|
// entire reason this is a list rather than a text field.
|
||||||
|
text: "Handgloves 0123"
|
||||||
|
font.family: candidate.modelData
|
||||||
|
font.pixelSize: Theme.fontSize + 1
|
||||||
|
color: candidate.selected ? Theme.accent : Theme.fgDim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.matches.length === 0
|
||||||
|
label: root.emptyText
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var check
|
||||||
|
property bool issue: false
|
||||||
|
property bool divider: true
|
||||||
|
property int actionActivationCount: 0
|
||||||
|
signal actionRequested(var check)
|
||||||
|
|
||||||
|
readonly property bool repairWorking: Health.repairingId === root.check.id
|
||||||
|
readonly property bool repairFailed: root.check.status !== "ok"
|
||||||
|
&& Health.lastRepair.checkId === root.check.id
|
||||||
|
&& (Health.lastRepair.accepted === false || Health.lastRepair.exitCode !== 0)
|
||||||
|
|
||||||
|
objectName: `health-check-row:${root.issue ? "issue" : "quiet"}:${root.check.id}`
|
||||||
|
implicitHeight: 62
|
||||||
|
|
||||||
|
// The isolated contract uses the same signal path as a pointer or keyboard
|
||||||
|
// activation instead of calling HealthPage's action handler directly.
|
||||||
|
function activateAction(): bool {
|
||||||
|
if (!actionButton.visible || !actionButton.enabled)
|
||||||
|
return false;
|
||||||
|
root.actionActivationCount += 1;
|
||||||
|
actionButton.clicked();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: string): string {
|
||||||
|
if (status === "ok") return "Healthy";
|
||||||
|
if (status === "warning") return "Needs attention";
|
||||||
|
if (status === "error") return "Action required";
|
||||||
|
return "Not set up";
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(status: string): color {
|
||||||
|
if (status === "ok") return Theme.ok;
|
||||||
|
if (status === "warning") return Theme.warn;
|
||||||
|
if (status === "error") return Theme.danger;
|
||||||
|
return Theme.fgMuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayedStatus(): string {
|
||||||
|
if (root.repairWorking)
|
||||||
|
return "Working…";
|
||||||
|
if (root.repairFailed)
|
||||||
|
return "Repair failed";
|
||||||
|
return root.statusLabel(root.check.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: issueMark
|
||||||
|
|
||||||
|
visible: root.issue
|
||||||
|
width: 7
|
||||||
|
height: 7
|
||||||
|
radius: 4
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 1
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: root.statusColor(root.check.status)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.bgPanel, 0.9)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.issue ? 22 : 0
|
||||||
|
anchors.right: trailing.left
|
||||||
|
anchors.rightMargin: 16
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.check.title
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.check.detail
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: trailing
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
objectName: `health-status-text:${root.issue ? "issue" : "quiet"}:${root.check.id}`
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.displayedStatus()
|
||||||
|
color: root.statusColor(root.check.status)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: actionButton
|
||||||
|
|
||||||
|
objectName: `health-row-action:${root.issue ? "issue" : "quiet"}:${root.check.id}`
|
||||||
|
visible: root.check.action !== undefined
|
||||||
|
text: root.check.action?.label ?? ""
|
||||||
|
enabled: visible && !root.repairWorking && !Health.busy
|
||||||
|
activeFocusOnTab: enabled
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: root.actionRequested(root.check)
|
||||||
|
Keys.onReturnPressed: if (enabled) root.actionRequested(root.check)
|
||||||
|
Keys.onSpacePressed: if (enabled) root.actionRequested(root.check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.divider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: Theme.alpha(Theme.fg, 0.07)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingsPage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
objectName: "system-health-page"
|
||||||
|
|
||||||
|
title: "System Health"
|
||||||
|
lede: "Checks the parts of the desktop this app owns, and explains what needs attention."
|
||||||
|
|
||||||
|
property var pendingConfirmation: null
|
||||||
|
property string instructionTarget: ""
|
||||||
|
|
||||||
|
readonly property var issueChecks: Health.checks.filter(check => check.status === "warning" || check.status === "error")
|
||||||
|
readonly property var groups: [
|
||||||
|
{
|
||||||
|
group: "desktop-foundation",
|
||||||
|
title: "Desktop foundation",
|
||||||
|
subtitle: "Compositor, shell, portals, wallpaper, idle policy, and launcher."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "input-media",
|
||||||
|
title: "Input & media",
|
||||||
|
subtitle: "Sound, clipboard, capture, OCR, and display controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "integrations",
|
||||||
|
title: "Integrations",
|
||||||
|
subtitle: "Only configured integrations affect health."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: "panama-tools",
|
||||||
|
title: "Desktop tools",
|
||||||
|
subtitle: "Tracked links, launcher commands, apps, and inhibitors."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
readonly property var applicationTargets: ({
|
||||||
|
"integration.nextcloud": "nextcloud",
|
||||||
|
"integration.rustdesk": "rustdesk",
|
||||||
|
"integration.kdeconnect": "kdeconnect",
|
||||||
|
"integration.bluebubbles": "bluebubbles"
|
||||||
|
})
|
||||||
|
|
||||||
|
function statusLabel(status: string): string {
|
||||||
|
if (status === "ok") return "Healthy";
|
||||||
|
if (status === "warning") return "Needs attention";
|
||||||
|
if (status === "error") return "Action required";
|
||||||
|
return "Not set up";
|
||||||
|
}
|
||||||
|
|
||||||
|
function checksForGroup(group: string): var {
|
||||||
|
return Health.checks.filter(check => check.group === group
|
||||||
|
&& (check.status === "ok" || check.status === "unconfigured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAction(check: var): void {
|
||||||
|
if (!check || !check.action)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (check.action.kind === "open") {
|
||||||
|
if (check.action.target) {
|
||||||
|
ShellState.openSettings(check.action.target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const application = root.applicationTargets[check.id];
|
||||||
|
if (application)
|
||||||
|
SystemSettings.openApplication(application);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check.action.kind === "instructions") {
|
||||||
|
root.instructionTarget = check.action.target || "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check.action.kind !== "repair")
|
||||||
|
return;
|
||||||
|
if (check.action.confirm) {
|
||||||
|
root.pendingConfirmation = check;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Health.repair(check.id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRepair(): void {
|
||||||
|
const check = root.pendingConfirmation;
|
||||||
|
root.pendingConfirmation = null;
|
||||||
|
if (check)
|
||||||
|
Health.repair(check.id, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function descendants(item: var, prefix: string): var {
|
||||||
|
let matches = [];
|
||||||
|
if (!item)
|
||||||
|
return matches;
|
||||||
|
if (String(item.objectName || "").indexOf(prefix) === 0)
|
||||||
|
matches.push(item);
|
||||||
|
for (const child of item.children || [])
|
||||||
|
matches = matches.concat(root.descendants(child, prefix));
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateRenderedAction(id: string): bool {
|
||||||
|
const suffix = `:${id}`;
|
||||||
|
const rows = root.descendants(root, "health-check-row:").filter(row =>
|
||||||
|
row.visible && String(row.objectName).endsWith(suffix));
|
||||||
|
return rows.length === 1 && rows[0].activateAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderedFocusChain(): var {
|
||||||
|
// Repeater delegates enter Qt's tab chain lazily after their enabled
|
||||||
|
// binding changes at the end of a scan. Touch each rendered action's
|
||||||
|
// real next-focus link before traversing from the first hero control.
|
||||||
|
const renderedActions = root.descendants(root, "health-row-action:").filter(item =>
|
||||||
|
item.visible && item.enabled && item.activeFocusOnTab);
|
||||||
|
for (const action of renderedActions)
|
||||||
|
action.nextItemInFocusChain(true);
|
||||||
|
|
||||||
|
const starts = root.descendants(root, "health-copy-report-button").filter(item => item.visible && item.enabled);
|
||||||
|
if (starts.length !== 1)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
const names = [];
|
||||||
|
const start = starts[0];
|
||||||
|
let current = start;
|
||||||
|
for (let index = 0; index < 128; index++) {
|
||||||
|
const name = String(current.objectName || "");
|
||||||
|
if (name !== "" && names.indexOf(name) < 0)
|
||||||
|
names.push(name);
|
||||||
|
current = current.nextItemInFocusChain(true);
|
||||||
|
if (!current || current === start)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic, read-only fixture seam used by the offscreen contract.
|
||||||
|
function uiDiagnostics(): var {
|
||||||
|
const summaries = root.descendants(root, "health-summary");
|
||||||
|
const rows = root.descendants(root, "health-check-row:").filter(row => row.visible);
|
||||||
|
const checkingLabels = root.descendants(root, "health-checking-label").filter(label => label.visible);
|
||||||
|
const confirmationSheets = root.descendants(root, "health-confirmation-sheet:").filter(sheet => sheet.visible);
|
||||||
|
const emptyGroups = root.descendants(root, "health-empty-group:").filter(label => label.visible);
|
||||||
|
const fedoraHandoffs = root.descendants(root, "health-fedora-handoff:").filter(row => row.visible);
|
||||||
|
return {
|
||||||
|
renderedRows: rows.map(row => {
|
||||||
|
const objectName = String(row.objectName);
|
||||||
|
const parts = objectName.split(":");
|
||||||
|
const statusTexts = root.descendants(row, "health-status-text:").filter(text => text.visible);
|
||||||
|
return {
|
||||||
|
objectName: objectName,
|
||||||
|
id: parts.slice(2).join(":"),
|
||||||
|
section: parts[1],
|
||||||
|
statusText: statusTexts.length === 1 ? statusTexts[0].text : ""
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
summaryHeight: summaries.length > 0 ? summaries[0].height : 0,
|
||||||
|
rowHeights: rows.map(row => row.height),
|
||||||
|
checking: Health.busy,
|
||||||
|
checkingText: checkingLabels.length > 0 ? checkingLabels[0].text : "",
|
||||||
|
focusChain: root.renderedFocusChain(),
|
||||||
|
activatedRows: rows.filter(row => row.actionActivationCount > 0).map(row => String(row.objectName)),
|
||||||
|
emptyQuietGroups: emptyGroups.map(label => String(label.objectName).slice("health-empty-group:".length)),
|
||||||
|
fedoraHandoffs: fedoraHandoffs.map(row => ({
|
||||||
|
id: String(row.objectName).slice("health-fedora-handoff:".length),
|
||||||
|
label: row.label,
|
||||||
|
action: row.action
|
||||||
|
})),
|
||||||
|
confirmationVisible: confirmationSheets.length === 1,
|
||||||
|
confirmationId: confirmationSheets.length === 1
|
||||||
|
? String(confirmationSheets[0].objectName).slice("health-confirmation-sheet:".length)
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: Health.refresh()
|
||||||
|
|
||||||
|
header: Component {
|
||||||
|
Rectangle {
|
||||||
|
objectName: `health-confirmation-sheet:${root.pendingConfirmation ? root.pendingConfirmation.id : ""}`
|
||||||
|
visible: root.pendingConfirmation !== null
|
||||||
|
implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
color: Theme.mix(Theme.bgPanel, Theme.warn, 0.1)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.warn, 0.34)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: confirmRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.margins: 16
|
||||||
|
spacing: 14
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - cancelButton.width - repairButton.width - 28
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.pendingConfirmation
|
||||||
|
? `${root.pendingConfirmation.action.label}?`
|
||||||
|
: "Restart the shell?"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "The desktop chrome will disappear briefly and return when Quickshell restarts."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: cancelButton
|
||||||
|
text: "Cancel"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: root.pendingConfirmation = null
|
||||||
|
Keys.onReturnPressed: root.pendingConfirmation = null
|
||||||
|
Keys.onSpacePressed: root.pendingConfirmation = null
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: repairButton
|
||||||
|
text: root.pendingConfirmation ? root.pendingConfirmation.action.label : "Restart the shell"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: root.confirmRepair()
|
||||||
|
Keys.onReturnPressed: root.confirmRepair()
|
||||||
|
Keys.onSpacePressed: root.confirmRepair()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HealthSummary {}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: root.issueChecks.length > 0
|
||||||
|
title: Health.status === "error" ? "Action required" : "Needs attention"
|
||||||
|
subtitle: Health.status === "error"
|
||||||
|
? "Resolve these items first. Healthy systems remain listed below."
|
||||||
|
: "Nothing here prevents you from using the desktop."
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: issueRows.implicitHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 4
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 25
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 25
|
||||||
|
width: 1
|
||||||
|
visible: root.issueChecks.length > 1
|
||||||
|
color: Theme.alpha(Health.status === "error" ? Theme.danger : Theme.warn, 0.44)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: issueRows
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: issueRepeater
|
||||||
|
model: root.issueChecks
|
||||||
|
|
||||||
|
HealthCheckRow {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: issueRows.width
|
||||||
|
check: modelData
|
||||||
|
issue: true
|
||||||
|
divider: index < issueRepeater.count - 1
|
||||||
|
onActionRequested: check => root.handleAction(check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: root.instructionTarget === "ddc-permissions"
|
||||||
|
title: "External monitor brightness"
|
||||||
|
subtitle: "The monitor reports DDC/CI support, but this session cannot reach the monitor bus."
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: Math.max(instructionCopy.implicitHeight, doneButton.implicitHeight) + 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: instructionCopy
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: doneButton.left
|
||||||
|
anchors.rightMargin: 18
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Reload the installed udev rules and trigger the i2c-dev and DRM devices. Sign out and back in if monitor access is still unavailable."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: doneButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Done"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: root.instructionTarget = ""
|
||||||
|
Keys.onReturnPressed: root.instructionTarget = ""
|
||||||
|
Keys.onSpacePressed: root.instructionTarget = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: groupGrid
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
columns: width >= 700 ? 2 : 1
|
||||||
|
columnSpacing: 16
|
||||||
|
rowSpacing: 16
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.groups
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
required property var modelData
|
||||||
|
readonly property var quietChecks: root.checksForGroup(modelData.group)
|
||||||
|
|
||||||
|
width: groupGrid.columns === 2
|
||||||
|
? (groupGrid.width - groupGrid.columnSpacing) / 2
|
||||||
|
: groupGrid.width
|
||||||
|
title: modelData.title
|
||||||
|
subtitle: modelData.subtitle
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: groupRows
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Text {
|
||||||
|
objectName: `health-empty-group:${modelData.group}`
|
||||||
|
width: parent.width
|
||||||
|
visible: quietChecks.length === 0
|
||||||
|
text: "Items needing attention are listed above."
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
topPadding: 8
|
||||||
|
bottomPadding: 8
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: groupRepeater
|
||||||
|
model: quietChecks
|
||||||
|
|
||||||
|
HealthCheckRow {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: groupRows.width
|
||||||
|
check: modelData
|
||||||
|
divider: index < groupRepeater.count - 1
|
||||||
|
onActionRequested: check => root.handleAction(check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Fedora system settings"
|
||||||
|
subtitle: "These areas remain owned by Fedora and GNOME's mature system panels."
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 42
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: gnomeSettingsButton.left
|
||||||
|
anchors.rightMargin: 18
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Use GNOME Settings for the parts of the system this app does not manage."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: gnomeSettingsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Open GNOME Settings"
|
||||||
|
activeFocusOnTab: true
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: SystemSettings.openGnomePanel("network")
|
||||||
|
Keys.onReturnPressed: SystemSettings.openGnomePanel("network")
|
||||||
|
Keys.onSpacePressed: SystemSettings.openGnomePanel("network")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
objectName: "health-fedora-handoff:users"
|
||||||
|
label: "Users"
|
||||||
|
detail: "Accounts, passwords, and automatic login"
|
||||||
|
action: "Open users"
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("system", "users")
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
objectName: "health-fedora-handoff:sharing"
|
||||||
|
label: "Sharing"
|
||||||
|
detail: "Remote desktop, media sharing, and remote login"
|
||||||
|
action: "Open sharing"
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("sharing")
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
objectName: "health-fedora-handoff:color"
|
||||||
|
label: "Colour profiles"
|
||||||
|
detail: "ICC profiles for displays, printers, and scanners"
|
||||||
|
action: "Open colour"
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("color")
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
objectName: "health-fedora-handoff:wellbeing"
|
||||||
|
label: "Digital wellbeing"
|
||||||
|
detail: "Screen time and break reminders"
|
||||||
|
action: "Open wellbeing"
|
||||||
|
divider: false
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("wellbeing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
objectName: "health-summary"
|
||||||
|
implicitHeight: 126
|
||||||
|
|
||||||
|
readonly property int observationCount: Health.summary.warnings + Health.summary.errors
|
||||||
|
readonly property string heroTitle: {
|
||||||
|
if (Health.diagnosticUnavailable)
|
||||||
|
return "Health check unavailable";
|
||||||
|
if (Health.checks.length === 0)
|
||||||
|
return "Checking the desktop";
|
||||||
|
if (Health.status === "error")
|
||||||
|
return "Action required";
|
||||||
|
if (Health.status === "warning")
|
||||||
|
return "Needs attention";
|
||||||
|
return "Healthy";
|
||||||
|
}
|
||||||
|
readonly property string heroDetail: {
|
||||||
|
if (Health.diagnosticUnavailable)
|
||||||
|
return Health.lastError || "The latest health check could not be completed.";
|
||||||
|
if (Health.checks.length === 0)
|
||||||
|
return "Checking the desktop services, tools, and integrations this app owns.";
|
||||||
|
if (Health.status === "error")
|
||||||
|
return root.observationCount === 1
|
||||||
|
? "One part of the desktop needs action."
|
||||||
|
: `${root.observationCount} parts of the desktop need action.`;
|
||||||
|
if (Health.status === "warning")
|
||||||
|
return root.observationCount === 1
|
||||||
|
? "Your desktop is working. One feature needs a decision."
|
||||||
|
: `Your desktop is working. ${root.observationCount} features need a decision.`;
|
||||||
|
return "Desktop services and tools are working normally.";
|
||||||
|
}
|
||||||
|
readonly property color statusColor: {
|
||||||
|
if (Health.diagnosticUnavailable || Health.status === "error")
|
||||||
|
return Theme.danger;
|
||||||
|
if (Health.status === "warning")
|
||||||
|
return Theme.warn;
|
||||||
|
if (Health.checks.length === 0)
|
||||||
|
return Theme.fgMuted;
|
||||||
|
return Theme.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanTime(): string {
|
||||||
|
const generatedAt = String(Health.snapshot.generatedAt || "");
|
||||||
|
if (generatedAt === "")
|
||||||
|
return "No completed check yet";
|
||||||
|
const date = new Date(generatedAt);
|
||||||
|
if (Number.isNaN(date.getTime()))
|
||||||
|
return "Last check completed";
|
||||||
|
return `Last checked ${date.toLocaleTimeString(Qt.locale(), Locale.ShortFormat)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: 96
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: actions.left
|
||||||
|
anchors.rightMargin: 22
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: Health.diagnosticUnavailable
|
||||||
|
? "DIAGNOSTICS"
|
||||||
|
: root.observationCount > 0
|
||||||
|
? root.observationCount + (root.observationCount === 1 ? " OBSERVATION" : " OBSERVATIONS")
|
||||||
|
: "PANAMA DESKTOP"
|
||||||
|
color: root.statusColor
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
font.letterSpacing: 1.15
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.heroTitle
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 24
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: root.heroDetail
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.scanTime()
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
objectName: "health-checking-label"
|
||||||
|
visible: Health.busy
|
||||||
|
text: "Checking…"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: Health.lastCopyResult !== ""
|
||||||
|
text: Health.lastCopyResult
|
||||||
|
color: Health.lastCopyResult === "Report copied." ? Theme.ok : Theme.warn
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: actions
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: copyButton
|
||||||
|
|
||||||
|
objectName: "health-copy-report-button"
|
||||||
|
visible: !Health.diagnosticUnavailable
|
||||||
|
text: "Copy report"
|
||||||
|
enabled: Health.checks.length > 0
|
||||||
|
activeFocusOnTab: enabled
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: Health.copyReport()
|
||||||
|
Keys.onReturnPressed: if (enabled) Health.copyReport()
|
||||||
|
Keys.onSpacePressed: if (enabled) Health.copyReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: refreshButton
|
||||||
|
|
||||||
|
objectName: "health-refresh-button"
|
||||||
|
text: Health.diagnosticUnavailable ? "Retry" : "Refresh"
|
||||||
|
tone: Health.diagnosticUnavailable ? "normal" : "accent"
|
||||||
|
enabled: !Health.busy
|
||||||
|
activeFocusOnTab: enabled
|
||||||
|
border.width: activeFocus ? 2 : (tone === "accent" ? 0 : 1)
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: Health.refresh()
|
||||||
|
Keys.onReturnPressed: if (enabled) Health.refresh()
|
||||||
|
Keys.onSpacePressed: if (enabled) Health.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,93 +9,61 @@ Rectangle {
|
|||||||
required property string sourceName
|
required property string sourceName
|
||||||
required property int index
|
required property int index
|
||||||
required property bool featured
|
required property bool featured
|
||||||
|
required property bool canMoveEarlier
|
||||||
|
required property bool canMoveLater
|
||||||
|
|
||||||
signal aliasCommitted(string id, string alias)
|
signal aliasCommitted(string id, string alias)
|
||||||
signal removeRequested(string id)
|
signal removeRequested(string id)
|
||||||
signal moveRequested(string id, int targetIndex)
|
signal moveRequested(string id, int targetIndex)
|
||||||
|
|
||||||
readonly property bool dragging: dragHandler.active
|
|
||||||
|
|
||||||
implicitHeight: 108
|
implicitHeight: 108
|
||||||
radius: Theme.cardRadius
|
radius: Theme.cardRadius
|
||||||
color: root.dragging
|
color: Theme.alpha(Theme.bgDark, 0.7)
|
||||||
? Theme.mix(Theme.bgDark, Theme.accent, 0.09)
|
border.width: 1
|
||||||
: Theme.alpha(Theme.bgDark, 0.7)
|
border.color: Theme.alpha(Theme.fg, 0.07)
|
||||||
border.width: root.dragging ? 2 : 1
|
|
||||||
border.color: root.dragging
|
|
||||||
? Theme.alpha(Theme.accent, 0.82)
|
|
||||||
: Theme.alpha(Theme.fg, 0.07)
|
|
||||||
z: root.dragging ? 10 : 0
|
|
||||||
|
|
||||||
transform: Translate {
|
|
||||||
x: root.dragging ? dragHandler.translation.x : 0
|
|
||||||
y: root.dragging ? dragHandler.translation.y : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PrismEdge {
|
PrismEdge {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
inset: root.radius
|
inset: root.radius
|
||||||
opacity: root.dragging ? 0.82 : 0.2
|
opacity: 0.2
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Column {
|
||||||
id: dragHandle
|
id: reorderControls
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 11
|
anchors.leftMargin: 8
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
width: 30
|
width: 30
|
||||||
height: 42
|
spacing: 4
|
||||||
radius: 9
|
|
||||||
activeFocusOnTab: true
|
|
||||||
color: root.dragging || activeFocus
|
|
||||||
? Theme.alpha(Theme.accent, 0.14)
|
|
||||||
: (handleMouse.containsMouse ? Theme.alpha(Theme.fg, 0.09) : Theme.alpha(Theme.fg, 0.045))
|
|
||||||
border.width: activeFocus ? 2 : 1
|
|
||||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.06)
|
|
||||||
|
|
||||||
Text {
|
SettingsButton {
|
||||||
anchors.centerIn: parent
|
width: 30
|
||||||
text: "⠿"
|
height: 29
|
||||||
color: root.dragging ? Theme.accent : Theme.fgDim
|
text: "↑"
|
||||||
font.family: Theme.fontFamily
|
enabled: root.canMoveEarlier
|
||||||
font.pixelSize: 16
|
activeFocusOnTab: enabled
|
||||||
|
onClicked: root.moveRequested(root.favorite.id, root.index - 1)
|
||||||
|
Keys.onReturnPressed: if (enabled) root.moveRequested(root.favorite.id, root.index - 1)
|
||||||
|
Keys.onSpacePressed: if (enabled) root.moveRequested(root.favorite.id, root.index - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
SettingsButton {
|
||||||
id: handleMouse
|
width: 30
|
||||||
anchors.fill: parent
|
height: 29
|
||||||
hoverEnabled: true
|
text: "↓"
|
||||||
acceptedButtons: Qt.NoButton
|
enabled: root.canMoveLater
|
||||||
cursorShape: Qt.SizeAllCursor
|
activeFocusOnTab: enabled
|
||||||
}
|
onClicked: root.moveRequested(root.favorite.id, root.index + 1)
|
||||||
|
Keys.onReturnPressed: if (enabled) root.moveRequested(root.favorite.id, root.index + 1)
|
||||||
DragHandler {
|
Keys.onSpacePressed: if (enabled) root.moveRequested(root.favorite.id, root.index + 1)
|
||||||
id: dragHandler
|
|
||||||
target: null
|
|
||||||
onActiveChanged: {
|
|
||||||
if (!active)
|
|
||||||
root.commitDrag();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (event.key === Qt.Key_Left || event.key === Qt.Key_Up) {
|
|
||||||
root.moveRequested(root.favorite.id, Math.max(0, root.index - 1));
|
|
||||||
event.accepted = true;
|
|
||||||
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Down) {
|
|
||||||
const grid = root.GridView.view;
|
|
||||||
const lastIndex = grid ? grid.count - 1 : root.index;
|
|
||||||
root.moveRequested(root.favorite.id, Math.min(lastIndex, root.index + 1));
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: aliasFrame
|
id: aliasFrame
|
||||||
anchors.left: dragHandle.right
|
anchors.left: reorderControls.right
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: 10
|
||||||
anchors.right: removeButton.left
|
anchors.right: removeButton.left
|
||||||
anchors.rightMargin: 12
|
anchors.rightMargin: 12
|
||||||
@@ -188,16 +156,4 @@ Rectangle {
|
|||||||
Keys.onReturnPressed: root.removeRequested(root.favorite.id)
|
Keys.onReturnPressed: root.removeRequested(root.favorite.id)
|
||||||
Keys.onSpacePressed: root.removeRequested(root.favorite.id)
|
Keys.onSpacePressed: root.removeRequested(root.favorite.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitDrag(): void {
|
|
||||||
const grid = root.GridView.view;
|
|
||||||
if (!grid || grid.count <= 0)
|
|
||||||
return;
|
|
||||||
const centerX = root.x + dragHandler.translation.x + root.width / 2;
|
|
||||||
const centerY = root.y + dragHandler.translation.y + root.height / 2;
|
|
||||||
const modelCount = grid.count;
|
|
||||||
const column = Math.max(0, Math.min(1, Math.floor(centerX / grid.cellWidth)));
|
|
||||||
const row = Math.max(0, Math.floor(centerY / grid.cellHeight));
|
|
||||||
root.moveRequested(root.favorite.id, Math.min(modelCount - 1, row * 2 + column));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ SettingsPage {
|
|||||||
readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening")
|
readonly property string greeting: openedHour < 12 ? "Good morning" : (openedHour < 18 ? "Good afternoon" : "Good evening")
|
||||||
|
|
||||||
title: `${root.greeting}, Gabriel`
|
title: `${root.greeting}, Gabriel`
|
||||||
lede: "Your Panama desktop is configured and ready."
|
lede: "Your desktop is configured and ready."
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: SystemSettings.monitorDescription || "Active display"
|
title: SystemSettings.monitorDescription || "Active display"
|
||||||
@@ -103,16 +103,20 @@ SettingsPage {
|
|||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Weather"
|
title: "Weather"
|
||||||
subtitle: "Local conditions in the date menu"
|
subtitle: "Local conditions in the date menu"
|
||||||
|
TextRow {
|
||||||
|
label: "Location"
|
||||||
|
detail: "Only the search term is sent; the name below is a label kept on this machine"
|
||||||
|
value: Settings.weatherLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationPicker {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
ChoiceRow { setting: "temperatureUnit" }
|
ChoiceRow { setting: "temperatureUnit" }
|
||||||
SliderRow { setting: "weatherRefreshMinutes"; divider: false }
|
SliderRow { setting: "weatherRefreshMinutes"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "System vitals"
|
|
||||||
subtitle: "Processor, memory, and graphics activity in the bar"
|
|
||||||
SliderRow { setting: "vitalsIntervalMs"; divider: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid {
|
Grid {
|
||||||
id: summaryCards
|
id: summaryCards
|
||||||
|
|
||||||
|
|||||||
@@ -43,13 +43,203 @@ SettingsPage {
|
|||||||
return "Home Assistant is unavailable";
|
return "Home Assistant is unavailable";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveHomeAssistantConfig(): void {
|
||||||
|
HomeAssistantConfig.save(
|
||||||
|
homeUrlInput.text,
|
||||||
|
homeEntitiesInput.text,
|
||||||
|
homeTokenInput.text
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: HomeAssistantConfig
|
||||||
|
|
||||||
|
function onConfigurationSaved(): void {
|
||||||
|
homeTokenInput.clear();
|
||||||
|
homeUrlInput.text = HomeAssistantConfig.url;
|
||||||
|
homeEntitiesInput.text = HomeAssistantConfig.entities.join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Home Assistant"
|
title: "Home Assistant"
|
||||||
subtitle: root.homeStatus()
|
subtitle: root.homeStatus()
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Connection"
|
||||||
|
detail: HomeAssistantConfig.tokenConfigured
|
||||||
|
? "A long-lived access token is stored privately"
|
||||||
|
: "Paste a long-lived access token to connect"
|
||||||
|
value: HomeAssistantConfig.configured ? "Configured" : "Not configured"
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Server URL"
|
||||||
|
detail: "The local or remote address of Home Assistant"
|
||||||
|
controlWidth: 330
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
color: Theme.alpha(Theme.fg, 0.07)
|
||||||
|
border.width: homeUrlInput.activeFocus ? 2 : 1
|
||||||
|
border.color: homeUrlInput.activeFocus
|
||||||
|
? Theme.alpha(Theme.accent, 0.55) : "transparent"
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: homeUrlInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
activeFocusOnTab: true
|
||||||
|
text: HomeAssistantConfig.url
|
||||||
|
color: Theme.fg
|
||||||
|
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
||||||
|
selectedTextColor: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: homeUrlInput.text === ""
|
||||||
|
text: "https://homeassistant.local:8123"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font: homeUrlInput.font
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Access token"
|
||||||
|
detail: HomeAssistantConfig.tokenConfigured
|
||||||
|
? "Stored · leave blank to keep it"
|
||||||
|
: "Create one in your Home Assistant profile"
|
||||||
|
controlWidth: 330
|
||||||
|
|
||||||
|
PasswordField {
|
||||||
|
id: homeTokenInput
|
||||||
|
anchors.fill: parent
|
||||||
|
placeholder: HomeAssistantConfig.tokenConfigured
|
||||||
|
? "Stored token" : "Long-lived access token"
|
||||||
|
onAccepted: root.saveHomeAssistantConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 7
|
||||||
|
topPadding: 10
|
||||||
|
bottomPadding: 12
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Light entities"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: "Comma-separated entity IDs. These define the discoverable light catalog."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 72
|
||||||
|
radius: 10
|
||||||
|
color: Theme.alpha(Theme.fg, 0.055)
|
||||||
|
border.width: homeEntitiesInput.activeFocus ? 2 : 1
|
||||||
|
border.color: homeEntitiesInput.activeFocus
|
||||||
|
? Theme.alpha(Theme.accent, 0.55) : Theme.alpha(Theme.fg, 0.06)
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: homeEntitiesInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
activeFocusOnTab: true
|
||||||
|
text: HomeAssistantConfig.entities.join(", ")
|
||||||
|
color: Theme.fg
|
||||||
|
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
||||||
|
selectedTextColor: Theme.fg
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: homeEntitiesInput.text === ""
|
||||||
|
text: "light.living_room, light.kitchen"
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font: homeEntitiesInput.font
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: HomeAssistantConfig.lastError !== ""
|
||||||
|
text: HomeAssistantConfig.lastError
|
||||||
|
color: Theme.danger
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
bottomPadding: 9
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Private configuration"
|
||||||
|
detail: "Saved with owner-only permissions in a private environment file"
|
||||||
|
controlWidth: 216
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: clearTokenButton
|
||||||
|
text: "Clear token"
|
||||||
|
enabled: HomeAssistantConfig.tokenConfigured && !HomeAssistantConfig.busy
|
||||||
|
activeFocusOnTab: enabled
|
||||||
|
border.width: activeFocus ? 2 : 1
|
||||||
|
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||||
|
onClicked: HomeAssistantConfig.clearToken()
|
||||||
|
Keys.onReturnPressed: if (enabled) HomeAssistantConfig.clearToken()
|
||||||
|
Keys.onSpacePressed: if (enabled) HomeAssistantConfig.clearToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: saveHomeConfigButton
|
||||||
|
text: HomeAssistantConfig.busy ? "Saving…" : "Save"
|
||||||
|
tone: "accent"
|
||||||
|
enabled: !HomeAssistantConfig.busy
|
||||||
|
activeFocusOnTab: enabled
|
||||||
|
border.width: activeFocus ? 2 : 0
|
||||||
|
border.color: activeFocus ? Theme.fg : "transparent"
|
||||||
|
onClicked: root.saveHomeAssistantConfig()
|
||||||
|
Keys.onReturnPressed: if (enabled) root.saveHomeAssistantConfig()
|
||||||
|
Keys.onSpacePressed: if (enabled) root.saveHomeAssistantConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingRow {
|
SettingRow {
|
||||||
label: "Light catalog"
|
label: "Light catalog"
|
||||||
detail: "Panama reads light state through the Home Assistant helper."
|
detail: "Light state is read through the Home Assistant helper."
|
||||||
divider: false
|
divider: false
|
||||||
controlWidth: 176
|
controlWidth: 176
|
||||||
|
|
||||||
@@ -122,6 +312,8 @@ SettingsPage {
|
|||||||
})
|
})
|
||||||
sourceName: modelData.sourceName
|
sourceName: modelData.sourceName
|
||||||
featured: index < 4
|
featured: index < 4
|
||||||
|
canMoveEarlier: index > 0
|
||||||
|
canMoveLater: index < favoritesGrid.count - 1
|
||||||
onAliasCommitted: (id, alias) => HomePreferences.setAlias(id, alias)
|
onAliasCommitted: (id, alias) => HomePreferences.setAlias(id, alias)
|
||||||
onMoveRequested: (id, targetIndex) => HomePreferences.move(id, targetIndex)
|
onMoveRequested: (id, targetIndex) => HomePreferences.move(id, targetIndex)
|
||||||
onRemoveRequested: id => HomePreferences.remove(id)
|
onRemoveRequested: id => HomePreferences.remove(id)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Choosing where the weather reading is for.
|
||||||
|
//
|
||||||
|
// A search box rather than latitude and longitude fields: nobody knows their
|
||||||
|
// own coordinates, and a control that demands them is one nobody ever uses. The
|
||||||
|
// coordinates are what actually get stored -- the name is only a label.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.clipboard
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
SearchField {
|
||||||
|
id: query
|
||||||
|
width: parent.width
|
||||||
|
placeholder: "Search for a town or city"
|
||||||
|
onTextChanged: Geocoding.search(query.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Geocoding.results
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: place
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: place.modelData.name
|
||||||
|
detail: [place.modelData.admin, place.modelData.country].filter(part => !!part).join(", ")
|
||||||
|
value: place.modelData.latitude.toFixed(2) + ", " + place.modelData.longitude.toFixed(2)
|
||||||
|
controlWidth: 150
|
||||||
|
divider: place.index < Geocoding.results.length - 1
|
||||||
|
activatable: true
|
||||||
|
onActivated: {
|
||||||
|
if (Geocoding.choose(place.modelData))
|
||||||
|
query.text = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
width: parent.width
|
||||||
|
visible: Geocoding.searching || Geocoding.lastError !== ""
|
||||||
|
label: Geocoding.searching ? "Searching…" : "No result"
|
||||||
|
detail: Geocoding.searching ? "" : Geocoding.lastError
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string backgroundMode: DesktopPreferences.get("lockBackgroundMode")
|
||||||
|
property int blurLevel: DesktopPreferences.get("lockBlurLevel")
|
||||||
|
property bool showClock: DesktopPreferences.get("lockShowClock")
|
||||||
|
property bool showDate: DesktopPreferences.get("lockShowDate")
|
||||||
|
property bool showUser: DesktopPreferences.get("lockShowUser")
|
||||||
|
property bool fadeOnEmpty: DesktopPreferences.get("lockFadeOnEmpty")
|
||||||
|
property bool use24Hour: DesktopPreferences.get("use24Hour")
|
||||||
|
property string wallpaperPath: Wallpaper.active !== ""
|
||||||
|
? Wallpaper.active
|
||||||
|
: (Wallpaper.configured !== "" ? Wallpaper.configured : Wallpaper.shippedPath)
|
||||||
|
|
||||||
|
readonly property string previewMode: root.backgroundMode
|
||||||
|
readonly property real blurStrength: Math.max(0, Math.min(1, root.blurLevel / 5))
|
||||||
|
readonly property bool wallpaperVisible: wallpaper.visible
|
||||||
|
readonly property bool clockVisible: clockLabel.visible
|
||||||
|
readonly property bool dateVisible: dateLabel.visible
|
||||||
|
readonly property bool userVisible: userLabel.visible
|
||||||
|
readonly property bool passwordVisible: passwordField.visible
|
||||||
|
|
||||||
|
width: parent ? parent.width : 620
|
||||||
|
implicitHeight: 230
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
clip: true
|
||||||
|
color: Theme.bg
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.10)
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: wallpaper
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.backgroundMode === "wallpaper"
|
||||||
|
source: visible && root.wallpaperPath !== "" ? root.wallpaperPath : ""
|
||||||
|
asynchronous: true
|
||||||
|
cache: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
sourceSize.width: 960
|
||||||
|
sourceSize.height: 540
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screenshot mode stays representative instead of taking a real desktop
|
||||||
|
// capture inside Settings. The layered window silhouettes communicate the
|
||||||
|
// selected softness without a live ShaderEffect or GPU repaint loop.
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.backgroundMode === "screenshot"
|
||||||
|
color: Theme.bgDark
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width * 0.10
|
||||||
|
y: parent.height * 0.12
|
||||||
|
width: parent.width * 0.45
|
||||||
|
height: parent.height * 0.66
|
||||||
|
radius: 14 + root.blurStrength * 10
|
||||||
|
color: Theme.alpha(Theme.bgHighlight, 0.52 - root.blurStrength * 0.18)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.accent, 0.14)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width * 0.48
|
||||||
|
y: parent.height * 0.24
|
||||||
|
width: parent.width * 0.40
|
||||||
|
height: parent.height * 0.57
|
||||||
|
radius: 14 + root.blurStrength * 10
|
||||||
|
color: Theme.alpha(Theme.bgPanel, 0.64 - root.blurStrength * 0.20)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.accentSecondary, 0.13)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: root.backgroundMode === "wallpaper"
|
||||||
|
? Theme.alpha(Theme.bg, 0.42)
|
||||||
|
: Theme.alpha(Theme.bg, 0.12 + root.blurStrength * 0.16)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: clockLabel
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 26
|
||||||
|
visible: root.showClock
|
||||||
|
text: Qt.formatDateTime(clock.date, root.use24Hour ? "HH:mm" : "h:mm")
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 42
|
||||||
|
font.weight: Font.Light
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: dateLabel
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.top: clockLabel.visible ? clockLabel.bottom : parent.top
|
||||||
|
anchors.topMargin: clockLabel.visible ? 1 : 35
|
||||||
|
visible: root.showDate
|
||||||
|
text: Qt.formatDateTime(clock.date, "dddd, MMMM d")
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: passwordField
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: root.showUser ? 48 : 30
|
||||||
|
visible: !root.fadeOnEmpty
|
||||||
|
width: Math.min(270, parent.width * 0.48)
|
||||||
|
height: 36
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.alpha(Theme.bgPanel, 0.84)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.accent, 0.78)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Password"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.italic: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: userLabel
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 19
|
||||||
|
visible: root.showUser
|
||||||
|
text: Quickshell.env("USER") || "User"
|
||||||
|
color: Theme.alpha(Theme.fg, 0.90)
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemClock {
|
||||||
|
id: clock
|
||||||
|
precision: SystemClock.Minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Mouse & Touchpad.
|
||||||
|
//
|
||||||
|
// GNOME splits pointing devices out of Keyboard, and so does this: the settings
|
||||||
|
// are unrelated, and burying pointer speed under a page called "Shortcuts" is
|
||||||
|
// where it was before. These all reach Hyprland's input section, which until now
|
||||||
|
// could only be changed by editing hypr/input.lua by hand.
|
||||||
|
//
|
||||||
|
// The touchpad card renders only when a touchpad is attached. On a desktop it
|
||||||
|
// would be worse than useless -- every switch on it appears to work, because the
|
||||||
|
// preference is stored and the compositor accepts an option for a device class
|
||||||
|
// with no members, so the user would be toggling settings that can never affect
|
||||||
|
// anything with nothing to say so.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingsPage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: "Mouse & Touchpad"
|
||||||
|
lede: InputDevices.hasTouchpad
|
||||||
|
? "Pointer behaviour for your mouse and touchpad."
|
||||||
|
: "Pointer behaviour. Touchpad settings appear when a touchpad is attached."
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Mouse"
|
||||||
|
subtitle: "Applied to every pointing device that is not a touchpad."
|
||||||
|
|
||||||
|
SliderRow { setting: "pointerSensitivity" }
|
||||||
|
ChoiceRow { setting: "accelProfile" }
|
||||||
|
ToggleRow { setting: "naturalScroll" }
|
||||||
|
SliderRow { setting: "scrollFactor" }
|
||||||
|
ToggleRow { setting: "leftHanded" }
|
||||||
|
ToggleRow {
|
||||||
|
setting: "middleClickPaste"
|
||||||
|
detail: "Paste the primary selection in GTK and native Wayland applications; individual apps may choose not to support it"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: InputDevices.hasTouchpad
|
||||||
|
title: "Touchpad"
|
||||||
|
subtitle: "Separate from the mouse on purpose: libinput keeps them apart, and a touchpad and a mouse usually want to scroll in opposite directions."
|
||||||
|
|
||||||
|
ToggleRow { setting: "touchpadTapToClick" }
|
||||||
|
ToggleRow { setting: "touchpadNaturalScroll" }
|
||||||
|
ToggleRow { setting: "touchpadDisableWhileTyping" }
|
||||||
|
SliderRow { setting: "touchpadScrollFactor" }
|
||||||
|
ChoiceRow { setting: "touchpadDragLock" }
|
||||||
|
ToggleRow { setting: "touchpadMiddleButtonEmulation"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Pointer"
|
||||||
|
|
||||||
|
ChoiceRow { setting: "followMouse" }
|
||||||
|
SliderRow { setting: "cursorInactiveTimeout"; zeroLabel: "Never" }
|
||||||
|
SliderRow { setting: "cursorSize"; divider: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ SettingsPage {
|
|||||||
|
|
||||||
TextRow {
|
TextRow {
|
||||||
label: "Notification history"
|
label: "Notification history"
|
||||||
detail: "Live notifications retained by Panama"
|
detail: "Live notifications retained by the shell"
|
||||||
value: `${Notifs.history.length} items`
|
value: `${Notifs.history.length} items`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +50,89 @@ SettingsPage {
|
|||||||
SliderRow { setting: "maxVisibleToasts"; divider: false }
|
SliderRow { setting: "maxVisibleToasts"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Application rules"
|
||||||
|
subtitle: "Apps appear here after they send a notification."
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
visible: Notifs.applications.length === 0
|
||||||
|
label: "No applications remembered yet"
|
||||||
|
detail: "Application controls will appear after the first notification arrives."
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Notifs.applications
|
||||||
|
|
||||||
|
Column {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property var app: modelData
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: app.name
|
||||||
|
detail: app.id
|
||||||
|
controlWidth: 48
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: Notifs.appRule(app.id).enabled
|
||||||
|
onToggled: value => Notifs.setAppRule(app.id, { enabled: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Show on lock screen"
|
||||||
|
detail: "Allow this app's notifications on the lock screen"
|
||||||
|
controlWidth: 48
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: Notifs.appRule(app.id).showOnLockScreen
|
||||||
|
onToggled: value => Notifs.setAppRule(app.id, { showOnLockScreen: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Show content on lock screen"
|
||||||
|
detail: "Show message details when this app is visible there"
|
||||||
|
divider: false
|
||||||
|
controlWidth: 48
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: Notifs.appRule(app.id).showContentOnLockScreen
|
||||||
|
onToggled: value => Notifs.setAppRule(app.id, { showContentOnLockScreen: value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Focus sessions"
|
title: "Focus sessions"
|
||||||
subtitle: "A focus session binds quiet mode and Caffeine to the current workspace."
|
subtitle: "A focus session binds quiet mode and Caffeine to the current workspace."
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Keep the screen awake"
|
||||||
|
detail: Caffeine.enabled
|
||||||
|
? "The display will not blank or lock while this is on"
|
||||||
|
: "Idle timings on Power & Lock apply normally"
|
||||||
|
controlWidth: 48
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: Caffeine.enabled
|
||||||
|
onToggled: value => Caffeine.enabled = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingRow {
|
SettingRow {
|
||||||
id: durationRow
|
id: durationRow
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Online Accounts.
|
||||||
|
//
|
||||||
|
// GNOME's panel of the same name, except for one step. The accounts are GNOME
|
||||||
|
// Online Accounts objects on D-Bus, the daemon already runs here, and listing,
|
||||||
|
// per-service toggles and removal are all done natively on this page.
|
||||||
|
//
|
||||||
|
// Signing in to an OAuth provider is handed to GNOME's panel, because the
|
||||||
|
// credentials are OAuth tokens and the code that obtains them ships without a
|
||||||
|
// scriptable binding. That is stated on the page rather than hidden behind a
|
||||||
|
// button that looks native, because a hand-off the user does not expect reads
|
||||||
|
// as a bug.
|
||||||
|
//
|
||||||
|
// Accounts needing attention are surfaced first. GOA knows when a token has
|
||||||
|
// expired and nothing outside its own panel says so, which is how an account
|
||||||
|
// quietly stops syncing for weeks.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingsPage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: "Online Accounts"
|
||||||
|
lede: OnlineAccounts.attentionCount > 0
|
||||||
|
? OnlineAccounts.attentionCount + (OnlineAccounts.attentionCount === 1
|
||||||
|
? " account needs you to sign in again."
|
||||||
|
: " accounts need you to sign in again.")
|
||||||
|
: "Accounts your mail, calendar, contacts, and files come from."
|
||||||
|
|
||||||
|
Component.onCompleted: if (!OnlineAccounts.scanned) OnlineAccounts.refresh()
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: !OnlineAccounts.available
|
||||||
|
title: "Accounts unavailable"
|
||||||
|
subtitle: OnlineAccounts.lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: OnlineAccounts.scanned && OnlineAccounts.available && OnlineAccounts.accounts.length === 0
|
||||||
|
title: "No accounts yet"
|
||||||
|
subtitle: "Adding one lets mail, calendar, contacts, and file managers share a single sign-in."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Add an account"
|
||||||
|
detail: "Google, Nextcloud, Microsoft Exchange, IMAP, WebDAV, and Kerberos"
|
||||||
|
action: "Add account"
|
||||||
|
divider: false
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("online-accounts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: OnlineAccounts.accounts
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
id: accountCard
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
title: accountCard.modelData.identity || accountCard.modelData.providerName
|
||||||
|
subtitle: accountCard.modelData.needsAttention
|
||||||
|
? accountCard.modelData.providerName + " · sign-in expired, so this account has stopped syncing"
|
||||||
|
: accountCard.modelData.providerName
|
||||||
|
|
||||||
|
// Only when it is true, because it is the one thing on this page
|
||||||
|
// that needs acting on.
|
||||||
|
ActionRow {
|
||||||
|
visible: accountCard.modelData.needsAttention
|
||||||
|
label: "Sign in again"
|
||||||
|
detail: "Re-authorising uses the provider's own sign-in page, which GNOME's panel hosts"
|
||||||
|
action: "Sign in"
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("online-accounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: accountCard.modelData.services
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: serviceRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: serviceRow.modelData.label
|
||||||
|
detail: serviceRow.modelData.enabled
|
||||||
|
? "Applications using " + serviceRow.modelData.label.toLowerCase() + " can see this account"
|
||||||
|
: "Hidden from applications"
|
||||||
|
controlWidth: 48
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: serviceRow.modelData.enabled
|
||||||
|
onToggled: value => OnlineAccounts.setService(
|
||||||
|
accountCard.modelData.path, serviceRow.modelData.key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Remove this account"
|
||||||
|
detail: "Signs out and removes it from every application that was using it"
|
||||||
|
action: "Remove"
|
||||||
|
divider: false
|
||||||
|
onTriggered: OnlineAccounts.remove(accountCard.modelData.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: OnlineAccounts.available && OnlineAccounts.accounts.length > 0
|
||||||
|
title: "Add another account"
|
||||||
|
subtitle: "Signing in happens on the provider's own page. GNOME's panel hosts that step; everything after it is managed here."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Add an account"
|
||||||
|
detail: "Google, Nextcloud, Microsoft Exchange, IMAP, WebDAV, and Kerberos"
|
||||||
|
action: "Add account"
|
||||||
|
divider: false
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("online-accounts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// A password entry with a reveal toggle.
|
||||||
|
//
|
||||||
|
// Deliberately not the clipboard popover's SearchField with different text: a
|
||||||
|
// Wi-Fi key typed into a field that echoes it is readable by anyone behind you,
|
||||||
|
// and a search glyph in front of a password prompt is simply wrong. Masked by
|
||||||
|
// default, revealable while held, because the reason people want to see it is
|
||||||
|
// to check a character they just typed.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias text: input.text
|
||||||
|
property string placeholder: "Password"
|
||||||
|
property bool revealed: false
|
||||||
|
|
||||||
|
signal accepted
|
||||||
|
|
||||||
|
implicitHeight: 32
|
||||||
|
radius: Theme.pillRadius
|
||||||
|
color: Theme.alpha(Theme.fg, 0.07)
|
||||||
|
// Not left at 0 so the focus ring has something to animate. See the note in
|
||||||
|
// modules/clipboard/SearchField.qml.
|
||||||
|
border.width: 1
|
||||||
|
border.color: input.activeFocus ? Theme.alpha(Theme.accent, 0.55) : "transparent"
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.durFast }
|
||||||
|
}
|
||||||
|
|
||||||
|
function grab(): void {
|
||||||
|
input.forceActiveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(): void {
|
||||||
|
input.text = "";
|
||||||
|
root.revealed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: input
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 13
|
||||||
|
anchors.right: revealButton.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
activeFocusOnTab: true
|
||||||
|
color: Theme.fg
|
||||||
|
selectionColor: Theme.alpha(Theme.accent, 0.5)
|
||||||
|
selectedTextColor: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
echoMode: root.revealed ? TextInput.Normal : TextInput.Password
|
||||||
|
passwordCharacter: "•"
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
onAccepted: root.accepted()
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
visible: input.text === ""
|
||||||
|
text: root.placeholder
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: revealButton
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 5
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 26
|
||||||
|
height: 24
|
||||||
|
radius: 7
|
||||||
|
color: revealHover.hovered ? Theme.alpha(Theme.fg, 0.12) : "transparent"
|
||||||
|
border.width: 0
|
||||||
|
visible: input.text !== ""
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
// Nerd Font eye / eye-slash. fontMono is used for icon glyphs only.
|
||||||
|
text: root.revealed ? "\u{F070}" : "\u{F06E}"
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: root.revealed ? Theme.accent : Theme.fgMuted
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: revealHover
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: root.revealed = !root.revealed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,14 +14,48 @@ import qs.services
|
|||||||
SettingsPage {
|
SettingsPage {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
// Probing the power daemon is a D-Bus round trip, so it happens when the
|
||||||
|
// page opens rather than at shell startup.
|
||||||
|
Component.onCompleted: if (!PowerProfiles.scanned) PowerProfiles.refresh()
|
||||||
|
|
||||||
title: "Power & Lock"
|
title: "Power & Lock"
|
||||||
lede: "When the screen turns off, when the session locks, and whether it ever sleeps."
|
lede: "When the screen turns off, when the session locks, and whether it ever sleeps."
|
||||||
|
|
||||||
|
// The same profiles GNOME's Power panel offers. Not a stored preference --
|
||||||
|
// the daemon owns it, it survives Panama restarts, and anything else on the
|
||||||
|
// system can change it, so a copy here would drift.
|
||||||
|
SettingsCard {
|
||||||
|
visible: PowerProfiles.available || PowerProfiles.lastError !== ""
|
||||||
|
title: "Power profile"
|
||||||
|
subtitle: PowerProfiles.degraded !== ""
|
||||||
|
? "Performance is limited right now: " + PowerProfiles.degraded
|
||||||
|
: (PowerProfiles.available
|
||||||
|
? "Applies to the whole system and persists across sessions."
|
||||||
|
: PowerProfiles.lastError)
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: PowerProfiles.profiles
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: profileRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: PowerProfiles.label(profileRow.modelData)
|
||||||
|
detail: PowerProfiles.detail(profileRow.modelData)
|
||||||
|
value: profileRow.modelData === PowerProfiles.active ? "Active" : ""
|
||||||
|
divider: profileRow.index < PowerProfiles.profiles.length - 1
|
||||||
|
activatable: profileRow.modelData !== PowerProfiles.active && !PowerProfiles.busy
|
||||||
|
onActivated: PowerProfiles.set(profileRow.modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Idle behaviour"
|
title: "Idle behaviour"
|
||||||
subtitle: IdleLock.managed
|
subtitle: IdleLock.managed
|
||||||
? "Panama is managing hypridle. Changes take effect immediately."
|
? "Idle timings are managed here. Changes take effect immediately."
|
||||||
: "hypridle is running Panama's shipped configuration. Turn on management below to make these adjustable."
|
: "hypridle is running its shipped configuration. Turn on management below to make these adjustable."
|
||||||
|
|
||||||
SliderRow { setting: "screenBlankMinutes"; zeroLabel: "Never" }
|
SliderRow { setting: "screenBlankMinutes"; zeroLabel: "Never" }
|
||||||
SliderRow { setting: "lockMinutes"; zeroLabel: "Never" }
|
SliderRow { setting: "lockMinutes"; zeroLabel: "Never" }
|
||||||
@@ -41,10 +75,10 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Management"
|
title: "Management"
|
||||||
subtitle: "Panama generates hypridle's configuration into your state directory and points the service at it with a systemd drop-in. ~/.config/hypr is a symlink into the Panama repository, so the shipped configuration cannot be rewritten in place."
|
subtitle: "hypridle's configuration is generated into your state directory and the service is pointed at it with a systemd drop-in. ~/.config/hypr is a symlink into the configuration repository, so the shipped file cannot be rewritten in place."
|
||||||
|
|
||||||
SettingRow {
|
SettingRow {
|
||||||
label: "Let Panama manage idle timings"
|
label: "Manage idle timings here"
|
||||||
detail: IdleLock.serviceState === "active"
|
detail: IdleLock.serviceState === "active"
|
||||||
? "hypridle is running"
|
? "hypridle is running"
|
||||||
: "hypridle is " + IdleLock.serviceState
|
: "hypridle is " + IdleLock.serviceState
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Privacy & Security.
|
||||||
|
//
|
||||||
|
// GNOME's Privacy panel covers screen lock, camera and microphone access, file
|
||||||
|
// history, trash, and device security. Panama covers the parts it genuinely
|
||||||
|
// owns and is explicit about the parts it does not.
|
||||||
|
//
|
||||||
|
// The file-history and trash settings are the notable omission, and the reason
|
||||||
|
// is worth stating: those are GNOME preferences enforced by gsd-housekeeping,
|
||||||
|
// which is not running in a Hyprland session. Offering switches for them would
|
||||||
|
// store a preference, change nothing, and give no sign of it -- the exact
|
||||||
|
// failure this codebase keeps designing against. So they are delegated by name
|
||||||
|
// rather than reimplemented as controls that lie.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingsPage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: "Privacy & Security"
|
||||||
|
lede: DeviceSecurity.scanned && DeviceSecurity.attentionCount === 0
|
||||||
|
? "Screen lock, device access, and a machine whose security settings all check out."
|
||||||
|
: "Screen lock, which applications can see you, and how this machine is protected."
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!DeviceSecurity.scanned)
|
||||||
|
DeviceSecurity.refresh();
|
||||||
|
if (!Keyring.scanned)
|
||||||
|
Keyring.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Screen lock"
|
||||||
|
subtitle: "The same settings as Power & Lock, which is where the idle timings live."
|
||||||
|
|
||||||
|
SliderRow { setting: "lockMinutes"; zeroLabel: "Never" }
|
||||||
|
ToggleRow { setting: "lockOnSleep"; divider: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The login keyring, which nothing else surfaces.
|
||||||
|
//
|
||||||
|
// It is unlocked at sign-in by PAM, so this card normally just confirms
|
||||||
|
// that. It earns its place on the rare occasion it is not: a locked keyring
|
||||||
|
// breaks saved passwords everywhere at once, and does it without ever
|
||||||
|
// saying the word "keyring" -- you get a mail account that will not
|
||||||
|
// authenticate and a git push that cannot find its key.
|
||||||
|
SettingsCard {
|
||||||
|
visible: Keyring.scanned
|
||||||
|
title: "Saved passwords"
|
||||||
|
subtitle: !Keyring.available
|
||||||
|
? "No secret service is answering, so saved passwords are unavailable."
|
||||||
|
: Keyring.locked
|
||||||
|
? "The login keyring is locked. Saved passwords cannot be read until it is unlocked, and applications that need one will appear to fail for unrelated reasons."
|
||||||
|
: "The login keyring is unlocked, as it is after every normal sign-in."
|
||||||
|
|
||||||
|
// Two rows rather than one with a conditional button: a locked keyring
|
||||||
|
// needs an action, an unlocked one is a statement of fact, and ActionRow
|
||||||
|
// and TextRow already say exactly those two things.
|
||||||
|
ActionRow {
|
||||||
|
visible: Keyring.available && Keyring.locked
|
||||||
|
label: "Login keyring"
|
||||||
|
detail: "Unlock to restore access to stored passwords and keys"
|
||||||
|
action: Keyring.unlocking ? "Waiting…" : "Unlock"
|
||||||
|
enabled: !Keyring.unlocking
|
||||||
|
divider: Keyring.replacementDaemon || Keyring.lastError !== ""
|
||||||
|
onTriggered: Keyring.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
visible: !(Keyring.available && Keyring.locked)
|
||||||
|
label: "Login keyring"
|
||||||
|
detail: Keyring.available
|
||||||
|
? "Unlocked at sign-in by PAM, the same way GNOME does it"
|
||||||
|
: "No secret service is answering on this session"
|
||||||
|
value: Keyring.available ? "Unlocked" : "Unavailable"
|
||||||
|
divider: Keyring.replacementDaemon || Keyring.lastError !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only shown when it is true, because it is a diagnostic rather than a
|
||||||
|
// setting: it means the daemon holding your secrets is not the one PAM
|
||||||
|
// started, so whatever unlocked it will not survive a restart.
|
||||||
|
SettingRow {
|
||||||
|
visible: Keyring.replacementDaemon
|
||||||
|
label: "Keyring service"
|
||||||
|
detail: "The original keyring service was replaced during this session, usually after it crashed. Signing out and back in restores the one PAM unlocks."
|
||||||
|
value: "Replaced"
|
||||||
|
divider: Keyring.lastError !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
visible: Keyring.lastError !== ""
|
||||||
|
label: "Keyring problem"
|
||||||
|
detail: Keyring.lastError
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Camera & microphone"
|
||||||
|
subtitle: PrivacyState.anyActive
|
||||||
|
? "In use right now — the bar shows an indicator whenever this is true."
|
||||||
|
: "Nothing is using your camera or microphone."
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Camera"
|
||||||
|
detail: PrivacyState.cameraActive
|
||||||
|
? "In use by " + (PrivacyState.cameraApp || "an application")
|
||||||
|
: "Not in use"
|
||||||
|
value: PrivacyState.cameraActive ? "Active" : "Idle"
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Microphone"
|
||||||
|
detail: PrivacyState.microphoneActive
|
||||||
|
? "In use by " + (PrivacyState.microphoneApp || "an application")
|
||||||
|
: "Not in use"
|
||||||
|
value: PrivacyState.microphoneActive ? "Active" : "Idle"
|
||||||
|
}
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Screen sharing"
|
||||||
|
detail: PrivacyState.screenSharingActive
|
||||||
|
? "Being shared by " + (PrivacyState.screenSharingApp || "an application")
|
||||||
|
: "Not being shared"
|
||||||
|
value: PrivacyState.screenSharingActive ? "Active" : "Idle"
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Device security"
|
||||||
|
subtitle: DeviceSecurity.attentionCount === 0
|
||||||
|
? "Everything below is in its recommended state."
|
||||||
|
: DeviceSecurity.attentionCount + " item"
|
||||||
|
+ (DeviceSecurity.attentionCount === 1 ? "" : "s") + " below may deserve attention."
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: DeviceSecurity.facts
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
id: factRow
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
label: factRow.modelData.label
|
||||||
|
detail: factRow.modelData.detail
|
||||||
|
value: factRow.modelData.value
|
||||||
|
divider: factRow.index < DeviceSecurity.facts.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Owned by Fedora"
|
||||||
|
subtitle: "File history and trash retention are GNOME preferences, applied by a housekeeping service that does not run in a Hyprland session. They are not offered as switches here, because storing that preference would change nothing."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "File history & trash"
|
||||||
|
detail: "Opens GNOME Settings, which owns these"
|
||||||
|
action: "Open privacy"
|
||||||
|
divider: false
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("privacy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,36 @@
|
|||||||
# Panama Settings
|
# Settings
|
||||||
|
|
||||||
The control centre for everything Panama owns. Anything the system owns —
|
The control centre 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 labelled as
|
||||||
such rather than half-reimplemented.
|
such rather than half-reimplemented.
|
||||||
|
|
||||||
|
## System Health
|
||||||
|
|
||||||
|
The stable internal `services` route renders **System Health**. It is reachable
|
||||||
|
from the Settings sidebar and its live 54px footer, the degraded-only bar
|
||||||
|
indicator, and Vicinae's **Panama: Check System Health** command. Healthy scans
|
||||||
|
reserve no bar space and produce no notification.
|
||||||
|
|
||||||
|
`services/Health.qml` owns the last accepted redacted snapshot. It invokes
|
||||||
|
`scripts/panama-doctor` for scans and bounded repairs, `wl-copy` only for an
|
||||||
|
explicit **Copy Report**, and bounded `notify-send` only when an external repair
|
||||||
|
fails. For a concise terminal view, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.config/quickshell/scripts/panama-doctor --summary
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper diagnoses Panama-owned desktop services, dependencies, links, and
|
||||||
|
configured integrations. It does not read secret values, clipboard or
|
||||||
|
notification contents, calendar events, SSIDs, addresses, or arbitrary command
|
||||||
|
output. Its repair interface is an authored allow-list: it never installs a
|
||||||
|
package, runs `sudo`, deletes user data, or repairs a service Panama does not
|
||||||
|
own. A repair remains degraded until a fresh scan observes recovery.
|
||||||
|
|
||||||
|
The final card is the ownership boundary. Network configuration and the exact
|
||||||
|
Users, Sharing, Colour profiles, and Digital wellbeing handoffs open GNOME
|
||||||
|
Settings because Fedora's system services own those areas.
|
||||||
|
|
||||||
## Adding a setting
|
## Adding a setting
|
||||||
|
|
||||||
One schema entry. That is the whole job.
|
One schema entry. That is the whole job.
|
||||||
@@ -32,6 +59,47 @@ If it is compositor-backed, add the matching `prefs.get("blurSize", 8)` in
|
|||||||
`hypr/looks.lua` so the Hyprland config still stands alone with no settings
|
`hypr/looks.lua` so the Hyprland config still stands alone with no settings
|
||||||
file.
|
file.
|
||||||
|
|
||||||
|
## Setting ownership
|
||||||
|
|
||||||
|
Every preference has **one primary page**, derived from its schema `group` and
|
||||||
|
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
|
||||||
|
part of another established mental model; otherwise use a labelled handoff to
|
||||||
|
the owner instead of duplicating it.
|
||||||
|
|
||||||
|
### Intentional mirrors
|
||||||
|
|
||||||
|
| Setting | Primary page | Mirror | Why the mirror earns its place |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `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. |
|
||||||
|
| `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. |
|
||||||
|
| `lockMinutes` | Power | Privacy | Idle timing owns the mechanism; privacy owns the expectation that the unattended desktop locks. |
|
||||||
|
| `lockOnSleep` | Power | Privacy | Suspend owns the transition; privacy owns whether waking requires authentication. |
|
||||||
|
|
||||||
|
Lock-screen visuals belong only to **Appearance**: background source, blur,
|
||||||
|
clock, date, user name, and password-field presentation. **Power** owns when
|
||||||
|
the session locks, while **Privacy** keeps only the established timing mirrors
|
||||||
|
above. Visual controls must not be copied onto either page.
|
||||||
|
|
||||||
|
**Displays** is the sole owner of mode, scale, rotation, arrangement, and primary role.
|
||||||
|
Those values form one safety transaction: every connected output
|
||||||
|
is applied, verified, confirmed, or restored together. Other pages may link to
|
||||||
|
Displays, but must never expose a second geometry control or persist a partial
|
||||||
|
layout.
|
||||||
|
|
||||||
|
Mirrors must remain the same schema-backed control, never a second preference
|
||||||
|
or a copied default. Additions to this table require a concrete discoverability
|
||||||
|
reason and an update to `tests/quickshell/settings-ownership-contract.sh`.
|
||||||
|
|
||||||
|
Window border colour follows the same ownership rule. The inactive border is a
|
||||||
|
**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
|
||||||
|
accent role owned by the visual theme (and, eventually, an accent picker).
|
||||||
|
`ColorScheme.qml` must never write the focused border, so changing schemes
|
||||||
|
cannot erase a user-selected accent.
|
||||||
|
|
||||||
## The rows
|
## The rows
|
||||||
|
|
||||||
| Component | For |
|
| Component | For |
|
||||||
@@ -83,11 +151,12 @@ slot**, because that is the row's default property, so only the right-hand edge
|
|||||||
becomes clickable. Use `activatable: true` with `onActivated` for a whole-row
|
becomes clickable. Use `activatable: true` with `onActivated` for a whole-row
|
||||||
target.
|
target.
|
||||||
|
|
||||||
**A copy of the Quickshell config shares the live shell's ID.** Quickshell
|
**A content-identical Quickshell entry can share the live shell's ID.**
|
||||||
derives the Shell ID from config *content*, not path, so
|
Quickshell derives the Shell ID from config *content*, not path. Runtime
|
||||||
`cp -a config/dot/quickshell $tmp && qs -p $tmp kill` kills the running
|
harnesses therefore create a distinct semantic entry file, address that exact
|
||||||
desktop, and `qs -p $tmp ipc call …` can drive it. Harnesses that point at a
|
file with `qs -p`, and discover its PID from the exact Config path in
|
||||||
single distinct `.qml` file are safe; copying the whole directory is not.
|
`qs list --all`. They terminate only that recorded PID with `kill`; never use
|
||||||
|
`qs kill` from a copied configuration.
|
||||||
|
|
||||||
## Where state lives
|
## Where state lives
|
||||||
|
|
||||||
@@ -97,6 +166,7 @@ single distinct `.qml` file are safe; copying the whole directory is not.
|
|||||||
| `$XDG_STATE_HOME/panama/panama-home.json` | Home accessory favourites and aliases |
|
| `$XDG_STATE_HOME/panama/panama-home.json` | Home accessory favourites 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 |
|
||||||
|
|
||||||
`SystemSettings.restoreDefaults()` spans all of them. A reset that silently
|
`SystemSettings.restoreDefaults()` spans all of them. A reset that silently
|
||||||
skipped one would be worse than having no reset, because nothing would say so.
|
skipped one would be worse than having no reset, because nothing would say so.
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Region & Language.
|
||||||
|
//
|
||||||
|
// GNOME keeps language and formats under System; the setting itself is the
|
||||||
|
// machine's locale, which localectl owns. Panama does not store a copy of it --
|
||||||
|
// there is exactly one system locale and localectl is where it lives, so a
|
||||||
|
// preference here would be a second source of truth that drifts the moment
|
||||||
|
// anything else changes it.
|
||||||
|
//
|
||||||
|
// Keyboard layout is deliberately not repeated here even though GNOME groups it
|
||||||
|
// with region. It is a compositor setting that applies instantly, it lives on
|
||||||
|
// the Keyboard page with the rest of the typing settings, and showing it twice
|
||||||
|
// invites the two views to disagree.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingsPage {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: "Region & Language"
|
||||||
|
lede: SystemLocale.pendingRestart
|
||||||
|
? "Your new language applies to programs started after you sign out and back in."
|
||||||
|
: "The language and regional formats this machine uses."
|
||||||
|
|
||||||
|
Component.onCompleted: if (SystemLocale.locales.length === 0) SystemLocale.refresh()
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Language"
|
||||||
|
subtitle: "Changing this needs your password, and takes effect for programs started afterwards."
|
||||||
|
|
||||||
|
TextRow {
|
||||||
|
label: "Current language"
|
||||||
|
detail: SystemLocale.pendingRestart
|
||||||
|
? "Chosen, but not in use until you sign out and back in"
|
||||||
|
: "Used by programs that ask the system what language to speak"
|
||||||
|
value: SystemLocale.currentLabel || "Reading…"
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchPicker {
|
||||||
|
width: parent.width
|
||||||
|
items: SystemLocale.locales
|
||||||
|
current: SystemLocale.current
|
||||||
|
placeholder: "Search languages and regions"
|
||||||
|
emptyText: SystemLocale.scanning ? "Reading installed locales…" : "No locales are installed"
|
||||||
|
onPicked: value => SystemLocale.set(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
visible: SystemLocale.lastError !== ""
|
||||||
|
title: "Language problem"
|
||||||
|
subtitle: SystemLocale.lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Formats"
|
||||||
|
subtitle: "Dates, times, and numbers follow the language above. The desktop's own clock formatting is on the Appearance page."
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Clock and date display"
|
||||||
|
detail: "How the desktop itself shows the time"
|
||||||
|
action: "Open appearance"
|
||||||
|
onTriggered: ShellState.openSettings("appearance")
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionRow {
|
||||||
|
label: "Regional formats"
|
||||||
|
detail: "Separate per-category formats (LC_TIME, LC_NUMERIC) are owned by Fedora"
|
||||||
|
action: "Open system"
|
||||||
|
divider: false
|
||||||
|
onTriggered: SystemSettings.openGnomePanel("system", "region")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Read anything on screen"
|
title: "Read anything on screen"
|
||||||
subtitle: "Select a region, window, or display. Panama recognizes it locally and gives you clean follow-up actions."
|
subtitle: "Select a region, window, or display. It is recognized locally, with clean follow-up actions."
|
||||||
|
|
||||||
SettingRow {
|
SettingRow {
|
||||||
icon: ""
|
icon: ""
|
||||||
@@ -52,7 +52,7 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Local recognition"
|
title: "Local recognition"
|
||||||
subtitle: "The screen image stays in Panama's cache and is deleted when you dismiss the result."
|
subtitle: "The screen image stays in a local cache and is deleted when you dismiss the result."
|
||||||
|
|
||||||
TextRow {
|
TextRow {
|
||||||
label: "Text recognition"
|
label: "Text recognition"
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// A searchable list of choices, for settings with far too many values to put in
|
||||||
|
// a dropdown and no useful preview to show.
|
||||||
|
//
|
||||||
|
// SearchPicker {
|
||||||
|
// items: Locale.locales // [{ value, label, detail }]
|
||||||
|
// current: Locale.current
|
||||||
|
// placeholder: "Search languages"
|
||||||
|
// onPicked: value => Locale.set(value)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// FontPicker is the same idea specialised: it draws each candidate in its own
|
||||||
|
// family, which is the whole reason it is a list rather than a text field. This
|
||||||
|
// one is for choices whose only useful preview is their name.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
// SearchField lives with the clipboard module, which is where it was first
|
||||||
|
// needed; FontPicker imports it from the same place.
|
||||||
|
import qs.modules.clipboard
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// [{ value, label, detail }]
|
||||||
|
property var items: []
|
||||||
|
property string current: ""
|
||||||
|
property string emptyText: "Nothing to choose from"
|
||||||
|
property string placeholder: "Search"
|
||||||
|
|
||||||
|
signal picked(string value)
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// Capped and filtered rather than listing everything: 327 locales inside a
|
||||||
|
// page that is already scrolling is worse than a search box.
|
||||||
|
readonly property var matches: {
|
||||||
|
const needle = filter.text.trim().toLowerCase();
|
||||||
|
const list = root.items.filter(item =>
|
||||||
|
needle === ""
|
||||||
|
? true
|
||||||
|
: (String(item.label).toLowerCase().indexOf(needle) >= 0
|
||||||
|
|| String(item.detail).toLowerCase().indexOf(needle) >= 0));
|
||||||
|
|
||||||
|
// The current choice stays visible while browsing, so it is always
|
||||||
|
// clear what would be replaced.
|
||||||
|
const selected = list.find(item => item.value === root.current);
|
||||||
|
if (needle === "" && selected !== undefined)
|
||||||
|
return [selected].concat(list.filter(item => item.value !== root.current)).slice(0, 12);
|
||||||
|
return list.slice(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchField {
|
||||||
|
id: filter
|
||||||
|
width: parent.width
|
||||||
|
placeholder: root.placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.matches
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: candidate
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool selected: candidate.modelData.value === root.current
|
||||||
|
|
||||||
|
label: candidate.modelData.label
|
||||||
|
detail: candidate.selected ? "Currently in use" : candidate.modelData.detail
|
||||||
|
controlWidth: 120
|
||||||
|
divider: candidate.index < root.matches.length - 1
|
||||||
|
activatable: !candidate.selected
|
||||||
|
onActivated: root.picked(candidate.modelData.value)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: candidate.selected
|
||||||
|
text: "✓"
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize + 2
|
||||||
|
color: Theme.accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.matches.length === 0
|
||||||
|
label: root.emptyText
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.config
|
|
||||||
import qs.services
|
|
||||||
|
|
||||||
SettingsPage {
|
|
||||||
title: "Startup & Services"
|
|
||||||
lede: "A clear view of the background tools that make the desktop feel complete."
|
|
||||||
|
|
||||||
function status(active: bool): string {
|
|
||||||
return active ? "Running" : "Stopped";
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
implicitHeight: refresh.implicitHeight
|
|
||||||
|
|
||||||
SettingsButton {
|
|
||||||
id: refresh
|
|
||||||
anchors.right: parent.right
|
|
||||||
text: SystemSettings.busy ? "Refreshing…" : "Refresh"
|
|
||||||
enabled: !SystemSettings.busy
|
|
||||||
onClicked: SystemSettings.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Your services"
|
|
||||||
|
|
||||||
SettingRow {
|
|
||||||
label: "Nextcloud"
|
|
||||||
detail: "File synchronization and tray status"
|
|
||||||
controlWidth: 190
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 10
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: status(SystemSettings.nextcloudActive)
|
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
}
|
|
||||||
SettingsButton {
|
|
||||||
text: "Open"
|
|
||||||
onClicked: SystemSettings.openApplication("nextcloud")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingRow {
|
|
||||||
label: "RustDesk"
|
|
||||||
detail: "Remote access through the enabled system service"
|
|
||||||
controlWidth: 190
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 10
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: status(SystemSettings.rustdeskActive)
|
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
}
|
|
||||||
SettingsButton {
|
|
||||||
text: "Open"
|
|
||||||
onClicked: SystemSettings.openApplication("rustdesk")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingRow {
|
|
||||||
label: "KDE Connect"
|
|
||||||
detail: "Phone pairing, clipboard, files, and remote controls"
|
|
||||||
divider: false
|
|
||||||
controlWidth: 190
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 10
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: status(SystemSettings.kdeconnectActive)
|
|
||||||
color: Theme.fgDim
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSize
|
|
||||||
}
|
|
||||||
SettingsButton {
|
|
||||||
text: "Open"
|
|
||||||
onClicked: SystemSettings.openApplication("kdeconnect")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Desktop foundation"
|
|
||||||
|
|
||||||
TextRow {
|
|
||||||
label: "Hyprpaper"
|
|
||||||
detail: "Wallpaper service"
|
|
||||||
value: status(SystemSettings.hyprpaperActive)
|
|
||||||
}
|
|
||||||
TextRow {
|
|
||||||
label: "Hypridle"
|
|
||||||
detail: "Idle and lock policy"
|
|
||||||
value: status(SystemSettings.hypridleActive)
|
|
||||||
}
|
|
||||||
TextRow {
|
|
||||||
label: "Vicinae"
|
|
||||||
detail: "Spotlight-style launcher daemon"
|
|
||||||
value: status(SystemSettings.vicinaeActive)
|
|
||||||
divider: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Fedora system settings"
|
|
||||||
subtitle: "These remain owned by trusted system services and GNOME's mature panels."
|
|
||||||
|
|
||||||
ActionRow {
|
|
||||||
label: "Network, Bluetooth, printers, users, and accounts"
|
|
||||||
detail: "GNOME Settings remains searchable from the launcher too"
|
|
||||||
divider: false
|
|
||||||
action: "Open network"
|
|
||||||
onTriggered: SystemSettings.openGnomePanel("network")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,19 @@ Rectangle {
|
|||||||
&& pageLoader.item.objectName === "home-phone-page"
|
&& pageLoader.item.objectName === "home-phone-page"
|
||||||
? pageLoader.item.pageDiagnostics
|
? pageLoader.item.pageDiagnostics
|
||||||
: ({})
|
: ({})
|
||||||
|
readonly property var healthDiagnostics: pageLoader.status === Loader.Ready
|
||||||
|
&& pageLoader.item
|
||||||
|
&& pageLoader.item.objectName === "system-health-page"
|
||||||
|
? pageLoader.item.uiDiagnostics()
|
||||||
|
: ({})
|
||||||
|
|
||||||
|
function requestHealthAction(id: string): bool {
|
||||||
|
if (pageLoader.status !== Loader.Ready
|
||||||
|
|| !pageLoader.item
|
||||||
|
|| pageLoader.item.objectName !== "system-health-page")
|
||||||
|
return false;
|
||||||
|
return pageLoader.item.activateRenderedAction(id);
|
||||||
|
}
|
||||||
|
|
||||||
color: Theme.bg
|
color: Theme.bg
|
||||||
radius: 18
|
radius: 18
|
||||||
@@ -40,7 +53,7 @@ Rectangle {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 18
|
anchors.leftMargin: 18
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Panama Settings"
|
text: "Settings"
|
||||||
color: Theme.fg
|
color: Theme.fg
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSize
|
font.pixelSize: Theme.fontSize
|
||||||
@@ -99,11 +112,15 @@ Rectangle {
|
|||||||
case "notifications": return notificationsPage;
|
case "notifications": return notificationsPage;
|
||||||
case "screen-intelligence": return screenIntelligencePage;
|
case "screen-intelligence": return screenIntelligencePage;
|
||||||
case "shortcuts": return shortcutsPage;
|
case "shortcuts": return shortcutsPage;
|
||||||
|
case "mouse": return mousePage;
|
||||||
|
case "privacy": return privacyPage;
|
||||||
|
case "region": return regionPage;
|
||||||
|
case "accounts": return onlineAccountsPage;
|
||||||
case "accessibility": return accessibilityPage;
|
case "accessibility": return accessibilityPage;
|
||||||
case "power": return powerPage;
|
case "power": return powerPage;
|
||||||
case "datetime": return dateTimePage;
|
case "datetime": return dateTimePage;
|
||||||
case "applications": return applicationsPage;
|
case "applications": return applicationsPage;
|
||||||
case "services": return servicesPage;
|
case "services": return healthPage;
|
||||||
case "about": return aboutPage;
|
case "about": return aboutPage;
|
||||||
default: return homePage;
|
default: return homePage;
|
||||||
}
|
}
|
||||||
@@ -153,7 +170,11 @@ Rectangle {
|
|||||||
Component { id: notificationsPage; NotificationsPage {} }
|
Component { id: notificationsPage; NotificationsPage {} }
|
||||||
Component { id: screenIntelligencePage; ScreenIntelligencePage {} }
|
Component { id: screenIntelligencePage; ScreenIntelligencePage {} }
|
||||||
Component { id: shortcutsPage; ShortcutsPage {} }
|
Component { id: shortcutsPage; ShortcutsPage {} }
|
||||||
Component { id: servicesPage; ServicesPage {} }
|
Component { id: mousePage; MousePage {} }
|
||||||
|
Component { id: privacyPage; PrivacyPage {} }
|
||||||
|
Component { id: regionPage; RegionPage {} }
|
||||||
|
Component { id: onlineAccountsPage; OnlineAccountsPage {} }
|
||||||
|
Component { id: healthPage; HealthPage {} }
|
||||||
Component { id: aboutPage; AboutPage {} }
|
Component { id: aboutPage; AboutPage {} }
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
|
|||||||
@@ -31,13 +31,17 @@ Rectangle {
|
|||||||
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
{ page: "sound", label: "Sound", icon: "\u{F057E}" },
|
||||||
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}" },
|
{ page: "notifications", label: "Notifications & Focus", icon: "\u{F009A}" },
|
||||||
{ page: "screen-intelligence", label: "Screen Intelligence", icon: "\u{F05A8}" },
|
{ page: "screen-intelligence", label: "Screen Intelligence", icon: "\u{F05A8}" },
|
||||||
{ page: "shortcuts", label: "Input & Shortcuts", icon: "\u{F030C}" },
|
{ page: "shortcuts", label: "Keyboard", icon: "\u{F030C}" },
|
||||||
|
{ page: "mouse", label: "Mouse & Touchpad", icon: "\u{F037D}" },
|
||||||
|
{ page: "privacy", label: "Privacy & Security", icon: "\u{F0483}" },
|
||||||
|
{ page: "region", label: "Region & Language", icon: "\u{F0AC2}" },
|
||||||
|
{ page: "accounts", label: "Online Accounts", icon: "\u{F0004}" },
|
||||||
{ page: "accessibility", label: "Accessibility", icon: "\u{F0208}" },
|
{ page: "accessibility", label: "Accessibility", icon: "\u{F0208}" },
|
||||||
{ page: "power", label: "Power & Lock", icon: "\u{F0425}" },
|
{ page: "power", label: "Power & Lock", icon: "\u{F0425}" },
|
||||||
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
|
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
|
||||||
{ page: "applications", label: "Applications", icon: "\u{F003B}" },
|
{ page: "applications", label: "Applications", icon: "\u{F003B}" },
|
||||||
{ page: "services", label: "Startup & Services", icon: "\u{F0493}" },
|
{ page: "services", label: "System Health", icon: "\u{F0493}" },
|
||||||
{ page: "about", label: "About Panama", icon: "\u{F02FD}" }
|
{ page: "about", label: "About", icon: "\u{F02FD}" }
|
||||||
]
|
]
|
||||||
|
|
||||||
width: 272
|
width: 272
|
||||||
@@ -56,15 +60,6 @@ Rectangle {
|
|||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "PANAMA"
|
|
||||||
color: Theme.fg
|
|
||||||
font.family: Theme.fontFamily
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
font.weight: Font.DemiBold
|
|
||||||
font.letterSpacing: 2.2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 36
|
height: 36
|
||||||
@@ -297,8 +292,34 @@ Rectangle {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
height: 54
|
height: 54
|
||||||
color: Theme.alpha(Theme.bg, 0.35)
|
activeFocusOnTab: true
|
||||||
border.width: 0
|
color: footerTap.hovered || activeFocus
|
||||||
|
? Theme.alpha(Theme.fg, 0.07)
|
||||||
|
: Theme.alpha(Theme.bg, 0.35)
|
||||||
|
border.width: activeFocus ? 2 : 0
|
||||||
|
border.color: Theme.accent
|
||||||
|
|
||||||
|
function footerText(): string {
|
||||||
|
if (Health.checks.length === 0)
|
||||||
|
return Health.diagnosticUnavailable ? "Health check unavailable" : "Checking the desktop";
|
||||||
|
if (Health.status === "error")
|
||||||
|
return "Desktop needs attention";
|
||||||
|
if (Health.status === "warning") {
|
||||||
|
const count = Health.summary.warnings + Health.summary.errors;
|
||||||
|
return count + (count === 1 ? " health observation" : " health observations");
|
||||||
|
}
|
||||||
|
return "Desktop is healthy";
|
||||||
|
}
|
||||||
|
|
||||||
|
function footerColor(): color {
|
||||||
|
if (Health.checks.length === 0)
|
||||||
|
return Health.diagnosticUnavailable ? Theme.danger : Theme.fgMuted;
|
||||||
|
if (Health.status === "error")
|
||||||
|
return Theme.danger;
|
||||||
|
if (Health.status === "warning")
|
||||||
|
return Theme.warn;
|
||||||
|
return Theme.ok;
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 7
|
width: 7
|
||||||
@@ -307,17 +328,28 @@ Rectangle {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 19
|
anchors.leftMargin: 19
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: Theme.ok
|
color: healthFooter.footerColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 36
|
anchors.leftMargin: 36
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Panama desktop is healthy"
|
text: healthFooter.footerText()
|
||||||
color: Theme.fgDim
|
color: Theme.fgDim
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
id: footerTap
|
||||||
|
onTapped: root.pageRequested("services")
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: root.pageRequested("services")
|
||||||
|
Keys.onSpacePressed: root.pageRequested("services")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ FloatingWindow {
|
|||||||
|
|
||||||
readonly property var homePhoneDiagnostics: settingsShell.homePhoneDiagnostics
|
readonly property var homePhoneDiagnostics: settingsShell.homePhoneDiagnostics
|
||||||
|
|
||||||
title: "Panama Settings"
|
title: "Settings"
|
||||||
visible: ShellState.settingsOpen
|
visible: ShellState.settingsOpen
|
||||||
implicitWidth: 1120
|
implicitWidth: 1120
|
||||||
implicitHeight: 760
|
implicitHeight: 760
|
||||||
|
|||||||
@@ -21,6 +21,25 @@ SettingsPage {
|
|||||||
// when nothing is being captured. Held here rather than per row so that
|
// when nothing is being captured. Held here rather than per row so that
|
||||||
// starting a new capture cancels any other.
|
// starting a new capture cancels any other.
|
||||||
property string capturingChord: ""
|
property string capturingChord: ""
|
||||||
|
readonly property string storedXkbOptions: String(DesktopPreferences.get("keyboardOptions") ?? "")
|
||||||
|
|
||||||
|
function xkbOptions(): var {
|
||||||
|
return root.storedXkbOptions
|
||||||
|
.split(",")
|
||||||
|
.map(option => option.trim())
|
||||||
|
.filter(option => option !== "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentXkbOption(prefix: string): string {
|
||||||
|
return root.xkbOptions().find(option => option.indexOf(prefix) === 0) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setXkbOption(prefix: string, option: string): void {
|
||||||
|
const options = root.xkbOptions().filter(option => option.indexOf(prefix) !== 0);
|
||||||
|
if (option !== "")
|
||||||
|
options.push(option);
|
||||||
|
SystemSettings.commitPreference("keyboardOptions", options.join(","));
|
||||||
|
}
|
||||||
|
|
||||||
title: "Input & Shortcuts"
|
title: "Input & Shortcuts"
|
||||||
lede: "The Forge mental model, carried forward into native tiling."
|
lede: "The Forge mental model, carried forward into native tiling."
|
||||||
@@ -28,19 +47,65 @@ SettingsPage {
|
|||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Keyboard"
|
title: "Keyboard"
|
||||||
|
|
||||||
|
// These were read-only text, on the grounds that a layout change needed
|
||||||
|
// a compositor reload. It does not: setting input:kb_variant through
|
||||||
|
// hl.config re-keymaps attached keyboards immediately -- verified by
|
||||||
|
// watching active_keymap on a real keyboard change and change back. So
|
||||||
|
// they are real controls.
|
||||||
|
TextEntryRow { setting: "keyboardLayout"; placeholder: "us" }
|
||||||
|
TextEntryRow { setting: "keyboardVariant"; placeholder: "none" }
|
||||||
|
|
||||||
|
ChoiceGrid {
|
||||||
|
width: parent.width
|
||||||
|
label: "Caps Lock"
|
||||||
|
detail: "Keep it conventional, or turn a prime keyboard position into Escape or Control"
|
||||||
|
current: root.currentXkbOption("caps:")
|
||||||
|
options: [
|
||||||
|
{ value: "", label: "Standard" },
|
||||||
|
{ value: "caps:escape_shifted_capslock", label: "Esc · Shift for Caps" },
|
||||||
|
{ value: "caps:escape", label: "Escape" },
|
||||||
|
{ value: "caps:ctrl_modifier", label: "Control" }
|
||||||
|
]
|
||||||
|
onPicked: value => root.setXkbOption("caps:", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChoiceGrid {
|
||||||
|
width: parent.width
|
||||||
|
label: "Compose key"
|
||||||
|
detail: "Type accented characters and symbols with memorable key sequences"
|
||||||
|
current: root.currentXkbOption("compose:")
|
||||||
|
options: [
|
||||||
|
{ value: "", label: "Off" },
|
||||||
|
{ value: "compose:ralt", label: "Right Alt" },
|
||||||
|
{ value: "compose:rwin", label: "Right Super" },
|
||||||
|
{ value: "compose:menu", label: "Menu" }
|
||||||
|
]
|
||||||
|
onPicked: value => root.setXkbOption("compose:", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChoiceGrid {
|
||||||
|
width: parent.width
|
||||||
|
label: "Layout switching"
|
||||||
|
detail: "Used when Keyboard layout contains more than one comma-separated layout"
|
||||||
|
current: root.currentXkbOption("grp:")
|
||||||
|
options: [
|
||||||
|
{ value: "", label: "Off" },
|
||||||
|
{ value: "grp:win_space_toggle", label: "Super + Space" },
|
||||||
|
{ value: "grp:alt_shift_toggle", label: "Alt + Shift" },
|
||||||
|
{ value: "grp:ctrl_shift_toggle", label: "Ctrl + Shift" },
|
||||||
|
{ value: "grp:caps_toggle", label: "Caps Lock" }
|
||||||
|
]
|
||||||
|
onPicked: value => root.setXkbOption("grp:", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presets preserve every option outside their own category. The raw
|
||||||
|
// value remains visible for less common xkeyboard-config features.
|
||||||
|
TextEntryRow { setting: "keyboardOptions"; placeholder: "compose:ralt" }
|
||||||
SliderRow { setting: "keyRepeatDelay" }
|
SliderRow { setting: "keyRepeatDelay" }
|
||||||
SliderRow { setting: "keyRepeatRate" }
|
SliderRow { setting: "keyRepeatRate" }
|
||||||
ToggleRow { setting: "numlockByDefault"; divider: false }
|
ToggleRow { setting: "numlockByDefault"; divider: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
title: "Pointer"
|
|
||||||
|
|
||||||
ChoiceRow { setting: "followMouse" }
|
|
||||||
SliderRow { setting: "pointerSensitivity" }
|
|
||||||
SliderRow { setting: "cursorInactiveTimeout"; zeroLabel: "Never"; divider: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Hardware input"
|
title: "Hardware input"
|
||||||
|
|
||||||
@@ -150,7 +215,7 @@ SettingsPage {
|
|||||||
SettingsCard {
|
SettingsCard {
|
||||||
visible: Object.keys(Keybinds.overrides).length > 0
|
visible: Object.keys(Keybinds.overrides).length > 0
|
||||||
title: "Changed shortcuts"
|
title: "Changed shortcuts"
|
||||||
subtitle: "Rebinding stores only the new chord; what a shortcut does always comes from Panama's configuration."
|
subtitle: "Rebinding stores only the new chord; what a shortcut does always comes from the desktop's configuration."
|
||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
label: "Restore every shipped shortcut"
|
label: "Restore every shipped shortcut"
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool output: true
|
||||||
|
readonly property var nodes: AudioDevices.nodes(root.output)
|
||||||
|
readonly property var current: AudioDevices.current(root.output)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 620
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.nodes
|
||||||
|
|
||||||
|
SoundDeviceRow {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: root.width
|
||||||
|
node: modelData
|
||||||
|
output: root.output
|
||||||
|
selected: modelData === root.current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.nodes.length === 0
|
||||||
|
text: Pipewire.ready ? "No audio devices found" : "Discovering audio devices…"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
topPadding: 18
|
||||||
|
bottomPadding: 18
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.config
|
||||||
|
import qs.widgets
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.quicksettings
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var node
|
||||||
|
property bool output: true
|
||||||
|
property bool selected: false
|
||||||
|
|
||||||
|
implicitHeight: root.output || !root.selected ? 88 : 105
|
||||||
|
radius: Theme.cardRadius
|
||||||
|
color: root.selected ? Theme.alpha(Theme.accent, 0.09) : Theme.alpha(Theme.fg, 0.025)
|
||||||
|
border.width: 1
|
||||||
|
border.color: root.selected ? Theme.alpha(Theme.accent, 0.34) : Theme.alpha(Theme.fg, 0.07)
|
||||||
|
|
||||||
|
PwObjectTracker {
|
||||||
|
objects: root.node ? [root.node] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
PwNodePeakMonitor {
|
||||||
|
id: inputPeak
|
||||||
|
node: root.node
|
||||||
|
enabled: !root.output && root.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real volume: root.node?.audio?.volume ?? 0
|
||||||
|
readonly property bool muted: root.node?.audio?.muted ?? false
|
||||||
|
|
||||||
|
function iconName(): string {
|
||||||
|
if (!root.output)
|
||||||
|
return root.muted ? "microphone-sensitivity-muted-symbolic" : "audio-input-microphone-symbolic";
|
||||||
|
if (root.muted || root.volume <= 0.001)
|
||||||
|
return "audio-volume-muted-symbolic";
|
||||||
|
if (root.volume < 0.34)
|
||||||
|
return "audio-volume-low-symbolic";
|
||||||
|
if (root.volume < 0.67)
|
||||||
|
return "audio-volume-medium-symbolic";
|
||||||
|
return "audio-volume-high-symbolic";
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: heading
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.topMargin: 8
|
||||||
|
height: 28
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
ThemedIcon {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
size: 20
|
||||||
|
icon: root.output ? "audio-speakers-symbolic" : "audio-input-microphone-symbolic"
|
||||||
|
iconFallback: "audio-card-symbolic"
|
||||||
|
tint: root.selected ? Theme.accent : Theme.fg
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: Math.max(0, parent.width - 20 - useButton.width - parent.spacing * 2)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: AudioDevices.label(root.node)
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: root.selected ? Font.DemiBold : Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.node.nickname && root.node.nickname !== AudioDevices.label(root.node)
|
||||||
|
text: root.node.nickname ?? ""
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: useButton
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: root.selected ? 78 : 64
|
||||||
|
text: root.selected ? "Default" : "Use"
|
||||||
|
enabled: !root.selected
|
||||||
|
onClicked: AudioDevices.select(root.output, root.node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: muteButton
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.top: heading.bottom
|
||||||
|
anchors.topMargin: 7
|
||||||
|
size: 30
|
||||||
|
iconSize: 17
|
||||||
|
icon: root.iconName()
|
||||||
|
iconFallback: root.output ? "audio-volume-high-symbolic" : "audio-input-microphone-symbolic"
|
||||||
|
onClicked: {
|
||||||
|
if (root.node?.audio)
|
||||||
|
root.node.audio.muted = !root.node.audio.muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueSlider {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.left: muteButton.right
|
||||||
|
anchors.leftMargin: 7
|
||||||
|
anchors.right: volumeText.left
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.verticalCenter: muteButton.verticalCenter
|
||||||
|
value: root.muted ? 0 : root.volume
|
||||||
|
onMoved: value => {
|
||||||
|
if (!root.node?.audio)
|
||||||
|
return;
|
||||||
|
root.node.audio.muted = false;
|
||||||
|
root.node.audio.volume = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: volumeText
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
anchors.verticalCenter: muteButton.verticalCenter
|
||||||
|
width: 38
|
||||||
|
text: Math.round(root.volume * 100) + "%"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: volumeSlider.left
|
||||||
|
anchors.right: volumeText.right
|
||||||
|
anchors.top: muteButton.bottom
|
||||||
|
anchors.topMargin: 5
|
||||||
|
height: 5
|
||||||
|
radius: height / 2
|
||||||
|
visible: !root.output && root.selected
|
||||||
|
color: Theme.alpha(Theme.fg, 0.10)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: parent.width * Math.max(0, Math.min(1, inputPeak.peak))
|
||||||
|
radius: parent.radius
|
||||||
|
color: inputPeak.peak > 0.88 ? Theme.danger : Theme.accentSecondary
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import qs.config
|
import qs.config
|
||||||
import qs.services
|
import qs.services
|
||||||
import qs.modules.quicksettings
|
|
||||||
|
|
||||||
SettingsPage {
|
SettingsPage {
|
||||||
title: "Sound"
|
title: "Sound"
|
||||||
@@ -10,43 +8,67 @@ SettingsPage {
|
|||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Output"
|
title: "Output"
|
||||||
subtitle: Pipewire.defaultAudioSink?.description ?? "No output device"
|
subtitle: AudioDevices.current(true)?.description ?? "No output device"
|
||||||
|
|
||||||
AudioSlider {
|
SoundDeviceList {
|
||||||
width: parent.width
|
|
||||||
node: Pipewire.defaultAudioSink
|
|
||||||
output: true
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.alpha(Theme.fg, 0.06)
|
|
||||||
}
|
|
||||||
AudioDeviceList {
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
output: true
|
output: true
|
||||||
maxHeight: 190
|
}
|
||||||
|
|
||||||
|
AudioBalance {
|
||||||
|
width: parent.width
|
||||||
|
node: AudioDevices.current(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
title: "Input"
|
title: "Input"
|
||||||
subtitle: Pipewire.defaultAudioSource?.description ?? "No input device"
|
subtitle: AudioDevices.current(false)?.description ?? "No input device"
|
||||||
|
|
||||||
AudioSlider {
|
SoundDeviceList {
|
||||||
width: parent.width
|
|
||||||
node: Pipewire.defaultAudioSource
|
|
||||||
output: false
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.alpha(Theme.fg, 0.06)
|
|
||||||
}
|
|
||||||
AudioDeviceList {
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
output: false
|
output: false
|
||||||
maxHeight: 160
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Applications"
|
||||||
|
subtitle: "Control each application currently playing through PipeWire."
|
||||||
|
|
||||||
|
ApplicationMixer {
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
title: "Sound feedback"
|
||||||
|
subtitle: "Use the same event preferences as GTK and GNOME applications."
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Event sounds"
|
||||||
|
detail: "Play alerts and interface event sounds"
|
||||||
|
controlWidth: 42
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.fill: parent
|
||||||
|
checked: SoundFeedback.eventSounds
|
||||||
|
enabled: !SoundFeedback.busy
|
||||||
|
onToggled: checked => SoundFeedback.setEventSounds(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
label: "Input feedback"
|
||||||
|
detail: "Play sounds for supported typing and input events"
|
||||||
|
controlWidth: 42
|
||||||
|
divider: false
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.fill: parent
|
||||||
|
checked: SoundFeedback.inputFeedback
|
||||||
|
enabled: !SoundFeedback.busy
|
||||||
|
onToggled: checked => SoundFeedback.setInputFeedback(checked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,8 +76,8 @@ SettingsPage {
|
|||||||
title: "Advanced sound"
|
title: "Advanced sound"
|
||||||
|
|
||||||
ActionRow {
|
ActionRow {
|
||||||
label: "Application volumes and profiles"
|
label: "Device profiles"
|
||||||
detail: "Open Fedora's complete sound panel"
|
detail: "Open Fedora's complete device profile panel"
|
||||||
divider: false
|
divider: false
|
||||||
action: "Open panel"
|
action: "Open panel"
|
||||||
onTriggered: SystemSettings.openGnomePanel("sound")
|
onTriggered: SystemSettings.openGnomePanel("sound")
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// A free-text setting, bound to a schema key by name.
|
||||||
|
//
|
||||||
|
// TextEntryRow { setting: "keyboardOptions"; placeholder: "compose:ralt" }
|
||||||
|
//
|
||||||
|
// For the settings whose value is a short string with too many legal values to
|
||||||
|
// enumerate -- XKB layouts and options, chiefly. Label, explanation, and
|
||||||
|
// validation all come from PreferenceSchema, so a row cannot drift from the
|
||||||
|
// setting it edits.
|
||||||
|
//
|
||||||
|
// Rejected input is shown as rejected rather than silently dropped or quietly
|
||||||
|
// sanitised. Several of these strings are serialised into an hl.config payload,
|
||||||
|
// 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
|
||||||
|
// it fails, and nothing is committed until it passes.
|
||||||
|
//
|
||||||
|
// Committed on Enter or when focus leaves, not per keystroke: half a layout
|
||||||
|
// name is a valid string that means something else, and each commit is a round
|
||||||
|
// trip to the compositor.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property string setting
|
||||||
|
property string placeholder: ""
|
||||||
|
|
||||||
|
readonly property var spec: PreferenceSchema.spec(root.setting)
|
||||||
|
readonly property string stored: String(DesktopPreferences.get(root.setting) ?? "")
|
||||||
|
|
||||||
|
// Empty is always allowed to be typed through, even where the pattern
|
||||||
|
// forbids it, so a field can be cleared on the way to a new value.
|
||||||
|
readonly property bool valid: input.text === ""
|
||||||
|
|| !root.spec?.pattern
|
||||||
|
|| new RegExp(root.spec.pattern).test(input.text)
|
||||||
|
|
||||||
|
label: root.spec ? root.spec.label : root.setting
|
||||||
|
detail: root.spec ? root.spec.detail : ""
|
||||||
|
controlWidth: 210
|
||||||
|
|
||||||
|
function commit(): void {
|
||||||
|
if (!root.valid || input.text === root.stored)
|
||||||
|
return;
|
||||||
|
SystemSettings.commitPreference(root.setting, input.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function revert(): void {
|
||||||
|
input.text = root.stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follows the store when the value changes elsewhere -- a reset, a restored
|
||||||
|
// backup -- but never while the field has focus, which would overwrite what
|
||||||
|
// is being typed.
|
||||||
|
onStoredChanged: if (!input.activeFocus) input.text = root.stored
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: root.controlWidth
|
||||||
|
height: 30
|
||||||
|
radius: 8
|
||||||
|
color: Theme.alpha(Theme.fg, 0.05)
|
||||||
|
border.width: 1
|
||||||
|
border.color: !root.valid
|
||||||
|
? Theme.danger
|
||||||
|
: (input.activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.12))
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: input
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
text: root.stored
|
||||||
|
color: root.valid ? Theme.fg : Theme.danger
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
selectByMouse: true
|
||||||
|
selectionColor: Theme.alpha(Theme.accent, 0.35)
|
||||||
|
|
||||||
|
onAccepted: root.commit()
|
||||||
|
|
||||||
|
// Commit, then show what is actually stored. If the compositor
|
||||||
|
// refused the value, the field snaps back to the value in effect
|
||||||
|
// rather than displaying a setting that was never applied; if it
|
||||||
|
// accepted, onStoredChanged brings the field back up to date the
|
||||||
|
// moment the write lands.
|
||||||
|
onActiveFocusChanged: if (!activeFocus) { root.commit(); root.revert(); }
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
root.revert();
|
||||||
|
input.focus = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: input.text === "" && !input.activeFocus
|
||||||
|
text: root.placeholder
|
||||||
|
color: Theme.fgMuted
|
||||||
|
font: input.font
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// A time of day, stored as a decimal hour.
|
||||||
|
//
|
||||||
|
// SliderRow would render 17.5 as "17.50", which is not how anyone reads a
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Honours the 24-hour clock preference, because a user who has asked for 18:30
|
||||||
|
// everywhere else should not be shown 6:30 PM here.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
SliderRow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// SliderRow renders `unit` after the number; a time needs the whole readout
|
||||||
|
// replaced, so the formatting is done here instead.
|
||||||
|
function display(value: real): string {
|
||||||
|
const hour = Math.floor(value);
|
||||||
|
const minute = Math.round((value - hour) * 60);
|
||||||
|
const padded = String(minute).padStart(2, "0");
|
||||||
|
|
||||||
|
if (Settings.use24Hour)
|
||||||
|
return `${String(hour).padStart(2, "0")}:${padded}`;
|
||||||
|
|
||||||
|
const suffix = hour < 12 ? "AM" : "PM";
|
||||||
|
const twelve = hour % 12 === 0 ? 12 : hour % 12;
|
||||||
|
return `${twelve}:${padded} ${suffix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string mode: DesktopPreferences.get("wallpaperMode")
|
||||||
|
property var outputs: Wallpaper.outputNames()
|
||||||
|
property string selectedOutput: root.outputs.length > 0 ? root.outputs[0] : ""
|
||||||
|
|
||||||
|
property var setModeAction: function(mode) { Wallpaper.setMode(mode); }
|
||||||
|
property var setIntervalAction: function(minutes) { Wallpaper.setIntervalMinutes(minutes); }
|
||||||
|
property var setShuffleAction: function(enabled) { Wallpaper.setShuffle(enabled); }
|
||||||
|
|
||||||
|
width: parent ? parent.width : 620
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
ChoiceGrid {
|
||||||
|
width: parent.width
|
||||||
|
label: "Wallpaper mode"
|
||||||
|
detail: "Use one image, rotate a collection, or choose per display"
|
||||||
|
options: PreferenceSchema.spec("wallpaperMode").options
|
||||||
|
current: root.mode
|
||||||
|
onPicked: value => root.setModeAction(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ChoiceGrid {
|
||||||
|
visible: root.mode === "per-monitor"
|
||||||
|
width: parent.width
|
||||||
|
label: "Display"
|
||||||
|
detail: "Choose which display the thumbnail grid assigns"
|
||||||
|
options: root.outputs.map(output => ({ value: output, label: output }))
|
||||||
|
current: root.selectedOutput
|
||||||
|
onPicked: value => root.selectedOutput = value
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
visible: root.mode === "slideshow"
|
||||||
|
label: "Change background every"
|
||||||
|
detail: "Time between slideshow images"
|
||||||
|
controlWidth: 280
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
ValueSlider {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: intervalText.left
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
value: (DesktopPreferences.get("wallpaperIntervalMinutes") - 5) / 1435
|
||||||
|
onMoved: ratio => {
|
||||||
|
const raw = 5 + ratio * 1435;
|
||||||
|
root.setIntervalAction(Math.max(5, Math.min(1440, Math.round(raw / 5) * 5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: intervalText
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 62
|
||||||
|
text: DesktopPreferences.get("wallpaperIntervalMinutes") + " min"
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontMono
|
||||||
|
font.features: Theme.tabularFigures
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
visible: root.mode === "slideshow"
|
||||||
|
label: "Shuffle"
|
||||||
|
detail: "Show every selected image before repeating"
|
||||||
|
divider: false
|
||||||
|
controlWidth: 48
|
||||||
|
|
||||||
|
SettingsToggle {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
checked: DesktopPreferences.get("wallpaperShuffle") === true
|
||||||
|
onToggled: enabled => root.setShuffleAction(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.mode === "slideshow"
|
||||||
|
&& (DesktopPreferences.get("wallpaperSlideshowPaths") ?? []).length === 0
|
||||||
|
text: "Select two or more images below to begin a slideshow."
|
||||||
|
color: Theme.fgDim
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
topPadding: 5
|
||||||
|
bottomPadding: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,9 +19,51 @@ Item {
|
|||||||
|
|
||||||
implicitHeight: grid.implicitHeight
|
implicitHeight: grid.implicitHeight
|
||||||
|
|
||||||
|
// Sixty tiles is two screens of wallpaper on a page that also holds
|
||||||
|
// typography, window geometry and effects -- everything below it becomes
|
||||||
|
// unreachable without a long scroll past pictures. Two rows by default,
|
||||||
|
// all of them on request.
|
||||||
|
property bool expanded: false
|
||||||
|
property string mode: DesktopPreferences.get("wallpaperMode")
|
||||||
|
property string selectedOutput: Wallpaper.outputNames()[0] ?? ""
|
||||||
|
property var activeByOutput: Wallpaper.activeByOutput
|
||||||
|
property var slideshowPaths: DesktopPreferences.get("wallpaperSlideshowPaths") ?? []
|
||||||
|
property var assignments: DesktopPreferences.get("wallpaperPerMonitor") ?? ({})
|
||||||
|
property var setSingleAction: function(path) { Wallpaper.setSingle(path); }
|
||||||
|
property var toggleSlideshowAction: function(path) { Wallpaper.toggleSlideshowPath(path); }
|
||||||
|
property var setAssignmentAction: function(output, path) { Wallpaper.setAssignment(output, path); }
|
||||||
|
readonly property int collapsedRows: 2
|
||||||
|
|
||||||
|
readonly property var shown: root.expanded
|
||||||
|
? Wallpaper.available
|
||||||
|
: Wallpaper.available.slice(0, root.columns * root.collapsedRows)
|
||||||
|
|
||||||
|
readonly property int hidden: Wallpaper.available.length - root.shown.length
|
||||||
|
|
||||||
readonly property int columns: Math.max(2, Math.floor(width / 190))
|
readonly property int columns: Math.max(2, Math.floor(width / 190))
|
||||||
readonly property real cellWidth: columns > 0 ? (width - (columns - 1) * 10) / columns : 160
|
readonly property real cellWidth: columns > 0 ? (width - (columns - 1) * 10) / columns : 160
|
||||||
|
|
||||||
|
function isCurrent(path: string): bool {
|
||||||
|
return root.activeByOutput[root.selectedOutput] === path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selected(path: string): bool {
|
||||||
|
if (root.mode === "slideshow")
|
||||||
|
return root.slideshowPaths.includes(path);
|
||||||
|
if (root.mode === "per-monitor")
|
||||||
|
return root.assignments[root.selectedOutput] === path;
|
||||||
|
return root.isCurrent(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(path: string): void {
|
||||||
|
if (root.mode === "slideshow")
|
||||||
|
root.toggleSlideshowAction(path);
|
||||||
|
else if (root.mode === "per-monitor")
|
||||||
|
root.setAssignmentAction(root.selectedOutput, path);
|
||||||
|
else
|
||||||
|
root.setSingleAction(path);
|
||||||
|
}
|
||||||
|
|
||||||
Grid {
|
Grid {
|
||||||
id: grid
|
id: grid
|
||||||
|
|
||||||
@@ -30,14 +72,15 @@ Item {
|
|||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Wallpaper.available
|
model: root.shown
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: tile
|
id: tile
|
||||||
|
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
readonly property bool current: Wallpaper.active === tile.modelData
|
readonly property bool current: root.isCurrent(tile.modelData)
|
||||||
|
readonly property bool member: root.mode === "slideshow" && root.selected(tile.modelData)
|
||||||
|
|
||||||
width: root.cellWidth
|
width: root.cellWidth
|
||||||
height: Math.round(root.cellWidth * 9 / 16)
|
height: Math.round(root.cellWidth * 9 / 16)
|
||||||
@@ -101,6 +144,28 @@ Item {
|
|||||||
border.color: Theme.accent
|
border.color: Theme.accent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: 8
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: 12
|
||||||
|
visible: tile.member
|
||||||
|
color: Theme.alpha(Theme.bgDark, 0.82)
|
||||||
|
border.width: 1
|
||||||
|
border.color: Theme.alpha(Theme.fg, 0.16)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "✓"
|
||||||
|
color: Theme.fg
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: 14
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -119,7 +184,9 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: 7
|
anchors.margins: 7
|
||||||
text: tile.current ? "Current wallpaper" : Wallpaper.titleFor(tile.modelData)
|
text: tile.current
|
||||||
|
? "Current wallpaper"
|
||||||
|
: (tile.member ? "In slideshow" : Wallpaper.titleFor(tile.modelData))
|
||||||
color: tile.current ? Theme.accent : Theme.fg
|
color: tile.current ? Theme.accent : Theme.fg
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
@@ -131,7 +198,7 @@ Item {
|
|||||||
HoverHandler { id: hover }
|
HoverHandler { id: hover }
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
onTapped: Wallpaper.set(tile.modelData)
|
onTapped: root.activate(tile.modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +210,7 @@ Item {
|
|||||||
width: parent.width - 40
|
width: parent.width - 40
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
text: "No images found. Panama looks in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds."
|
text: "No images found. Looked in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds."
|
||||||
color: Theme.fgMuted
|
color: Theme.fgMuted
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Wi-Fi, at page size.
|
||||||
|
//
|
||||||
|
// The quick settings version is a popover: a compact list you glance at. This
|
||||||
|
// is the one you sit in front of when a network is not behaving, so each row
|
||||||
|
// carries what you would otherwise open a terminal to find out — signal,
|
||||||
|
// security, and whether it is a network this machine already knows.
|
||||||
|
//
|
||||||
|
// Joining a secured network reveals an inline password field rather than
|
||||||
|
// failing silently, which is the one interaction the popover already got right
|
||||||
|
// and is worth keeping identical.
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Networking
|
||||||
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
import qs.widgets
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
// SSID whose password field is open, and the last failure.
|
||||||
|
property string passwordFor: ""
|
||||||
|
property string failedSsid: ""
|
||||||
|
property string failedText: ""
|
||||||
|
|
||||||
|
function activate(network: var): void {
|
||||||
|
root.failedSsid = "";
|
||||||
|
if (network.connected)
|
||||||
|
return;
|
||||||
|
if (network.known || !Connectivity.isSecured(network)) {
|
||||||
|
root.passwordFor = "";
|
||||||
|
network.connect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.passwordFor = root.passwordFor === network.name ? "" : network.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Connectivity.networks
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: entry
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
label: entry.modelData.name || "Hidden network"
|
||||||
|
detail: {
|
||||||
|
const bits = [];
|
||||||
|
if (entry.modelData.connected)
|
||||||
|
bits.push("Connected");
|
||||||
|
else if (entry.modelData.known)
|
||||||
|
bits.push("Saved");
|
||||||
|
bits.push(Connectivity.signalLabel(entry.modelData.signalStrength));
|
||||||
|
bits.push(Connectivity.securityLabel(entry.modelData));
|
||||||
|
return bits.join(" · ");
|
||||||
|
}
|
||||||
|
divider: entry.index < Connectivity.networks.length - 1 || root.passwordFor === entry.modelData.name
|
||||||
|
controlWidth: 190
|
||||||
|
activatable: !entry.modelData.connected
|
||||||
|
onActivated: root.activate(entry.modelData)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 7
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: entry.modelData.connected
|
||||||
|
text: "Connected"
|
||||||
|
color: Theme.accent
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: entry.modelData.connected
|
||||||
|
text: "Disconnect"
|
||||||
|
onClicked: entry.modelData.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: !entry.modelData.connected
|
||||||
|
text: entry.modelData.known ? "Connect" : "Join"
|
||||||
|
onClicked: root.activate(entry.modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The password field for this network, when it is the one being
|
||||||
|
// joined. Inline rather than a dialog: a dialog over a tiled window
|
||||||
|
// is a worse place to type than the row you just clicked.
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: root.passwordFor === entry.modelData.name ? 54 : 0
|
||||||
|
visible: height > 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible)
|
||||||
|
password.grab();
|
||||||
|
else
|
||||||
|
password.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
PasswordField {
|
||||||
|
id: password
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: joinButton.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
placeholder: "Password for " + (entry.modelData.name || "network")
|
||||||
|
onAccepted: joinButton.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButton {
|
||||||
|
id: joinButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "Join"
|
||||||
|
|
||||||
|
function join(): void {
|
||||||
|
entry.modelData.connect(password.text);
|
||||||
|
root.passwordFor = "";
|
||||||
|
password.text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: joinButton.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: root.failedSsid === entry.modelData.name
|
||||||
|
leftPadding: 2
|
||||||
|
bottomPadding: 8
|
||||||
|
text: root.failedText
|
||||||
|
color: Theme.warn
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: entry.modelData
|
||||||
|
function onConnectionFailed(reason): void {
|
||||||
|
root.failedSsid = entry.modelData.name;
|
||||||
|
root.failedText = Connectivity.connectionFailureText(reason);
|
||||||
|
root.passwordFor = entry.modelData.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingRow {
|
||||||
|
width: parent.width
|
||||||
|
visible: Connectivity.networks.length === 0
|
||||||
|
label: !Connectivity.wifiDevice
|
||||||
|
? "No Wi-Fi adapter"
|
||||||
|
: (Connectivity.wifiEnabled ? "Looking for networks…" : "Wi-Fi is off")
|
||||||
|
detail: Connectivity.wifiDevice && !Connectivity.wifiEnabled
|
||||||
|
? "Turn it on above to see what is nearby"
|
||||||
|
: ""
|
||||||
|
divider: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,9 @@ DisplaysPage 1.0 DisplaysPage.qml
|
|||||||
HomePage 1.0 HomePage.qml
|
HomePage 1.0 HomePage.qml
|
||||||
NotificationsPage 1.0 NotificationsPage.qml
|
NotificationsPage 1.0 NotificationsPage.qml
|
||||||
ScreenIntelligencePage 1.0 ScreenIntelligencePage.qml
|
ScreenIntelligencePage 1.0 ScreenIntelligencePage.qml
|
||||||
ServicesPage 1.0 ServicesPage.qml
|
HealthPage 1.0 HealthPage.qml
|
||||||
|
HealthSummary 1.0 HealthSummary.qml
|
||||||
|
HealthCheckRow 1.0 HealthCheckRow.qml
|
||||||
SettingRow 1.0 SettingRow.qml
|
SettingRow 1.0 SettingRow.qml
|
||||||
SettingsCard 1.0 SettingsCard.qml
|
SettingsCard 1.0 SettingsCard.qml
|
||||||
SettingsButton 1.0 SettingsButton.qml
|
SettingsButton 1.0 SettingsButton.qml
|
||||||
@@ -27,13 +29,35 @@ ChoiceRow 1.0 ChoiceRow.qml
|
|||||||
ActionRow 1.0 ActionRow.qml
|
ActionRow 1.0 ActionRow.qml
|
||||||
TextRow 1.0 TextRow.qml
|
TextRow 1.0 TextRow.qml
|
||||||
DesktopPreview 1.0 DesktopPreview.qml
|
DesktopPreview 1.0 DesktopPreview.qml
|
||||||
|
LockScreenPreview 1.0 LockScreenPreview.qml
|
||||||
PowerPage 1.0 PowerPage.qml
|
PowerPage 1.0 PowerPage.qml
|
||||||
DateTimePage 1.0 DateTimePage.qml
|
DateTimePage 1.0 DateTimePage.qml
|
||||||
AccessibilityPage 1.0 AccessibilityPage.qml
|
AccessibilityPage 1.0 AccessibilityPage.qml
|
||||||
WallpaperPicker 1.0 WallpaperPicker.qml
|
WallpaperPicker 1.0 WallpaperPicker.qml
|
||||||
|
WallpaperControls 1.0 WallpaperControls.qml
|
||||||
ApplicationsPage 1.0 ApplicationsPage.qml
|
ApplicationsPage 1.0 ApplicationsPage.qml
|
||||||
|
AutostartAppPicker 1.0 AutostartAppPicker.qml
|
||||||
DockPinsEditor 1.0 DockPinsEditor.qml
|
DockPinsEditor 1.0 DockPinsEditor.qml
|
||||||
DockAppPicker 1.0 DockAppPicker.qml
|
DockAppPicker 1.0 DockAppPicker.qml
|
||||||
ShortcutCapture 1.0 ShortcutCapture.qml
|
ShortcutCapture 1.0 ShortcutCapture.qml
|
||||||
ChoiceGrid 1.0 ChoiceGrid.qml
|
ChoiceGrid 1.0 ChoiceGrid.qml
|
||||||
DisplayModePicker 1.0 DisplayModePicker.qml
|
DisplayModePicker 1.0 DisplayModePicker.qml
|
||||||
|
DisplayArrangement 1.0 DisplayArrangement.qml
|
||||||
|
DisplayIdentify 1.0 DisplayIdentify.qml
|
||||||
|
WifiPanel 1.0 WifiPanel.qml
|
||||||
|
BluetoothPanel 1.0 BluetoothPanel.qml
|
||||||
|
PasswordField 1.0 PasswordField.qml
|
||||||
|
AudioBalance 1.0 AudioBalance.qml
|
||||||
|
SoundDeviceList 1.0 SoundDeviceList.qml
|
||||||
|
SoundDeviceRow 1.0 SoundDeviceRow.qml
|
||||||
|
ApplicationMixer 1.0 ApplicationMixer.qml
|
||||||
|
ApplicationVolumeRow 1.0 ApplicationVolumeRow.qml
|
||||||
|
TimeOfDayRow 1.0 TimeOfDayRow.qml
|
||||||
|
LocationPicker 1.0 LocationPicker.qml
|
||||||
|
FontPicker 1.0 FontPicker.qml
|
||||||
|
MousePage 1.0 MousePage.qml
|
||||||
|
TextEntryRow 1.0 TextEntryRow.qml
|
||||||
|
PrivacyPage 1.0 PrivacyPage.qml
|
||||||
|
RegionPage 1.0 RegionPage.qml
|
||||||
|
SearchPicker 1.0 SearchPicker.qml
|
||||||
|
OnlineAccountsPage 1.0 OnlineAccountsPage.qml
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user