Compare commits
196
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d61792433c | ||
|
|
6016efa436 | ||
|
|
ed428e87c4 | ||
|
|
69ecec7ecf | ||
|
|
a90f6eb357 | ||
|
|
c4642919f7 | ||
|
|
c4733e0624 | ||
|
|
e1faaf7a76 | ||
|
|
47f29f9fa9 | ||
|
|
88497826ec | ||
|
|
b319d1a5e1 | ||
|
|
13f3648e4d | ||
|
|
359fb922aa | ||
|
|
b6448c9876 | ||
|
|
15d54b16f6 | ||
|
|
96e4085919 | ||
|
|
6bcc351e07 | ||
|
|
3b01f1e020 | ||
|
|
23141673a2 | ||
|
|
495fb9b41b | ||
|
|
3b657e0a56 | ||
|
|
c148bae4ac | ||
|
|
4cbab01ae2 | ||
|
|
de45f205ad | ||
|
|
52e2a83a78 | ||
|
|
79b3d5cb85 | ||
|
|
1b845c0126 | ||
|
|
f53ca16392 | ||
|
|
6dc606b872 | ||
|
|
32fab59d24 | ||
|
|
f6b970da21 | ||
|
|
7e1c85b094 | ||
|
|
0c364f38e6 | ||
|
|
1aa1324083 | ||
|
|
ac5e6e2130 | ||
|
|
536958430f | ||
|
|
fd99569666 | ||
|
|
a412e3d894 | ||
|
|
4cbe3b882a | ||
|
|
edc504af2e | ||
|
|
a15f019c17 | ||
|
|
116510caa8 | ||
|
|
b10f8e2593 | ||
|
|
8f0fe23377 | ||
|
|
89cf0f8c29 | ||
|
|
e290a262f3 | ||
|
|
6997dd535f | ||
|
|
1f40f8e136 | ||
|
|
e9d567aa72 | ||
|
|
914d58f52b | ||
|
|
a23b42841a | ||
|
|
e0c0e53ae0 | ||
|
|
180308135a | ||
|
|
b743f44c5b | ||
|
|
2528edfddc | ||
|
|
dbd1472e6b | ||
|
|
eeb49c5aff | ||
|
|
8e93f08977 | ||
|
|
99433c0e8e | ||
|
|
a68e4f6dcd | ||
|
|
2b3b762793 | ||
|
|
91306ce810 | ||
|
|
1b94af1163 | ||
|
|
b4ce148caf | ||
|
|
2fcaada7e8 | ||
|
|
588dec4adc | ||
|
|
d96863b687 | ||
|
|
8156fc47ca | ||
|
|
9bc771bcca | ||
|
|
fe74a82a46 | ||
|
|
ac231eeb54 | ||
|
|
8cf03d4529 | ||
|
|
2a716dac9e | ||
|
|
719ef2f38e | ||
|
|
9ba224d776 | ||
|
|
8b59b78d9f | ||
|
|
e6b4d3c1a1 | ||
|
|
70d8d32ee2 | ||
|
|
c37ca3baee | ||
|
|
bd5d030d91 | ||
|
|
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.
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Generated from the color scheme; machine state, not configuration.
|
||||
/config/dot/kitty/current-theme.conf
|
||||
|
||||
# Generated from the color 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
|
||||
|
||||
@@ -8,34 +8,62 @@ git clone https://git.gbrown.org/gib/Panama.git ~/.local/share/Panama
|
||||
~/.local/share/Panama/install
|
||||
```
|
||||
|
||||
`install` runs everything in `setup/scripts/` in order:
|
||||
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
||||
in order, without stopping again:
|
||||
|
||||
| Script | Does |
|
||||
|---|---|
|
||||
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), then the four package lists in `setup/packages/` |
|
||||
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>` |
|
||||
| `interview` | Every prompt, before anything is installed. Answers last one run and are never written to a durable path |
|
||||
| `install-packages` | Repos (RPM Fusion, Terra, Hyprland COPR), the package lists in `setup/packages/`, then whichever optional categories were chosen |
|
||||
| `link-dotfiles` | Symlinks `config/dot/<name>` → `~/.config/<name>`, and seeds the wallpaper, cursor theme and Firefox chrome |
|
||||
| `change-settings` | Copies `config/copy/` over `/`, applies gsettings, enables user services |
|
||||
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
||||
| `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for |
|
||||
| `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot |
|
||||
|
||||
The run ends with a health summary from `panama-doctor`, which reports what is
|
||||
actually running rather than what was attempted. It never fails the install: on a
|
||||
fresh machine it legitimately reports things as not yet configured.
|
||||
|
||||
### Optional applications
|
||||
|
||||
Every machine gets the lists in `setup/packages/`. The interview also offers the
|
||||
categories in `setup/packages/extras/` as a checklist, so a work laptop need not
|
||||
acquire emulators and a desktop need not skip Steam. Nothing is preselected.
|
||||
|
||||
A category is one file. A bare line is a dnf package and a `flatpak:` line is a
|
||||
Flathub ID, because the applications in a category do not all come from one
|
||||
place. Adding a category is adding a file — the menu is read from the directory,
|
||||
not written down anywhere.
|
||||
|
||||
Existing configs are moved to `config/old/` rather than overwritten.
|
||||
|
||||
## Desktops
|
||||
## The desktop
|
||||
|
||||
Panama configures **two** desktops that coexist. Both sessions stay available in
|
||||
GDM, so you can switch back and forth while you settle in.
|
||||
Hyprland, with a shell written from scratch. It began as a replacement for a
|
||||
GNOME session — Forge for tiling, Dash-to-Dock, Openbar, Vitals — and was built
|
||||
to reproduce it closely enough that muscle memory transferred: same keybinds,
|
||||
same panel contents, same dock, same Tokyo Night Moon palette.
|
||||
|
||||
### GNOME
|
||||
The original setup: Forge for tiling, Dash-to-Dock, Openbar, Vitals,
|
||||
AppIndicator support. Configured through `config/dot/forge/` and gsettings.
|
||||
That is history now rather than a second option. Panama installs and configures
|
||||
one desktop, and the GNOME session it grew out of is neither installed nor
|
||||
configured here. What each piece replaced is recorded in
|
||||
[`config/dot/hypr/DESKTOP-PARITY.md`](config/dot/hypr/DESKTOP-PARITY.md) and in
|
||||
the comments of the components themselves, because knowing what a thing was
|
||||
modelled on explains why it behaves the way it does.
|
||||
|
||||
### Hyprland
|
||||
A from-scratch replacement built to reproduce the GNOME setup closely enough
|
||||
that muscle memory transfers — same keybinds, same panel contents, same dock,
|
||||
same Tokyo Night Moon palette.
|
||||
GNOME is not gone from the machine: `gnome-control-center` is a declared
|
||||
dependency, and Panama's own Settings hands off to it for the panels it
|
||||
deliberately does not own — Online Accounts, Color, Sound, Network, Keyboard,
|
||||
Privacy, Wellbeing, Accessibility, and System for users, date and time, region
|
||||
and remote desktop. The allow-list in
|
||||
[`services/SystemSettings.qml`](config/dot/quickshell/services/SystemSettings.qml)
|
||||
is what decides; anything not on it is a panel Panama owns itself.
|
||||
|
||||
| Piece | What it is |
|
||||
|---|---|
|
||||
| `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/uwsm/` | Session environment (see the uwsm caveat in the hypr README) |
|
||||
| `config/dot/wofi/` | Fallback launcher, in case the shell fails to start |
|
||||
@@ -50,13 +78,56 @@ Log in as **"Hyprland (uwsm-managed)"**, not plain "Hyprland".
|
||||
## Layout
|
||||
|
||||
```
|
||||
bin/ Small user-facing commands on PATH
|
||||
bin/ Small user-facing commands on PATH; `panama` is the entry point
|
||||
config/
|
||||
bash/ .bashrc, aliases, env (env is gitignored)
|
||||
copy/ Files copied verbatim over / (needs sudo)
|
||||
dot/ Symlinked into ~/.config
|
||||
firefox/ Vendored Firefox chrome, linked into the browser profile
|
||||
local/ Icons and the cursor theme, linked into ~/.local/share
|
||||
old/ Backups of whatever was replaced (gitignored)
|
||||
wallpapers/ Copied into ~/Pictures/Wallpapers when absent
|
||||
setup/
|
||||
packages/ One package per line
|
||||
packages/ One package per line; extras/ holds the optional categories
|
||||
scripts/ Run in order by ./install
|
||||
tests/ Contracts. See below
|
||||
docs/ Settings reference, and the design specs behind the work
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
121 of them, under `tests/`. Run the lot, or a subset by pattern:
|
||||
|
||||
```sh
|
||||
panama test # everything
|
||||
panama test dock # just the ones matching "dock"
|
||||
tests/setup/interview-contract # or one directly; they are plain executables
|
||||
```
|
||||
|
||||
They are called contracts rather than unit tests because that is what they are:
|
||||
each one pins a decision that was expensive to get right and is cheap to undo by
|
||||
accident. Most read or measure the real thing — launching a shell to measure a
|
||||
surface's geometry, standing stub commands on `PATH` to see what a stage would
|
||||
have installed, running a script against a throwaway `HOME` — rather than
|
||||
asserting things about source text, because the bugs worth catching here have all
|
||||
been ones that source text looked fine for.
|
||||
|
||||
```
|
||||
tests/setup/ The installer: the interview, package lists, hardware, extras
|
||||
tests/quickshell/ The shell and its settings pages
|
||||
tests/hypr/ The compositor config
|
||||
```
|
||||
|
||||
## The `panama` command
|
||||
|
||||
```sh
|
||||
panama update # review, commit and sync this repo
|
||||
panama edit # open it in Neovim
|
||||
panama doctor # what is actually running, not what was installed
|
||||
panama test # every contract, or a subset by pattern
|
||||
panama upgrade # re-run ./install from anywhere
|
||||
```
|
||||
|
||||
None of the scripts in this repository carry a `.sh` extension. A shebang and
|
||||
the executable bit already select the interpreter, and the extension only
|
||||
becomes something to keep in sync — which it did not stay.
|
||||
|
||||
@@ -152,11 +152,14 @@ case "$cmd" in
|
||||
|
||||
# 2.4) Run flatpak updates (user then system)
|
||||
flatpak update -y
|
||||
sudo flatpak update
|
||||
sudo flatpak update -y
|
||||
|
||||
# 2.5) Optional firmware via fwupd
|
||||
if $firmware; then
|
||||
sudo fwupdmgr refresh
|
||||
# fwupdmgr exits non-zero when metadata is already current -- that is
|
||||
# not an error, but under 'set -e' it would abort the script before
|
||||
# 'fwupdmgr update' ever runs.
|
||||
sudo fwupdmgr refresh || true
|
||||
sudo fwupdmgr update
|
||||
fi
|
||||
|
||||
|
||||
+109
@@ -7,6 +7,9 @@
|
||||
# Commands:
|
||||
# update Commit & sync local changes (or just pull if clean)
|
||||
# edit Open the Panama repo in Neovim
|
||||
# doctor Report what is actually running on this machine
|
||||
# test Run every contract under tests/
|
||||
# upgrade Re-run the installer from anywhere
|
||||
# help Show this help
|
||||
#
|
||||
# Designed to grow: add new subcommands as cmd_<name> functions and
|
||||
@@ -62,6 +65,12 @@ ${BOLD}Commands:${RESET}
|
||||
${GREEN}update${RESET} Review, commit & sync local changes. If the working tree is
|
||||
clean it simply runs 'git pull'.
|
||||
${GREEN}edit${RESET} Open the Panama repo in Neovim.
|
||||
${GREEN}doctor${RESET} Report what is actually running on this machine, rather
|
||||
than what was installed. Takes --summary for one line per check.
|
||||
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
|
||||
a subset: 'panama test dock' runs the ones matching 'dock'.
|
||||
${GREEN}upgrade${RESET} Re-run ./install from anywhere. Safe: every stage is
|
||||
idempotent and this is the documented upgrade path.
|
||||
${GREEN}help${RESET} Show this help (also -h, --help).
|
||||
|
||||
${BOLD}Options:${RESET}
|
||||
@@ -71,6 +80,9 @@ ${BOLD}Options:${RESET}
|
||||
${BOLD}Examples:${RESET}
|
||||
$PROGRAM update
|
||||
$PROGRAM edit
|
||||
$PROGRAM doctor --summary
|
||||
$PROGRAM test dock
|
||||
$PROGRAM upgrade
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -205,6 +217,100 @@ cmd_edit() {
|
||||
exec nvim .
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: doctor
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# The health check already exists and the installer already runs it; what it did
|
||||
# not have was a way to reach it from a terminal. Everything is passed straight
|
||||
# through, so --summary and anything added later work without this knowing about
|
||||
# them.
|
||||
cmd_doctor() {
|
||||
local doctor="$PANAMA_DIR/config/dot/quickshell/scripts/panama-doctor"
|
||||
if [[ ! -x "$doctor" ]]; then
|
||||
err "panama-doctor is missing from $doctor"
|
||||
exit 1
|
||||
fi
|
||||
exec "$doctor" "$@"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: test
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# The contracts are the main safety net in this repository and had no entry
|
||||
# point: 121 executables with no runner and no mention in the README, which is
|
||||
# most of the way to not having them.
|
||||
#
|
||||
# Each runs in its own process and a failure does not stop the rest, because the
|
||||
# useful output is the whole list of what is broken rather than the first thing
|
||||
# that broke. The exit code is what a caller can act on.
|
||||
cmd_test() {
|
||||
local pattern="${1:-}"
|
||||
local -a suite=()
|
||||
|
||||
# Executables, plus the Python suites. Those are unittest files rather than
|
||||
# executables, and collecting only what has the executable bit would skip them
|
||||
# without saying so -- which is how all three came to be run by nothing at all.
|
||||
# A runner with a blind spot is worse than no runner, because it reports PASS.
|
||||
while IFS= read -r path; do
|
||||
[[ -x "$path" || "$path" == *_test.py ]] || continue
|
||||
[[ -z "$pattern" || "$path" == *"$pattern"* ]] && suite+=("$path")
|
||||
done < <(find "$PANAMA_DIR/tests" -type f -not -path '*/fixtures/*' -not -path '*__pycache__*' | sort)
|
||||
|
||||
if (( ${#suite[@]} == 0 )); then
|
||||
err "No contracts match '${pattern}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Running ${#suite[@]} contract(s)"
|
||||
local -a failed=()
|
||||
local path name
|
||||
local -a runner
|
||||
for path in "${suite[@]}"; do
|
||||
name="${path#"$PANAMA_DIR"/tests/}"
|
||||
if [[ "$path" == *_test.py ]]; then
|
||||
runner=(python3 "$path")
|
||||
else
|
||||
runner=("$path")
|
||||
fi
|
||||
if "${runner[@]}" >/dev/null 2>&1; then
|
||||
ok "$name"
|
||||
else
|
||||
err "$name"
|
||||
failed+=("$name")
|
||||
fi
|
||||
done
|
||||
|
||||
header "Result"
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
ok "${#suite[@]} contract(s) passed"
|
||||
return 0
|
||||
fi
|
||||
err "${#failed[@]} of ${#suite[@]} failed:"
|
||||
printf ' %s\n' "${failed[@]}" >&2
|
||||
warn "Run one on its own to see why: ${BOLD}${PANAMA_DIR}/tests/<name>${RESET}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: upgrade
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# ./install is the upgrade path -- every stage is idempotent and re-running is
|
||||
# the documented way to repair a machine. This only saves remembering where the
|
||||
# repository lives.
|
||||
cmd_upgrade() {
|
||||
local installer="$PANAMA_DIR/install"
|
||||
if [[ ! -x "$installer" ]]; then
|
||||
err "The installer is missing from $installer"
|
||||
exit 1
|
||||
fi
|
||||
info "Re-running ${BOLD}${installer}${RESET}"
|
||||
cd "$PANAMA_DIR"
|
||||
exec "$installer" "$@"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Dispatcher
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -213,6 +319,9 @@ main() {
|
||||
case "$cmd" in
|
||||
update) shift; cmd_update "$@" ;;
|
||||
edit) shift; cmd_edit "$@" ;;
|
||||
doctor) shift; cmd_doctor "$@" ;;
|
||||
test) shift; cmd_test "$@" ;;
|
||||
upgrade) shift; cmd_upgrade "$@" ;;
|
||||
help|-h|--help|"") usage ;;
|
||||
--version) printf '%s %s\n' "$PROGRAM" "$VERSION" ;;
|
||||
*)
|
||||
|
||||
+4
-5
@@ -24,8 +24,7 @@ else
|
||||
fi
|
||||
export PATH
|
||||
fi
|
||||
. "$HOME/.cargo/env"
|
||||
|
||||
# >>> Codex installer >>>
|
||||
export PATH="/home/gib/.local/bin:$PATH"
|
||||
# <<< Codex installer <<<
|
||||
# rustup writes this file, and initial-packages installs rustup rather than
|
||||
# running rustup-init -- so on a fresh machine it does not exist yet and an
|
||||
# unguarded source made every single shell start with an error.
|
||||
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
||||
|
||||
@@ -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 colors 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,72 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Emulator",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Emulator",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Emulator",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Emulator",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Emulator",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Emulator",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"overrides": [
|
||||
{
|
||||
"wmClass": "jetbrains-toolbox",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.amezin.ddterm",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Com.github.donadigo.eddy",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Conky",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Gnome-initial-setup",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "org.gnome.Calculator",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "gnome-terminal-preferences",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Guake",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "zoom",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Bitwarden",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Hidamari",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.mattjakeman.ExtensionManager",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Cider",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Ulauncher",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "com.nextcloud.desktopclient.nextcloud",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "mpv",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "Spotify",
|
||||
"mode": "float"
|
||||
},
|
||||
{
|
||||
"wmClass": "bitwarden",
|
||||
"mode": "float"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
.tiled {
|
||||
color: rgba(236, 94, 94, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.split {
|
||||
color: rgba(255, 246, 108, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.stacked {
|
||||
color: rgba(247, 162, 43, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.tabbed {
|
||||
color: rgba(17, 199, 224, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.floated {
|
||||
color: rgba(180, 167, 214, 1);
|
||||
border-width: 3px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.window-tiled-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-split-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* forge-36le: St does not paint an asymmetric (single-side) border when a
|
||||
* border-radius is set, so the directional hint never rendered. Drop the radius
|
||||
* on the one-edge variants; St paints a straight single-side border reliably. */
|
||||
|
||||
.window-split-horizontal {
|
||||
border-left-width: 0;
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.window-split-vertical {
|
||||
border-left-width: 0;
|
||||
border-top-width: 0;
|
||||
border-right-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.window-stacked-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-tabbed-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-tabbed-bg {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.window-tabbed-tab {
|
||||
background-color: rgba(54, 47, 45, 1);
|
||||
border-color: rgba(98,160,234,0.6);
|
||||
border-width: 1px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin: 1px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.window-tabbed-tab-active {
|
||||
background-color: rgb(98,160,234);
|
||||
color: black;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.window-tabbed-tab-close {
|
||||
padding: 3px;
|
||||
margin: 4px;
|
||||
border-radius: 16px;
|
||||
width: 16px;
|
||||
background-color: #e06666;
|
||||
}
|
||||
|
||||
.window-tabbed-tab-icon {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.window-floated-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-tilepreview-tiled {
|
||||
border-width: 1px;
|
||||
border-color: rgba(98,160,234,0.3);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(98,160,234,0.2);
|
||||
}
|
||||
|
||||
.window-tilepreview-stacked {
|
||||
border-width: 1px;
|
||||
border-color: rgba(98,160,234,0.3);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(98,160,234,0.2);
|
||||
}
|
||||
|
||||
.window-tilepreview-swap {
|
||||
border-width: 1px;
|
||||
border-color: rgba(162, 247, 43, 0.4);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(162, 247, 43, 0.4);
|
||||
}
|
||||
|
||||
.window-tilepreview-tabbed {
|
||||
border-width: 1px;
|
||||
border-color: rgba(98,160,234,0.3);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(98,160,234,0.2);
|
||||
}
|
||||
|
||||
/* Keybindings Cheatsheet Overlay */
|
||||
|
||||
.forge-cheatsheet {
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 16px;
|
||||
padding: 24px 32px;
|
||||
border: 1px solid #4d4d4d;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.forge-cheatsheet-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-category {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #78aeed;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-row {
|
||||
font-size: 12px;
|
||||
color: #eeeeec;
|
||||
margin: 4px 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-key {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: #f5c211;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-desc {
|
||||
color: #d0cfcc;
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
.tiled {
|
||||
color: rgba(236, 94, 94, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.split {
|
||||
color: rgba(255, 246, 108, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.stacked {
|
||||
color: rgba(247, 162, 43, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.tabbed {
|
||||
color: rgba(17, 199, 224, 1);
|
||||
opacity: 1;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.floated {
|
||||
color: rgba(180, 167, 214, 1);
|
||||
border-width: 3px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.window-tiled-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-split-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* forge-36le: St does not paint an asymmetric (single-side) border when a
|
||||
* border-radius is set, so the directional hint never rendered. Drop the radius
|
||||
* on the one-edge variants; St paints a straight single-side border reliably. */
|
||||
|
||||
.window-split-horizontal {
|
||||
border-left-width: 0;
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.window-split-vertical {
|
||||
border-left-width: 0;
|
||||
border-top-width: 0;
|
||||
border-right-width: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.window-stacked-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-tabbed-border {
|
||||
border-width: 3px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-tabbed-bg {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.window-tabbed-tab {
|
||||
background-color: rgba(54, 47, 45, 1);
|
||||
border-color: rgba(98,160,234,0.6);
|
||||
border-width: 1px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin: 1px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.window-tabbed-tab-active {
|
||||
background-color: rgb(98,160,234);
|
||||
color: black;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.window-tabbed-tab-close {
|
||||
padding: 3px;
|
||||
margin: 4px;
|
||||
border-radius: 16px;
|
||||
width: 16px;
|
||||
background-color: #e06666;
|
||||
}
|
||||
|
||||
.window-tabbed-tab-icon {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.window-floated-border {
|
||||
border-width: 1px;
|
||||
border-color: rgb(98,160,234);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.window-tilepreview-tiled {
|
||||
border-width: 1px;
|
||||
border-color: rgba(98,160,234,0.3);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(98,160,234,0.2);
|
||||
}
|
||||
|
||||
.window-tilepreview-stacked {
|
||||
border-width: 1px;
|
||||
border-color: rgba(98,160,234,0.3);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(98,160,234,0.2);
|
||||
}
|
||||
|
||||
.window-tilepreview-swap {
|
||||
border-width: 1px;
|
||||
border-color: rgba(162, 247, 43, 0.4);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(162, 247, 43, 0.4);
|
||||
}
|
||||
|
||||
.window-tilepreview-tabbed {
|
||||
border-width: 1px;
|
||||
border-color: rgba(98,160,234,0.3);
|
||||
border-style: solid;
|
||||
border-radius: 14px;
|
||||
background-color: rgba(98,160,234,0.2);
|
||||
}
|
||||
|
||||
/* Keybindings Cheatsheet Overlay */
|
||||
|
||||
.forge-cheatsheet {
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 16px;
|
||||
padding: 24px 32px;
|
||||
border: 1px solid #4d4d4d;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.forge-cheatsheet-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-category {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: #78aeed;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-row {
|
||||
font-size: 12px;
|
||||
color: #eeeeec;
|
||||
margin: 4px 0;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-key {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
color: #f5c211;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.forge-cheatsheet-desc {
|
||||
color: #d0cfcc;
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
# GENERATED FILE -- edit settings.ini.template instead.
|
||||
#
|
||||
# The theme name and dark preference below follow Panama's color
|
||||
# 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]
|
||||
# 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
|
||||
# themes that aren't installed (Tahoe-Dark, WhiteSur-cursors) without anything
|
||||
# appearing broken.
|
||||
gtk-theme-name=adw-gtk3-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
|
||||
gtk-application-prefer-dark-theme=1
|
||||
gtk-application-prefer-dark-theme=@PREFER_DARK@
|
||||
|
||||
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
||||
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 color
|
||||
# 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
|
||||
@@ -15,15 +15,15 @@ Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
||||
| Hidden applications | GNOME App Hider list in Vicinae provider config | Live |
|
||||
| Tiling and dynamic workspaces | Hyprland Lua config, Forge keymap preserved | Live |
|
||||
| Overview | Continuum filmstrip, large workspace preview, cross-workspace search | Live |
|
||||
| Minimise | `SUPER+SHIFT+X` sends to scratchpad; `SUPER+X` restores | Live |
|
||||
| Dock | Quickshell dock with GNOME favourites and intellihide | Live |
|
||||
| Minimize | `SUPER+SHIFT+X` sends to scratchpad; `SUPER+X` restores | Live |
|
||||
| Dock | Quickshell dock with GNOME favorites and intellihide | Live |
|
||||
| Panel | Workspaces, CPU/RAM/GPU, clock, weather, quiet event capsule, media, clipboard, tray, status | Live |
|
||||
| Clipboard history | Vicinae owns capture/storage; Quickshell provides the panel | Live |
|
||||
| Notifications | Quickshell server, toasts, grouped history, DND, clear actions | Live |
|
||||
| Date menu | Two-column Daybook with Agenda, Ongoing activity, Notifications, month calendar, weather, and media | Live |
|
||||
| Ongoing activity | Focus, Caffeine, recording, screen sharing, camera, and microphone state with direct controls | Live |
|
||||
| Calendar agenda | Existing GNOME, Google, iCloud, and Nextcloud sources through Evolution Data Server | Live |
|
||||
| Control Center | Wi-Fi, Bluetooth, audio input/output, Caffeine, Night Light, Focus, Home Assistant favourites, and KDE Connect phone continuity | Live |
|
||||
| Control Center | Wi-Fi, Bluetooth, audio input/output, Caffeine, Night Light, Focus, Home Assistant favorites, and KDE Connect phone continuity | Live |
|
||||
| Focus sessions | Persistent workspace-bound timer, DND/caffeine coordination, Signal Glass controls | Live |
|
||||
| Brightness | Appears only when a backlight device exists | Live, not applicable on this monitor |
|
||||
| AppIndicator / tray | Native StatusNotifierItem host and themed menus | Live |
|
||||
@@ -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 |
|
||||
| Autostart apps | Nextcloud, Bitwarden, and RustDesk system service/tray | 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; labeled GNOME hardware/account handoffs | Live |
|
||||
| System health and recovery | Settings → System Health, `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 **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, color
|
||||
profiles, and digital wellbeing to their exact GNOME Settings panels rather
|
||||
than presenting inert Hyprland controls.
|
||||
|
||||
## GNOME extension migration
|
||||
|
||||
@@ -52,9 +77,9 @@ Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
||||
| Caffeine | Replaced by a real logind inhibitor in quick settings |
|
||||
| Blur My Shell / Openbar / User Theme | Replaced by the Prism shell and compositor blur |
|
||||
| Bluetooth Quick Connect | Replaced by the full Bluetooth picker |
|
||||
| Wi-Fi QR | Deliberately omitted; it is not useful enough to justify another credential-reading surface |
|
||||
| Wi-Fi QR | Replaced by an on-demand QR-code sharing flow in Control Center's Wi-Fi panel; the code is generated only while shown and written to tmpfs, never persisted |
|
||||
| GSConnect | Replaced by capability-aware KDE Connect phone continuity in Control Center; the paired iPhone exposes file, clipboard, and Ring actions when reachable |
|
||||
| Home Assistant | Replaced by secure favourites in Control Center; explicit private environment values take precedence over the existing GNOME extension and Secret Service setup |
|
||||
| Home Assistant | Replaced by secure favorites in Control Center; explicit private environment values take precedence over the existing GNOME extension and Secret Service setup |
|
||||
| Custom Hot Corners Extended | No action was configured, so there is no behavior to port |
|
||||
| App Hider | Hidden app list is now applied in Vicinae |
|
||||
| Allow Locked Remote Desktop | Mutter-specific patch; RustDesk is independent, but lock-screen access needs a real remote test |
|
||||
@@ -86,4 +111,4 @@ menu for a minority, which is worse than omitting it. Relevant references:
|
||||
- <https://github.com/hyprwm/Hyprland/discussions/1358>
|
||||
|
||||
A future focused-window menu could expose compositor actions such as float,
|
||||
move, fullscreen, and close. It should not be labelled as an application menu.
|
||||
move, fullscreen, and close. It should not be labeled as an application menu.
|
||||
|
||||
+23
-23
@@ -27,10 +27,10 @@ Don't "fix" them.
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `monitors.lua` | DP-2 geometry, scaling, and the HDR decision |
|
||||
| `looks.lua` | Colours, blur, glow, shadows, animations, VRR, scanout |
|
||||
| `looks.lua` | Colors, blur, glow, shadows, animations, VRR, scanout |
|
||||
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
||||
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
||||
| `keybinds.lua` | The full keymap |
|
||||
@@ -59,7 +59,7 @@ generated elsewhere:
|
||||
`quickshell/scripts/panama-idle` regenerates the hypridle config from the
|
||||
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;
|
||||
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.
|
||||
|
||||
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
|
||||
no settings file at all.
|
||||
- **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.
|
||||
|
||||
To add an adjustable setting: add an entry to
|
||||
@@ -87,15 +87,15 @@ persistence, validation, reset, and the live write are all derived from that
|
||||
entry.
|
||||
|
||||
`prefs.lua` never raises. A missing, empty, truncated, malformed, or
|
||||
wrong-typed settings file costs you your customisations and nothing else;
|
||||
`tests/hypr/prefs-fallback-contract.sh` pins that, including that Hyprland still
|
||||
wrong-typed settings file costs you your customizations and nothing else;
|
||||
`tests/hypr/prefs-fallback-contract` pins that, including that Hyprland still
|
||||
accepts the config in each of those states.
|
||||
|
||||
### Adding a keybind: use `bind`, not `hl.bind`
|
||||
|
||||
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
|
||||
Panama Settings without editing this file.
|
||||
the Settings app without editing this file.
|
||||
|
||||
```lua
|
||||
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||
@@ -118,9 +118,9 @@ 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
|
||||
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.
|
||||
`tests/quickshell/keybinds-contract.sh` fails if any bind lacks a description, so
|
||||
`tests/quickshell/keybinds-contract` fails if any bind lacks a description, so
|
||||
this cannot regress silently.
|
||||
|
||||
```lua
|
||||
@@ -163,7 +163,7 @@ four places, and nowhere else —
|
||||
dock, popovers), running blue on the left to orchid on the right and fading
|
||||
out before the corners. See `quickshell/widgets/PrismEdge.qml`.
|
||||
2. The focused window's border — `general.col.active_border`, blue→orchid at
|
||||
115°. Unfocused windows get no colour at all, because the gradient only
|
||||
115°. Unfocused windows get no color at all, because the gradient only
|
||||
means something if one window on screen is wearing it.
|
||||
3. The active workspace pill in the bar.
|
||||
4. Slider fills.
|
||||
@@ -179,9 +179,9 @@ which is the opposite of the intent.
|
||||
|
||||
The Nerd Font is still used, but only to draw **icon glyphs** — it is the
|
||||
pragmatic alternative to freedesktop symbolic icons, which ship with a
|
||||
hardcoded `#2e3436` fill that Qt (unlike GTK) will not recolour. Where a
|
||||
hardcoded `#2e3436` fill that Qt (unlike GTK) will not recolor. Where a
|
||||
themed freedesktop icon is wanted instead, `quickshell/widgets/ThemedIcon.qml`
|
||||
paints a palette colour through the icon's alpha.
|
||||
paints a palette color through the icon's alpha.
|
||||
|
||||
Anything whose digits change in place — the clock, the vitals percentages, the
|
||||
recording timer, the selection readout — sets `font.features:
|
||||
@@ -247,7 +247,7 @@ The mental model is unchanged from Forge:
|
||||
| `SUPER + SHIFT + H/J/K/L` | Move window |
|
||||
| `SUPER + CTRL + H/J/K/L` | Swap window |
|
||||
| `SUPER + SHIFT + Y/O` · `B/M` | Wider · narrower |
|
||||
| `SUPER + SHIFT + I/U` · `P/N` | Taller · shorter |
|
||||
| `SUPER + SHIFT + I/U` · `,/N` | Taller · shorter |
|
||||
| `SUPER + [` / `]` / `=` | Shrink / expand / reset split |
|
||||
| `SUPER + Q` | Close |
|
||||
| `SUPER + U` | Fullscreen |
|
||||
@@ -256,7 +256,7 @@ The mental model is unchanged from Forge:
|
||||
| `SUPER + CTRL + G` / `Z` / `V` | Toggle split / preselect right / preselect down |
|
||||
| `SUPER + Tab` / `SHIFT + Tab` | Cycle windows |
|
||||
| `SUPER + SHIFT + grave` | Last window |
|
||||
| `SUPER + X` / `SUPER + SHIFT + X` | Restore scratchpad / minimise to scratchpad |
|
||||
| `SUPER + X` / `SUPER + SHIFT + X` | Restore scratchpad / minimize to scratchpad |
|
||||
|
||||
### Workspaces (dynamic, like GNOME)
|
||||
| Key | Action |
|
||||
@@ -270,25 +270,25 @@ The mental model is unchanged from Forge:
|
||||
### Launcher and shell
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `SUPER + A` / `R` / `Space` | Launcher (vicinae) — all three, pick your favourite |
|
||||
| `SUPER + A` / `R` / `Space` | Launcher (vicinae) — all three, pick your favorite |
|
||||
| `SUPER + SHIFT + R` | Fallback launcher (wofi) if the shell is broken |
|
||||
| `SUPER + V` | Clipboard history |
|
||||
| `SUPER + .` | Emoji picker |
|
||||
| `SUPER + S` | Quick settings |
|
||||
| `SUPER + I` | Panama Settings |
|
||||
| `SUPER + I` | Settings |
|
||||
| `SUPER + SHIFT + F` | Start or reveal focus session |
|
||||
| `SUPER + B` | Notification centre |
|
||||
| `SUPER + B` | Notification center |
|
||||
| `SUPER + grave` | Workspace overview |
|
||||
| `Print` | Screenshot / record picker |
|
||||
| `SHIFT` / `ALT + Print` | Screenshot screen / window immediately |
|
||||
| `SUPER + SHIFT + S` | Screen Intelligence — read text and codes from a selection |
|
||||
| `SUPER + SHIFT + P` | Colour picker |
|
||||
| `SUPER + SHIFT + P` | Color picker |
|
||||
| `CTRL + ALT + L` | Lock (SUPER+L is "focus right") |
|
||||
| `CTRL + ALT + Delete` | Power menu |
|
||||
|
||||
### Apps
|
||||
`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.
|
||||
|
||||
## Calendar and notifications
|
||||
@@ -325,8 +325,8 @@ Caffeine, Night Light, Focus, audio input/output, user, settings, and power in
|
||||
one place. Home and Phone continue the same surface rather than opening extra
|
||||
dashboard windows.
|
||||
|
||||
Home shows the first four selected favourites at rest and every selected light
|
||||
when expanded. Use **Panama Settings → Home & Phone** to choose favourites,
|
||||
Home shows the first four selected favorites at rest and every selected light
|
||||
when expanded. Use **Settings → Home & Phone** to choose favorites,
|
||||
set Panama-only aliases, and arrange their order. Dragging a brightness control
|
||||
only previews the value; releasing it sends one brightness request. A normal
|
||||
power toggle leaves Home Assistant responsible for restoring its previous
|
||||
@@ -334,7 +334,7 @@ level.
|
||||
|
||||
Credentials stay private in the gitignored `config/bash/env` file, with the
|
||||
existing GNOME extension and Secret Service setup retained as a compatibility
|
||||
fallback. Favourites, aliases, and order live in Quickshell state. No shell
|
||||
fallback. Favorites, aliases, and order live in Quickshell state. No shell
|
||||
restart is required after changing credentials; close and reopen Control Center
|
||||
to refresh. If Home Assistant is offline, the last known values stay visible
|
||||
with a stale-state label and Retry action.
|
||||
@@ -361,7 +361,7 @@ These are the places a 1:1 port was impossible, and what was done instead:
|
||||
|
||||
- **Per-edge resize.** Forge resized one named edge; Hyprland resizes along an
|
||||
axis and lets the layout pick the edge. The eight Forge keys collapse to four
|
||||
behaviours, keeping the horizontal/vertical and grow/shrink pairing.
|
||||
behaviors, keeping the horizontal/vertical and grow/shrink pairing.
|
||||
- **Overview on `SUPER + grave`, not a bare SUPER tap.** Tap-detection on a
|
||||
modifier misfires when you're quick with SUPER combos.
|
||||
- **Lock on `CTRL + ALT + L`.** `SUPER + L` is "focus right" in this keymap.
|
||||
|
||||
@@ -23,8 +23,21 @@ hl.on("hyprland.start", function()
|
||||
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP=Hyprland")
|
||||
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
||||
|
||||
-- Units: polkit prompts, wallpaper, launcher daemon, idle/lock.
|
||||
hl.exec_cmd("systemctl --user start hyprpolkitagent.service hyprpaper.service vicinae.service hypridle.service")
|
||||
-- Units: polkit prompts, wallpaper, launcher daemon, idle/lock. All four
|
||||
-- carry `ConditionEnvironment=WAYLAND_DISPLAY`, and hl.exec_cmd fires
|
||||
-- commands without waiting for them to finish, so the dbus-update call
|
||||
-- above racing this one is not safe to assume complete -- a lost race
|
||||
-- leaves the Condition unmet and the unit silently never starts (exit 0,
|
||||
-- no error). hypridle is the only listener for the logind Lock signal,
|
||||
-- so that failure mode is "lock-session goes to nobody". Re-import
|
||||
-- synchronously in the same shell invocation first so the Condition
|
||||
-- always sees it, regardless of how the dbus-update call above scheduled.
|
||||
-- panama-polkit-agent replaces hyprpolkitagent, whose prompt is compiled
|
||||
-- into its binary and cannot be themed. Only one agent may register per
|
||||
-- session, so they must not both start. hyprpolkitagent stays INSTALLED as
|
||||
-- the fallback: `systemctl --user start hyprpolkitagent` restores the stock
|
||||
-- prompt if Panama's ever fails to come up.
|
||||
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user start panama-polkit-agent.service hyprpaper.service vicinae.service hypridle.service")
|
||||
|
||||
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
||||
-- No systemd unit ships with quickshell, so it runs as a compositor child.
|
||||
@@ -39,18 +52,22 @@ hl.on("hyprland.start", function()
|
||||
-- Keyring unlock, for Nextcloud and Bitwarden credential storage.
|
||||
hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=secrets,ssh,pkcs11")
|
||||
|
||||
-- Tray applications carried over from ~/.config/autostart. These need the
|
||||
-- Quickshell tray (an SNI host) to be up, hence starting after it.
|
||||
-- Nextcloud and Bitwarden are NOT started here. Both ship a
|
||||
-- ~/.config/autostart/*.desktop entry, and systemd's own
|
||||
-- systemd-xdg-autostart-generator turns every such entry into a
|
||||
-- `PartOf=graphical-session.target` unit (`[email protected]`,
|
||||
-- `[email protected]`) that fires once the uwsm
|
||||
-- session brings up graphical-session.target -- confirmed live via
|
||||
-- `systemctl --user list-units 'app-*@autostart.service'`. An explicit
|
||||
-- second launch here used to duplicate that: for Bitwarden specifically,
|
||||
-- each `flatpak run` gets its own sandbox instance, so the two starts
|
||||
-- didn't just race, they left two competing processes fighting over the
|
||||
-- app's single-instance lock, with neither reliably owning a usable
|
||||
-- window. Trust the generator instead of re-launching.
|
||||
--
|
||||
-- Commands are copied verbatim from the GNOME .desktop files rather than
|
||||
-- guessed: Bitwarden is a flatpak with no `bitwarden` binary on PATH, and
|
||||
-- its autostart entry launches a specific script with --autostart.
|
||||
--
|
||||
-- RustDesk is deliberately absent: it ships an enabled *system* service
|
||||
-- (`rustdesk --service`) that spawns --server and --tray for the session on
|
||||
-- its own. Starting it here as well would give you two trays.
|
||||
hl.exec_cmd("nextcloud --background")
|
||||
hl.exec_cmd("flatpak run --command=bitwarden.sh com.bitwarden.desktop --autostart")
|
||||
-- RustDesk is deliberately absent too: it ships an enabled *system*
|
||||
-- service (`rustdesk --service`) that spawns --server and --tray for the
|
||||
-- session on its own. Starting it here as well would give you two trays.
|
||||
end)
|
||||
|
||||
hl.on("hyprland.shutdown", function()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Environment
|
||||
--
|
||||
-- hl.env() sets variables before the display server initialises, which is what
|
||||
-- hl.env() sets variables before the display server initializes, which is what
|
||||
-- makes it valid for AQ_* and cursor variables.
|
||||
--
|
||||
-- NOTE for uwsm sessions: if you log in via "Hyprland (uwsm-managed)", uwsm
|
||||
@@ -9,6 +9,8 @@
|
||||
-- instead. They still work here; see the session notes in autostart.lua.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local prefs = require("prefs")
|
||||
|
||||
-- ── GPU selection ───────────────────────────────────────────────────────────
|
||||
-- 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.
|
||||
@@ -47,7 +49,7 @@ end
|
||||
-- (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
|
||||
-- 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")
|
||||
|
||||
-- ── Toolkits ────────────────────────────────────────────────────────────────
|
||||
@@ -56,7 +58,7 @@ hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
||||
|
||||
-- "gtk3" makes Qt derive its palette from the live GTK3 theme via the qgtk3
|
||||
-- platform plugin, so Qt apps match adw-gtk3-dark exactly rather than
|
||||
-- approximating it -- and it gives them GTK file/font/colour dialogs. It also
|
||||
-- approximating it -- and it gives them GTK file/font/color dialogs. It also
|
||||
-- covers Qt5 and Qt6 with one value.
|
||||
--
|
||||
-- Note this is a single value, NOT a fallback list: Qt splits on ":" and uses
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# hypridle — idle behaviour
|
||||
# hypridle — idle behavior
|
||||
#
|
||||
# Mirrors the GNOME power profile this replaces, exactly:
|
||||
# org.gnome.desktop.session idle-delay = 300 (screen off at 5min)
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
general {
|
||||
# `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
|
||||
after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Shared preferences first: looks/input/monitors read their defaults through it.
|
||||
-- It never raises, so a missing or malformed settings file costs customisations
|
||||
-- It never raises, so a missing or malformed settings file costs customizations
|
||||
-- and nothing else.
|
||||
require("prefs")
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# hyprlock — lock screen
|
||||
# hyprlock — lock screen.
|
||||
#
|
||||
# Tokyo Night Moon, matching quickshell/config/Theme.qml:
|
||||
# accent #82aaff fg #c8d3f5 dim #828bb8 bg #222436 red #ff757f
|
||||
# GENERATED FILE. Edit hyprlock.conf.template and re-run
|
||||
# quickshell/scripts/panama-theme-apps; editing this copy is overwritten on the
|
||||
# next color scheme change.
|
||||
#
|
||||
# The colors here follow the desktop's light/dark setting. They used to be
|
||||
# hardcoded Tokyo Night Moon, which meant the one screen you see most often
|
||||
# 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.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -45,7 +54,7 @@ background {
|
||||
vibrancy_darkness = 0.05
|
||||
|
||||
# Shown if the screenshot is unavailable.
|
||||
color = rgba(34, 36, 54, 1.0)
|
||||
color = rgba(@BG@, 1.0)
|
||||
|
||||
zindex = -1
|
||||
}
|
||||
@@ -54,7 +63,7 @@ background {
|
||||
label {
|
||||
monitor =
|
||||
text = cmd[update:1000] date +"%-I:%M"
|
||||
color = rgba(200, 211, 245, 1.0)
|
||||
color = rgba(@FG@, 1.0)
|
||||
font_size = 120
|
||||
font_family = Adwaita Sans Light
|
||||
position = 0, 260
|
||||
@@ -65,7 +74,7 @@ label {
|
||||
label {
|
||||
monitor =
|
||||
text = cmd[update:60000] date +"%A, %B %-d"
|
||||
color = rgba(130, 139, 184, 1.0)
|
||||
color = rgba(@MUTED@, 1.0)
|
||||
font_size = 24
|
||||
font_family = Adwaita Sans
|
||||
position = 0, 160
|
||||
@@ -84,18 +93,18 @@ input-field {
|
||||
outline_thickness = 2
|
||||
rounding = 26
|
||||
|
||||
outer_color = rgba(130, 170, 255, 0.9)
|
||||
inner_color = rgba(46, 47, 61, 0.85)
|
||||
font_color = rgba(200, 211, 245, 1.0)
|
||||
check_color = rgba(130, 170, 255, 1.0)
|
||||
fail_color = rgba(255, 117, 127, 1.0)
|
||||
outer_color = rgba(@ACCENT@, 0.9)
|
||||
inner_color = rgba(@FIELD@, 0.85)
|
||||
font_color = rgba(@FG@, 1.0)
|
||||
check_color = rgba(@ACCENT@, 1.0)
|
||||
fail_color = rgba(@ERROR@, 1.0)
|
||||
|
||||
dots_size = 0.25
|
||||
dots_spacing = 0.3
|
||||
dots_center = true
|
||||
|
||||
placeholder_text = <span foreground="##828bb8"><i>Password</i></span>
|
||||
fail_text = <span foreground="##ff757f"><i>$FAIL ($ATTEMPTS)</i></span>
|
||||
placeholder_text = <span foreground="##@MUTED_HEX@"><i>Password</i></span>
|
||||
fail_text = <span foreground="##@ERROR_HEX@"><i>$FAIL ($ATTEMPTS)</i></span>
|
||||
|
||||
fade_on_empty = false
|
||||
hide_input = false
|
||||
@@ -105,7 +114,7 @@ input-field {
|
||||
label {
|
||||
monitor =
|
||||
text = $USER
|
||||
color = rgba(200, 211, 245, 0.9)
|
||||
color = rgba(@FG@, 0.9)
|
||||
font_size = 16
|
||||
font_family = Adwaita Sans
|
||||
position = 0, -110
|
||||
@@ -9,7 +9,7 @@
|
||||
# hyprland-guiutils hyprland-dialog / hyprland-run
|
||||
# hyprpolkitagent authentication prompts (via hyprland-qt-support)
|
||||
#
|
||||
# Colours are Tokyo Night Moon, matching quickshell/config/Theme.qml and
|
||||
# Colors are Tokyo Night Moon, matching quickshell/config/Theme.qml and
|
||||
# hypr/looks.lua. Format is hyprlang; path is fixed at
|
||||
# $XDG_CONFIG_HOME/hypr/hyprtoolkit.conf (confirmed in libhyprtoolkit).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -10,9 +10,9 @@ local prefs = require("prefs")
|
||||
hl.config({
|
||||
input = {
|
||||
kb_layout = prefs.get("keyboardLayout", "us"),
|
||||
kb_variant = "",
|
||||
kb_variant = prefs.get("keyboardVariant", ""),
|
||||
kb_model = "",
|
||||
kb_options = "",
|
||||
kb_options = prefs.get("keyboardOptions", "caps:escape_shifted_capslock"),
|
||||
kb_rules = "",
|
||||
|
||||
numlock_by_default = prefs.get("numlockByDefault", true),
|
||||
@@ -21,10 +21,18 @@ hl.config({
|
||||
repeat_delay = prefs.get("keyRepeatDelay", 500),
|
||||
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 behavior", 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 behavior
|
||||
-- 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),
|
||||
|
||||
-- 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,
|
||||
|
||||
-- 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
|
||||
-- window against the already-running server (started in autostart.lua).
|
||||
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.
|
||||
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 .. " + C", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
||||
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" })
|
||||
|
||||
-- ── Launcher ────────────────────────────────────────────────────────────────
|
||||
@@ -115,7 +118,7 @@ bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd(qs("focus", "reveal")), { descriptio
|
||||
-- which Hyprland has no equivalent for.
|
||||
bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" })
|
||||
|
||||
-- Notification centre.
|
||||
-- Notification center.
|
||||
bind(mod .. " + B", hl.dsp.exec_cmd(qs("notifications", "toggle")), { description = "Notifications" })
|
||||
|
||||
-- Screenshot / screen record. One key, then pick screen / window / region and
|
||||
@@ -130,8 +133,8 @@ bind("ALT + Print", hl.dsp.exec_cmd(qs("capture", "windowNow")), { description =
|
||||
bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd(qs("screen-intelligence", "open")),
|
||||
{ description = "Screen Intelligence" })
|
||||
|
||||
-- Colour picker: copies the hex under the cursor to the clipboard.
|
||||
bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Colour picker" })
|
||||
-- Color picker: copies the hex under the cursor to the clipboard.
|
||||
bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd("hyprpicker -a -f hex"), { description = "Color picker" })
|
||||
|
||||
-- ── Window management ───────────────────────────────────────────────────────
|
||||
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||
@@ -177,24 +180,46 @@ bind(mod .. " + CTRL + L", hl.dsp.window.swap({ direction = "r" }), { descriptio
|
||||
|
||||
-- Resize (Forge: window-resize-<edge>-<increase|decrease>).
|
||||
--
|
||||
-- Forge resized one named edge at a time. Hyprland resizes the active window
|
||||
-- along an axis and lets the layout decide which edge actually moves, so the
|
||||
-- eight Forge keys collapse onto four behaviours. The pairing is kept
|
||||
-- consistent with the original: Y/B/O/M are horizontal, I/P/U/N are vertical,
|
||||
-- and "increase" always grows while "decrease" always shrinks.
|
||||
-- Forge resized one named EDGE at a time: its resize() grows the window for a
|
||||
-- positive amount in every direction, and the edge only decides which side
|
||||
-- moves -- Y grew leftward, O grew rightward, and so on. Hyprland resizes along
|
||||
-- an axis and lets the layout choose the border, so those eight distinct
|
||||
-- behaviours collapse onto four and the direction is simply not expressible.
|
||||
--
|
||||
-- Because of that the sizes are deliberately INVERTED from Forge's naming.
|
||||
-- Carried over faithfully, "increase" grew and "decrease" shrank, which was
|
||||
-- correct on paper and wrong under the fingers: with the edge gone, the keys
|
||||
-- that used to pull a window open from one side now push it from the other.
|
||||
-- Gabriel uses these daily and reads Y/O as shrink and B/M as grow, so that is
|
||||
-- what they do. Faithfulness to a mapping nobody can feel is not worth much.
|
||||
local step = 60
|
||||
bind(mod .. " + SHIFT + Y", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
||||
bind(mod .. " + SHIFT + O", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
||||
bind(mod .. " + SHIFT + B", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||
bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||
bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||
bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||
bind(mod .. " + SHIFT + P", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||
bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||
bind(mod .. " + SHIFT + Y", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||
bind(mod .. " + SHIFT + O", hl.dsp.window.resize({ x = -step, y = 0, relative = true }), { repeating = true, description = "Narrower" })
|
||||
bind(mod .. " + SHIFT + B", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
||||
bind(mod .. " + SHIFT + M", hl.dsp.window.resize({ x = step, y = 0, relative = true }), { repeating = true, description = "Wider" })
|
||||
bind(mod .. " + SHIFT + I", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||
bind(mod .. " + SHIFT + U", hl.dsp.window.resize({ x = 0, y = -step, relative = true }), { repeating = true, description = "Shorter" })
|
||||
-- SUPER+SHIFT+P was double-bound with the color picker above; moved to
|
||||
-- Comma, which continues the bottom-row cluster (B/M/N) this axis already
|
||||
-- uses rather than landing on an arbitrary free key.
|
||||
bind(mod .. " + SHIFT + Comma", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||
bind(mod .. " + SHIFT + N", hl.dsp.window.resize({ x = 0, y = step, relative = true }), { repeating = true, description = "Taller" })
|
||||
|
||||
-- Window cycling (GNOME: cycle-windows on SUPER+Tab).
|
||||
bind(mod .. " + Tab", hl.dsp.window.cycle_next({ next = true }), { description = "Next window" })
|
||||
bind(mod .. " + SHIFT + Tab", hl.dsp.window.cycle_next({ next = false }), { description = "Previous window" })
|
||||
-- Window cycling (GNOME: cycle-windows on SUPER+Tab), now with an overlay
|
||||
-- showing what you are choosing between.
|
||||
--
|
||||
-- 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.
|
||||
bind(mod .. " + SHIFT + grave", hl.dsp.focus({ last = true }), { description = "Last window" })
|
||||
|
||||
@@ -208,6 +233,17 @@ bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true, description
|
||||
-- Plain relative selectors ("+1" / "-1") reproduce GNOME's dynamic workspaces:
|
||||
-- moving right past the last workspace creates a new one, and moving left from
|
||||
-- the first clamps instead of wrapping.
|
||||
|
||||
-- Behavior for the relative/cyclic binds below. These are Hyprland's own
|
||||
-- `binds:` options -- not part of general/dwindle -- and have no other home
|
||||
-- in the config, so they are read here rather than in looks.lua.
|
||||
hl.config({
|
||||
binds = {
|
||||
workspace_back_and_forth = prefs.get("workspaceBackAndForth", false),
|
||||
allow_workspace_cycles = prefs.get("allowWorkspaceCycles", false),
|
||||
},
|
||||
})
|
||||
|
||||
bind("ALT + H", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||
bind("ALT + L", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||
bind("ALT + SHIFT + H", hl.dsp.window.move({ workspace = "-1" }), { description = "Move window to workspace left" })
|
||||
@@ -230,18 +266,18 @@ end
|
||||
bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||
bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||
|
||||
-- Minimise, as far as Hyprland has one.
|
||||
-- Minimize, as far as Hyprland has one.
|
||||
--
|
||||
-- Hyprland has no minimise: it receives the request (the binary has
|
||||
-- Hyprland has no minimize: it receives the request (the binary has
|
||||
-- setSetMinimized handlers for xdg, XWayland and foreign-toplevel) but exposes
|
||||
-- no dispatcher, no config option and not even an event to hook, so titlebar
|
||||
-- minimise buttons are inert and cannot be made to work. A tiling WM has no
|
||||
-- minimize buttons are inert and cannot be made to work. A tiling WM has no
|
||||
-- iconified state and no taskbar to restore from.
|
||||
--
|
||||
-- The scratchpad is the honest equivalent: the window goes away, and the same
|
||||
-- key brings it back. Bound to X to match the muscle memory it replaces.
|
||||
bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimised)" })
|
||||
bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimise to scratchpad" })
|
||||
bind(mod .. " + X", hl.dsp.workspace.toggle_special("scratch"), { description = "Toggle scratchpad (restore minimized)" })
|
||||
bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimize to scratchpad" })
|
||||
|
||||
-- ── Session ─────────────────────────────────────────────────────────────────
|
||||
-- GNOME's lock was SUPER+L, which is "focus right" here, so lock moves to
|
||||
@@ -252,23 +288,23 @@ bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { descri
|
||||
-- ── Media and volume ────────────────────────────────────────────────────────
|
||||
-- locked = true keeps these working on the lock screen, as they do in GNOME.
|
||||
-- 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("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 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("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true , description = "Mute microphone" })
|
||||
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd(osd("volume up 6")), { locked = true, repeating = true , description = "Volume up" })
|
||||
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd(osd("volume down 6")), { locked = true, repeating = true , description = "Volume down" })
|
||||
bind("XF86AudioMute", hl.dsp.exec_cmd(osd("volume toggle")), { locked = true , description = "Mute" })
|
||||
bind("XF86AudioMicMute", hl.dsp.exec_cmd(osd("microphone toggle")), { locked = true , description = "Mute microphone" })
|
||||
|
||||
-- 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 + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 1%-"), { locked = true, repeating = true , description = "Volume down (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(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("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true , description = "Play or pause" })
|
||||
bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true , description = "Next track" })
|
||||
bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true , description = "Previous track" })
|
||||
bind("XF86AudioStop", hl.dsp.exec_cmd("playerctl stop"), { locked = true , description = "Stop playback" })
|
||||
bind("XF86AudioPlay", hl.dsp.exec_cmd(osd("media 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(osd("media next")), { locked = true , description = "Next track" })
|
||||
bind("XF86AudioPrev", hl.dsp.exec_cmd(osd("media previous")), { locked = true , description = "Previous track" })
|
||||
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("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true , description = "Brightness down" })
|
||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd(osd("brightness up 5")), { locked = true, repeating = true , description = "Brightness up" })
|
||||
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.
|
||||
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||
|
||||
+90
-25
@@ -1,8 +1,8 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Look and feel -- Tokyo Night Moon
|
||||
--
|
||||
-- Colours here must stay in sync with quickshell/config/Theme.qml.
|
||||
-- accent #82aaff borders / focus
|
||||
-- Colors here must stay in sync with quickshell/config/Theme.qml.
|
||||
-- accent user-selectable, see `accents` below -- blue (#82aaff) ships
|
||||
-- bg #222436 base
|
||||
--
|
||||
-- Performance note: every animation below is event-driven. Nothing uses the
|
||||
@@ -14,6 +14,30 @@
|
||||
|
||||
local prefs = require("prefs")
|
||||
|
||||
-- Mirrors config/Theme.qml's `accents` map. Lua has no import path into a QML
|
||||
-- singleton, so the hex pairs are restated here -- just the four hex strings
|
||||
-- each named accent needs, not the labels, which stay UI-only.
|
||||
local accents = {
|
||||
blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
|
||||
orchid = { dark = "c099ff", darkSecondary = "fca7ea", light = "7847bd", lightSecondary = "9854f1" },
|
||||
teal = { dark = "86e1fc", darkSecondary = "82aaff", light = "007197", lightSecondary = "2e7de9" },
|
||||
green = { dark = "c3e88d", darkSecondary = "86e1fc", light = "587539", lightSecondary = "007197" },
|
||||
amber = { dark = "ffc777", darkSecondary = "ff966c", light = "8c6c3e", lightSecondary = "b15c00" },
|
||||
orange = { dark = "ff966c", darkSecondary = "ff757f", light = "b15c00", lightSecondary = "c64343" },
|
||||
rose = { dark = "ff757f", darkSecondary = "c099ff", light = "f52a65", lightSecondary = "9854f1" },
|
||||
slate = { dark = "828bb8", darkSecondary = "82aaff", light = "6172b0", lightSecondary = "2e7de9" },
|
||||
}
|
||||
|
||||
-- The accent pair a fresh session or `hyprctl reload` starts from.
|
||||
-- services/ColorScheme.qml overwrites this live, from the same Theme.accents
|
||||
-- data, once the shell settles (~1.2s after startup -- see its `settle`
|
||||
-- Timer). This table exists only so the compositor is never observably blue
|
||||
-- for a non-blue accent during the gap before that first live apply.
|
||||
local accentScheme = prefs.get("colorScheme", "dark")
|
||||
local accentPair = accents[prefs.get("accentName", "blue")] or accents.blue
|
||||
local accentStart = accentScheme == "light" and accentPair.light or accentPair.dark
|
||||
local accentEnd = accentScheme == "light" and accentPair.lightSecondary or accentPair.darkSecondary
|
||||
|
||||
hl.config({
|
||||
general = {
|
||||
gaps_in = prefs.get("gapsIn", 5),
|
||||
@@ -22,17 +46,24 @@ hl.config({
|
||||
border_size = prefs.get("borderSize", 2),
|
||||
|
||||
col = {
|
||||
-- The prism: blue leads, orchid follows, on a diagonal so the pair
|
||||
-- is visible on both a tall and a wide window. Same two colours as
|
||||
-- the shell's hairline (quickshell/widgets/PrismEdge.qml) and the
|
||||
-- tmux theme this palette came from.
|
||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
||||
-- Unfocused windows get no colour at all. The gradient only means
|
||||
-- something if exactly one window on screen is wearing it.
|
||||
inactive_border = "rgba(3b426199)",
|
||||
-- The focused accent role, on a diagonal so the pair is visible
|
||||
-- on both a tall and a wide window. Driven by the chosen
|
||||
-- accentName (see the `accents` table above); services/
|
||||
-- ColorScheme.qml applies the same values live, and restates them
|
||||
-- from Theme.accent/Theme.accentSecondary on every scheme change
|
||||
-- too, since each accent carries a separate pair per scheme.
|
||||
active_border = { colors = { "rgba(" .. accentStart .. "ee)", "rgba(" .. accentEnd .. "ee)" }, angle = 115 },
|
||||
-- The neutral inactive role follows the color scheme because a
|
||||
-- dark neutral disappears against a light desktop.
|
||||
-- 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.
|
||||
-- Harmless on its own; tearing only happens where a rule opts in.
|
||||
@@ -40,16 +71,22 @@ hl.config({
|
||||
|
||||
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 = {
|
||||
-- 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.
|
||||
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),
|
||||
|
||||
blur = {
|
||||
@@ -79,9 +116,13 @@ hl.config({
|
||||
shadow = {
|
||||
enabled = prefs.get("shadowEnabled", true),
|
||||
range = prefs.get("shadowRange", 20),
|
||||
render_power = 3,
|
||||
sharp = false,
|
||||
render_power = prefs.getInt("shadowRenderPower", 3),
|
||||
sharp = prefs.get("shadowSharp", false),
|
||||
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 },
|
||||
scale = 1.0,
|
||||
},
|
||||
@@ -89,11 +130,13 @@ hl.config({
|
||||
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
||||
-- border is the signature, and a strong halo would compete with it.
|
||||
-- This is just enough to lift the focused window off the wallpaper.
|
||||
-- Derived from the same accent as active_border above, not hardcoded,
|
||||
-- so the halo never disagrees with the border it surrounds.
|
||||
glow = {
|
||||
enabled = prefs.get("glowEnabled", true),
|
||||
range = prefs.get("glowRange", 8),
|
||||
render_power = 2,
|
||||
color = "rgba(82aaff33)",
|
||||
color = "rgba(" .. accentStart .. "33)",
|
||||
color_inactive = "rgba(00000000)",
|
||||
},
|
||||
|
||||
@@ -106,16 +149,38 @@ hl.config({
|
||||
dwindle = {
|
||||
-- Keep the split orientation a window was created with. Closest match
|
||||
-- to how the Forge extension behaved on GNOME.
|
||||
preserve_split = true,
|
||||
preserve_split = prefs.get("preserveSplit", 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 = {
|
||||
force_default_wallpaper = 0,
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
-- Stored as "show the logo / show the splash" and written as Hyprland's
|
||||
-- `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
|
||||
-- content type is "video" or "game" -- the tag rules.lua applies to
|
||||
@@ -128,12 +193,12 @@ hl.config({
|
||||
-- Let a crashed lock screen be recovered rather than stranding you.
|
||||
allow_session_lock_restore = true,
|
||||
|
||||
-- Don't let apps steal focus by shouting; matches GNOME's behaviour.
|
||||
-- Don't let apps steal focus by shouting; matches GNOME's behavior.
|
||||
focus_on_activate = false,
|
||||
},
|
||||
|
||||
render = {
|
||||
-- Colour management is stable and on by default in 0.56.
|
||||
-- Color management is stable and on by default in 0.56.
|
||||
cm_enabled = true,
|
||||
|
||||
-- 1 = automatically flip the monitor into HDR for fullscreen content
|
||||
@@ -164,8 +229,8 @@ hl.config({
|
||||
},
|
||||
|
||||
ecosystem = {
|
||||
no_update_news = true,
|
||||
no_donation_nag = true,
|
||||
no_update_news = not prefs.get("hyprlandUpdateNews", false),
|
||||
no_donation_nag = not prefs.get("hyprlandDonationNag", false),
|
||||
},
|
||||
|
||||
xwayland = {
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
local prefs = require("prefs")
|
||||
|
||||
-- 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. Color management and bit depth
|
||||
-- stay here, because those are the settings with a documented reason attached
|
||||
-- (see the header) rather than preferences, and a settings page has no way to
|
||||
-- explain the screencopy tradeoff at the moment you would be changing it.
|
||||
@@ -71,6 +74,26 @@ local function valid_transform(transform)
|
||||
and transform <= 3
|
||||
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)
|
||||
if type(output) ~= "string" or output == ""
|
||||
or output:match("^[%w_.-]+$") == nil then
|
||||
@@ -85,9 +108,23 @@ local function display_entry(output)
|
||||
or not valid_transform(entry.transform) then
|
||||
return nil
|
||||
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
|
||||
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_scale = 1.5
|
||||
local shipped_transform = 0
|
||||
@@ -96,7 +133,7 @@ local dp2 = display_entry("DP-2")
|
||||
hl.monitor({
|
||||
output = "DP-2",
|
||||
mode = dp2 and dp2.mode or shipped_mode,
|
||||
position = "0x0",
|
||||
position = display_position(dp2, "0x0"),
|
||||
scale = dp2 and dp2.scale or shipped_scale,
|
||||
transform = dp2 and dp2.transform or shipped_transform,
|
||||
|
||||
@@ -110,7 +147,7 @@ hl.monitor({
|
||||
})
|
||||
|
||||
-- Other connected outputs use the same validated per-output store. They keep
|
||||
-- automatic placement and the compositor's normal colour policy; DP-2 alone
|
||||
-- automatic placement and the compositor's normal color policy; DP-2 alone
|
||||
-- carries the panel-specific 10-bit policy documented above.
|
||||
for output, _ in pairs(displays) do
|
||||
if output ~= "DP-2" then
|
||||
@@ -119,7 +156,7 @@ for output, _ in pairs(displays) do
|
||||
hl.monitor({
|
||||
output = output,
|
||||
mode = entry.mode,
|
||||
position = "auto",
|
||||
position = display_position(entry, "auto"),
|
||||
scale = entry.scale,
|
||||
transform = entry.transform,
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
-- cannot drift apart.
|
||||
--
|
||||
-- Nothing here may raise. A missing, empty, truncated, or actively malformed
|
||||
-- file must cost the user nothing worse than their customisations; it must
|
||||
-- file must cost the user nothing worse than their customizations; it must
|
||||
-- never cost them a working compositor. Every failure path returns the caller's
|
||||
-- fallback.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -69,7 +69,7 @@ hl.window_rule({
|
||||
float = true,
|
||||
})
|
||||
|
||||
-- Settings-style utility windows and pickers: float and centre, like GNOME did.
|
||||
-- Settings-style utility windows and pickers: float and center, like GNOME did.
|
||||
hl.window_rule({
|
||||
match = { class = "^(pavucontrol|org\\.pulseaudio\\.pavucontrol|nm-connection-editor|blueman-manager|org\\.gnome\\.Settings)$" },
|
||||
float = true,
|
||||
@@ -77,6 +77,18 @@ hl.window_rule({
|
||||
center = true,
|
||||
})
|
||||
|
||||
-- Quick Look. The GNOME previewer is what the file manager opens on space,
|
||||
-- and it is an overlay rather than a window someone manages: tiled, it shoves
|
||||
-- the file manager aside and has to be dismissed before the list is usable
|
||||
-- again. Sized generously because a preview that needs zooming is not a
|
||||
-- preview; it still gets a margin so the file underneath stays visible.
|
||||
hl.window_rule({
|
||||
match = { class = "^org\\.gnome\\.NautilusPreviewer$" },
|
||||
float = true,
|
||||
size = { "monitor_w * 0.7", "monitor_h * 0.8" },
|
||||
center = true,
|
||||
})
|
||||
|
||||
-- Portal dialogs (file chooser, screen share picker) should always float.
|
||||
hl.window_rule({
|
||||
match = { class = "^(xdg-desktop-portal-gtk|org\\.freedesktop\\.impl\\.portal\\.desktop\\.gtk|hyprland-share-picker)$" },
|
||||
@@ -150,7 +162,7 @@ hl.layer_rule({
|
||||
ignore_alpha = 0.3,
|
||||
})
|
||||
|
||||
-- Popovers: calendar, quick settings, notification centre, tray menus.
|
||||
-- Popovers: calendar, quick settings, notification center, tray menus.
|
||||
hl.layer_rule({
|
||||
name = "qs-popover",
|
||||
match = { namespace = "^qs-popover" },
|
||||
|
||||
@@ -183,7 +183,6 @@ font_size 14.0
|
||||
|
||||
#: Cursor customization {{{
|
||||
|
||||
cursor #c8d3f5
|
||||
|
||||
#: 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
|
||||
@@ -193,7 +192,6 @@ cursor #c8d3f5
|
||||
#: precedence. Also, the cursor colors are modified if the cell
|
||||
#: 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 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
|
||||
#: much effort.
|
||||
|
||||
url_color #4fd6be
|
||||
# url_style curly
|
||||
|
||||
#: 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
|
||||
#: 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
|
||||
#: not draw borders around the active window.
|
||||
|
||||
inactive_border_color #2f334d
|
||||
|
||||
#: 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
|
||||
#: tab_title_template.
|
||||
|
||||
active_tab_foreground #1e2030
|
||||
active_tab_background #82aaff
|
||||
# active_tab_font_style bold-italic
|
||||
inactive_tab_foreground #c8d3f5
|
||||
inactive_tab_background #42465a
|
||||
# inactive_tab_font_style normal
|
||||
|
||||
#: Tab bar colors and styles.
|
||||
|
||||
tab_bar_background #222436
|
||||
|
||||
#: Background color for the tab bar. Defaults to using the terminal
|
||||
#: background color.
|
||||
@@ -983,8 +973,8 @@ tab_bar_margin_color none
|
||||
|
||||
#: Color scheme {{{
|
||||
|
||||
foreground #c8d3f5
|
||||
background #222436
|
||||
|
||||
include current-theme.conf
|
||||
|
||||
#: 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
|
||||
#: 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
|
||||
#: 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
|
||||
#: remaining 240 colors as color16 to color255.
|
||||
|
||||
color0 #1b1d2b
|
||||
color8 #444a73
|
||||
|
||||
#: black
|
||||
|
||||
color1 #ff757f
|
||||
color9 #ff757f
|
||||
|
||||
#: red
|
||||
|
||||
color2 #a5e8b5
|
||||
color10 #a5e8b5
|
||||
|
||||
#: green
|
||||
color3 #ffc777
|
||||
color11 #ffc777
|
||||
|
||||
#: yellow
|
||||
|
||||
color4 #82aaff
|
||||
color12 #82aaff
|
||||
|
||||
#: blue
|
||||
|
||||
color5 #c099ff
|
||||
color13 #c099ff
|
||||
|
||||
#: magenta
|
||||
|
||||
color6 #86e1fc
|
||||
color14 #86e1fc
|
||||
|
||||
#: cyan
|
||||
|
||||
color7 #828bb8
|
||||
color15 #c8d3f5
|
||||
|
||||
#: white
|
||||
|
||||
color16 #ff966c
|
||||
color17 #c53b53
|
||||
|
||||
# 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 color 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)
|
||||
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 color 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 color 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 color, a mid-gray gutter -- and
|
||||
-- applying them to Day would put light gray text on a light background, which
|
||||
-- is exactly the legibility problem they exist to solve, inverted.
|
||||
|
||||
local panama = require("config.panama")
|
||||
|
||||
return {
|
||||
{
|
||||
"folke/tokyonight.nvim",
|
||||
opts = {
|
||||
style = "moon",
|
||||
opts = function()
|
||||
local light = panama.is_light()
|
||||
|
||||
return {
|
||||
style = panama.tokyonight_style(),
|
||||
light_style = "day",
|
||||
transparent = true,
|
||||
|
||||
on_colors = function(colors)
|
||||
if light then
|
||||
-- Day's defaults are tuned for a light ground and mostly need no
|
||||
-- help. Comments are the exception: the shipped #848cb5 measures
|
||||
-- 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
|
||||
-- dimmer than Normal's 4.52:1 so it does not compete with code.
|
||||
colors.comment = "#7079a8"
|
||||
return
|
||||
end
|
||||
|
||||
colors.comment = "#a0a7c5"
|
||||
colors.fg_gutter = "#787f93"
|
||||
colors.terminal_black = "#828bb8"
|
||||
end,
|
||||
|
||||
on_highlights = function(highlights, colors)
|
||||
-- Fix inline code visibility in markdown
|
||||
-- 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 = colors.terminal_black,
|
||||
bg = light and colors.bg_highlight or colors.terminal_black,
|
||||
fg = colors.fg,
|
||||
}
|
||||
highlights["RenderMarkdownCodeInline"] = {
|
||||
bg = colors.terminal_black,
|
||||
bg = light and colors.bg_highlight or colors.terminal_black,
|
||||
fg = colors.fg,
|
||||
}
|
||||
-- Fix LspReference* readability: DiagnosticUnnecessary dims fg for unused
|
||||
-- imports, making text nearly invisible against LspReferenceText's background
|
||||
highlights["LspReferenceText"] = { bg = colors.fg_gutter, fg = colors.fg }
|
||||
highlights["LspReferenceRead"] = { bg = colors.fg_gutter, fg = colors.fg }
|
||||
highlights["LspReferenceWrite"] = { bg = colors.fg_gutter, 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",
|
||||
|
||||
@@ -22,7 +22,7 @@ console_title_template = '{{ .Shell }} in {{ .Folder }}'
|
||||
[[blocks.segments]]
|
||||
type = 'git'
|
||||
style = 'plain'
|
||||
foreground = 'p:grey'
|
||||
foreground = 'p:gray'
|
||||
background = 'transparent'
|
||||
template = ' {{ .HEAD }}{{ if or (.Working.Changed) (.Staging.Changed) }}*{{ end }} <cyan>{{ if gt .Behind 0 }}⇣{{ end }}{{ if gt .Ahead 0 }}⇡{{ end }}</>'
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
import qs.modules.settings
|
||||
import qs.services
|
||||
|
||||
ShellRoot {
|
||||
Item {
|
||||
width: 680
|
||||
height: editor.implicitHeight
|
||||
|
||||
AccentEditor {
|
||||
id: editor
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
ThemeProfilePicker {
|
||||
width: parent.width
|
||||
visible: false
|
||||
}
|
||||
|
||||
AccentPicker {
|
||||
width: parent.width
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "accent-controls-test"
|
||||
|
||||
function status(): string {
|
||||
return JSON.stringify(ThemeProfiles.activeProfile);
|
||||
}
|
||||
|
||||
function adjust(target: string, channel: string, ratio: real): string {
|
||||
editor.changeChannel(target, channel, ratio);
|
||||
return status();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
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 recognizes 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.loaded = true;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Singleton {
|
||||
printErrors: false
|
||||
atomicWrites: true
|
||||
onSaved: root.saveError = ""
|
||||
onSaveFailed: error => root.saveError = "Could not save Home favourites."
|
||||
onSaveFailed: error => root.saveError = "Could not save Home favorites."
|
||||
|
||||
JsonAdapter {
|
||||
id: values
|
||||
|
||||
@@ -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 recognize -- and an unrecognized 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 };
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,31 @@ Singleton {
|
||||
label: "Automatically hide the Dock",
|
||||
detail: "Reveal it at the bottom edge when a workspace is occupied"
|
||||
},
|
||||
{
|
||||
key: "dockPosition", type: "enum", def: "bottom", group: "dock",
|
||||
label: "Position",
|
||||
detail: "Which edge the Dock lives on",
|
||||
options: [
|
||||
{ value: "bottom", label: "Bottom" },
|
||||
{ value: "left", label: "Left" },
|
||||
{ value: "right", label: "Right" }
|
||||
]
|
||||
},
|
||||
{
|
||||
// A "json" value: the screen names the Dock appears on. Empty means
|
||||
// every screen, which is both the sensible default and the right
|
||||
// answer for the common single-monitor case -- storing a list of
|
||||
// names there would go stale the moment a display is unplugged.
|
||||
key: "dockScreens", type: "json", def: [], group: "dock",
|
||||
label: "Screens",
|
||||
detail: "Which displays show the Dock"
|
||||
},
|
||||
{
|
||||
key: "dockIconSize", type: "int", def: 48, min: 32, max: 80, step: 4,
|
||||
unit: "px", group: "dock",
|
||||
label: "Icon size",
|
||||
detail: "How large the Dock's application icons are drawn"
|
||||
},
|
||||
{
|
||||
key: "dockRevealDelayMs", type: "int", def: 0, min: 0, max: 1000, step: 25,
|
||||
unit: "ms",
|
||||
@@ -107,6 +132,41 @@ Singleton {
|
||||
},
|
||||
|
||||
// ── Focus ───────────────────────────────────────────────────────────
|
||||
// ── Focus modes ─────────────────────────────────────────────────────
|
||||
// A "json" value: named modes, each with what turns it on and what it
|
||||
// does. Triggers rather than a scheduler -- a mode is on because a
|
||||
// condition is true right now, which is re-evaluated rather than fired
|
||||
// once. A schedule is one of those conditions ("is now inside this
|
||||
// window?"), which is why suspend, a reboot mid-window, and a lid
|
||||
// opened after the start time all behave correctly without special
|
||||
// cases: there is no alarm to have missed.
|
||||
//
|
||||
// Gaming ships enabled because the behaviour already existed as
|
||||
// gamingSilenceNotifications; Sleep ships disabled, because a desktop
|
||||
// that starts silencing someone on first boot has overstepped.
|
||||
{
|
||||
key: "focusModes", type: "json", group: "focus",
|
||||
label: "Focus modes",
|
||||
detail: "What quiets this machine, and what turns it on",
|
||||
def: [
|
||||
{
|
||||
id: "deep-work", name: "Deep work", enabled: true,
|
||||
triggers: [{ kind: "manual" }],
|
||||
durationMinutes: 45, silence: true, keepAwake: true, allow: []
|
||||
},
|
||||
{
|
||||
id: "gaming", name: "Gaming", enabled: true,
|
||||
triggers: [{ kind: "game" }],
|
||||
durationMinutes: 0, silence: true, keepAwake: true, allow: []
|
||||
},
|
||||
{
|
||||
id: "sleep", name: "Sleep", enabled: false,
|
||||
triggers: [{ kind: "schedule", start: "23:30", end: "07:00",
|
||||
days: [0, 1, 2, 3, 4, 5, 6] }],
|
||||
durationMinutes: 0, silence: true, keepAwake: false, allow: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "focusDurationMinutes", type: "int", def: 45, min: 5, max: 180, step: 5,
|
||||
unit: "min",
|
||||
@@ -128,10 +188,21 @@ Singleton {
|
||||
{
|
||||
key: "vrrPolicy", type: "enum", def: 3, group: "display",
|
||||
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: [
|
||||
{ value: 0, label: "Off" },
|
||||
{ value: 3, label: "Content-aware" }
|
||||
{ value: 0, label: "Off",
|
||||
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" }
|
||||
},
|
||||
@@ -140,8 +211,12 @@ Singleton {
|
||||
label: "Direct scanout",
|
||||
detail: "Lets fullscreen content bypass compositing",
|
||||
options: [
|
||||
{ value: 0, label: "Off" },
|
||||
{ value: 2, label: "Automatic" }
|
||||
{ value: 0, label: "Off",
|
||||
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" }
|
||||
},
|
||||
@@ -156,7 +231,7 @@ Singleton {
|
||||
unit: "px",
|
||||
group: "windows",
|
||||
label: "Inner gaps",
|
||||
detail: "Space between neighbouring tiled windows",
|
||||
detail: "Space between neighboring tiled windows",
|
||||
hypr: { path: ["general", "gaps_in"], option: "general:gaps_in", readAs: "css" }
|
||||
},
|
||||
{
|
||||
@@ -191,6 +266,147 @@ Singleton {
|
||||
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 behavior 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: "Center" }
|
||||
],
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
{
|
||||
key: "blurEnabled", type: "bool", def: true, group: "effects",
|
||||
@@ -226,6 +442,19 @@ Singleton {
|
||||
detail: "How far the shadow spreads from the window edge",
|
||||
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",
|
||||
label: "Focus glow",
|
||||
@@ -257,6 +486,24 @@ Singleton {
|
||||
detail: "XKB layout name, or a comma-separated list to switch between",
|
||||
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
|
||||
// serialized 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",
|
||||
label: "Num Lock on login",
|
||||
@@ -280,19 +527,35 @@ Singleton {
|
||||
hypr: { path: ["input", "repeat_rate"], option: "input:repeat_rate", readAs: "int" }
|
||||
},
|
||||
{
|
||||
key: "followMouse", type: "enum", def: 1, group: "input",
|
||||
label: "Focus follows pointer",
|
||||
detail: "Click to focus matches GNOME; sloppy focus follows the pointer",
|
||||
key: "followMouse", type: "enum", def: 1, group: "pointer",
|
||||
label: "Pointer focus",
|
||||
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 labeled "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: [
|
||||
{ value: 0, label: "Never" },
|
||||
{ value: 1, label: "Click to focus" },
|
||||
{ value: 2, label: "Sloppy focus" }
|
||||
{ value: 0, label: "Click to focus",
|
||||
detail: "Moving the pointer never changes 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" }
|
||||
},
|
||||
{
|
||||
key: "pointerSensitivity", type: "real", def: 0.0, min: -1.0, max: 1.0, step: 0.05,
|
||||
group: "input",
|
||||
group: "pointer",
|
||||
label: "Pointer speed",
|
||||
detail: "Zero is flat, unaccelerated response",
|
||||
hypr: { path: ["input", "sensitivity"], option: "input:sensitivity", readAs: "float" }
|
||||
@@ -300,7 +563,7 @@ Singleton {
|
||||
{
|
||||
key: "cursorInactiveTimeout", type: "int", def: 4, min: 0, max: 60, step: 1,
|
||||
unit: "s",
|
||||
group: "input",
|
||||
group: "pointer",
|
||||
label: "Hide pointer after",
|
||||
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
|
||||
@@ -310,6 +573,233 @@ Singleton {
|
||||
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: "highContrast", type: "bool", def: false, group: "accessibility",
|
||||
label: "High contrast",
|
||||
detail: "Increases contrast in applications that support it. Modern GTK applications read this from the desktop portal and restyle themselves; older ones need a high-contrast theme, which is not installed here.",
|
||||
// No hypr mapping: this is a GNOME interface setting the portal
|
||||
// republishes as org.freedesktop.appearance contrast, which is what
|
||||
// libadwaita actually reads. DesktopStyle applies it.
|
||||
},
|
||||
{
|
||||
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" }
|
||||
},
|
||||
|
||||
// ── Gaming ──────────────────────────────────────────────────────────
|
||||
// What Panama does while a game runs. gamemode tells us when that
|
||||
// starts and stops through its own hook scripts, so these are real
|
||||
// behaviours rather than hints -- and each one is undone afterwards to
|
||||
// whatever it was before, not to a default.
|
||||
{
|
||||
key: "gamingPerformanceProfile", type: "bool", def: true, group: "gaming",
|
||||
label: "Use the performance power profile",
|
||||
detail: "Switches while a game runs and switches back when it exits"
|
||||
},
|
||||
{
|
||||
key: "gamingNotifyOnStart", type: "bool", def: false, group: "gaming",
|
||||
label: "Say when Game Mode engages",
|
||||
detail: "A notification when a game requests it, which is otherwise invisible"
|
||||
},
|
||||
|
||||
// ── Night light ─────────────────────────────────────────────────────
|
||||
{
|
||||
key: "nightLightEnabled", type: "bool", def: false, group: "nightLight",
|
||||
@@ -341,6 +831,66 @@ Singleton {
|
||||
label: "Wallpaper",
|
||||
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 ───────────────────────────────────────────
|
||||
// Written into a generated hypridle config; see scripts/panama-idle.
|
||||
@@ -369,6 +919,209 @@ Singleton {
|
||||
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"
|
||||
},
|
||||
|
||||
// ── Color 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 colors, 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" }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "accentName", type: "enum", def: "blue", group: "appearance",
|
||||
label: "Accent color",
|
||||
detail: "Drives the focused window border, the bar hairline, and every active state",
|
||||
// NAMED accents, not a free color. Each name carries a curated
|
||||
// pair per scheme, because one hex cannot serve both: a color
|
||||
// legible on the dark ground is usually illegible on the light one.
|
||||
// The palette and its measured contrast live in config/Theme.qml,
|
||||
// which is also what stops this list drifting from what is drawn.
|
||||
options: [
|
||||
{ value: "blue", label: "Prism blue" },
|
||||
{ value: "orchid", label: "Orchid" },
|
||||
{ value: "teal", label: "Teal" },
|
||||
{ value: "green", label: "Green" },
|
||||
{ value: "amber", label: "Amber" },
|
||||
{ value: "orange", label: "Orange" },
|
||||
{ value: "rose", label: "Rose" },
|
||||
{ value: "slate", label: "Slate" }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "themeProfileId", type: "string", def: "moon", group: "appearance",
|
||||
pattern: "^[a-z0-9][a-z0-9-]{0,63}$",
|
||||
label: "Selected theme profile",
|
||||
detail: "The shipped or saved theme currently applied to the desktop",
|
||||
internal: true
|
||||
},
|
||||
{
|
||||
key: "themeProfiles", type: "json", def: [], group: "appearance",
|
||||
label: "Saved theme profiles",
|
||||
detail: "Named custom colour schemes and accent pairs",
|
||||
internal: true
|
||||
},
|
||||
|
||||
// ── 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 honor 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 ───────────────────────────────────────────────────
|
||||
// Backed by gsettings so GTK applications agree with the shell, and
|
||||
// pushed to the compositor as well where it has its own notion.
|
||||
@@ -402,6 +1155,47 @@ Singleton {
|
||||
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 utilization; 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 recognizable.
|
||||
{
|
||||
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 ──────────────────────────────────────────────────
|
||||
{
|
||||
key: "vitalsIntervalMs", type: "int", def: 2000, min: 500, max: 10000, step: 500,
|
||||
@@ -476,13 +1270,18 @@ Singleton {
|
||||
// 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
|
||||
// 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",
|
||||
label: "Pinned applications",
|
||||
detail: "Applications that stay in the Dock whether or not they are running",
|
||||
def: [
|
||||
"org.gnome.Settings", "kitty", "org.gnome.Nautilus",
|
||||
"panama-settings", "kitty", "org.gnome.Nautilus",
|
||||
"com.bitwarden.desktop", "org.gnome.Software", "helium",
|
||||
"org.mozilla.thunderbird_esr", "com.slack.Slack",
|
||||
"app.bluebubbles.BlueBubbles", "rustdesk",
|
||||
@@ -509,8 +1308,8 @@ Singleton {
|
||||
},
|
||||
|
||||
// ── Display configuration ───────────────────────────────────────────
|
||||
// { "<output>": { mode, scale, transform } }, applied by
|
||||
// hypr/monitors.lua on top of the shipped values. Colour management and
|
||||
// { "<output>": { mode, scale, transform, x, y, primary } }, applied by
|
||||
// hypr/monitors.lua on top of the shipped values. Color management and
|
||||
// bit depth are deliberately not here: those carry a documented
|
||||
// screencopy tradeoff that a settings page cannot explain at the moment
|
||||
// you would be changing it.
|
||||
@@ -518,7 +1317,22 @@ Singleton {
|
||||
key: "displays", type: "json", def: ({}), group: "display",
|
||||
internal: true,
|
||||
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 ────────────────────────────────────────────────────────
|
||||
@@ -608,7 +1422,7 @@ Singleton {
|
||||
case "string": {
|
||||
const text = typeof value === "string" ? value : String(value);
|
||||
// A constrained string is rejected rather than sanitised. Several
|
||||
// of these are serialised into an hl.config payload, and quietly
|
||||
// of these are serialized into an hl.config payload, and quietly
|
||||
// stripping characters would turn a typo into a different setting
|
||||
// instead of an error the user can see.
|
||||
if (entry.pattern && !new RegExp(entry.pattern).test(text))
|
||||
|
||||
@@ -22,12 +22,12 @@ Singleton {
|
||||
// ── Weather ─────────────────────────────────────────────────────────────
|
||||
// Coordinates taken from the GNOME night-light setting, which had already
|
||||
// resolved the location. Uses Open-Meteo, which needs no API key.
|
||||
readonly property real latitude: 27.7375
|
||||
readonly property real longitude: -82.6861
|
||||
readonly property real latitude: DesktopPreferences.get("weatherLatitude")
|
||||
readonly property real longitude: DesktopPreferences.get("weatherLongitude")
|
||||
// Open-Meteo returns coordinates but no friendly place name. Keep the
|
||||
// label deliberately general rather than exposing precise coordinates in
|
||||
// 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 int weatherRefreshMinutes: DesktopPreferences.get("weatherRefreshMinutes")
|
||||
|
||||
@@ -41,13 +41,13 @@ Singleton {
|
||||
|
||||
// amdgpu exposes utilisation here. Verified present on this machine; the
|
||||
// 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 ─────────────────────────────────────────────────────────
|
||||
// Matches the (disabled) GNOME schedule: 3500K from 17:00 to 10:00.
|
||||
readonly property int nightLightTemperature: DesktopPreferences.get("nightLightTemperature")
|
||||
readonly property real nightLightFrom: 17.0
|
||||
readonly property real nightLightTo: 10.0
|
||||
readonly property real nightLightFrom: DesktopPreferences.get("nightLightFrom")
|
||||
readonly property real nightLightTo: DesktopPreferences.get("nightLightTo")
|
||||
readonly property bool nightLightEnabledByDefault: DesktopPreferences.get("nightLightEnabled")
|
||||
|
||||
// ── Notifications ───────────────────────────────────────────────────────
|
||||
@@ -62,7 +62,12 @@ Singleton {
|
||||
readonly property int focusDurationMinutes: DesktopPreferences.get("focusDurationMinutes")
|
||||
|
||||
// ── Dock ────────────────────────────────────────────────────────────────
|
||||
// Pinned apps, in order, taken from the GNOME dash favourites.
|
||||
// Pinned apps, in order, taken from the GNOME dash favorites.
|
||||
readonly property string dockPosition: DesktopPreferences.get("dockPosition")
|
||||
readonly property var dockScreens: {
|
||||
const stored = DesktopPreferences.get("dockScreens");
|
||||
return Array.isArray(stored) ? stored : [];
|
||||
}
|
||||
readonly property var dockPinned: DesktopPreferences.get("dockPinned")
|
||||
|
||||
// Dash-to-Dock was set to intellihide against all windows: the dock hides
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
pragma Singleton
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tokyo Night Moon — the single source of truth for every colour, radius,
|
||||
// Tokyo Night Moon — the single source of truth for every color, radius,
|
||||
// duration and font in the shell. Nothing below this file should hardcode a
|
||||
// colour; if you find yourself writing "#" outside this file, add a token here.
|
||||
// color; if you find yourself writing "#" outside this file, add a token here.
|
||||
//
|
||||
// The bar keeps GNOME's 36px information density but sits directly on the
|
||||
// desktop, edge to edge. The dock retains the former glass vocabulary: bottom,
|
||||
@@ -14,40 +14,77 @@ import Quickshell
|
||||
// QtQuick is required even though nothing visual is declared here: `color` is a
|
||||
// QtQuick value type, and Qt.rgba() lives in its JS namespace.
|
||||
import QtQuick
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// ── Color 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 ─────────────────────────────────────────────────────────────
|
||||
// Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly.
|
||||
readonly property color bg: "#222436"
|
||||
readonly property color bgDark: "#1e2030"
|
||||
readonly property color bgHighlight: "#2f334d"
|
||||
readonly property color bgPanel: "#2e2f3d" // dock glass surface
|
||||
readonly property color bgPopover: "#21212f" // Openbar submenu background
|
||||
readonly property color bg: root.dark ? "#222436" : "#e1e2e7"
|
||||
readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de"
|
||||
readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da"
|
||||
readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface
|
||||
readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background
|
||||
|
||||
readonly property color fg: "#c8d3f5"
|
||||
readonly property color fgDim: "#828bb8"
|
||||
readonly property color fgMuted: "#636da6"
|
||||
readonly property color gutter: "#3b4261"
|
||||
readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf"
|
||||
readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0"
|
||||
readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5"
|
||||
readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb"
|
||||
|
||||
// The pair. `accent` is the primary and carries every state meaning
|
||||
// (focused, active, on). `accentSecondary` is the orchid from the tmux
|
||||
// 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,
|
||||
// so the pink stops being special the moment it's used as a flat fill.
|
||||
readonly property color accent: "#82aaff" // blue
|
||||
readonly property color accentSecondary: "#b172b0" // orchid, from tmux
|
||||
readonly property color accentAlt: "#65bcff" // blue1, a lighter blue
|
||||
readonly property color cyan: "#86e1fc"
|
||||
readonly property color teal: "#4fd6be"
|
||||
readonly property color green: "#c3e88d"
|
||||
readonly property color yellow: "#ffc777"
|
||||
readonly property color orange: "#ff966c"
|
||||
readonly property color red: "#ff757f"
|
||||
readonly property color redDeep: "#c53b53"
|
||||
readonly property color magenta: "#c099ff"
|
||||
readonly property color pink: "#fca7ea"
|
||||
// ── The accent ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// `accent` is the primary and carries every state meaning (focused, active,
|
||||
// on). `accentSecondary` never appears alone, only as the far end of a
|
||||
// gradient. That restraint is the whole point: the two colors meeting is
|
||||
// the signature, so the second color stops being special the moment it is
|
||||
// used as a flat fill.
|
||||
//
|
||||
// NAMED accents rather than a free color. Each name carries a curated
|
||||
// triple per scheme, because an arbitrary hex cannot work in both: a color
|
||||
// legible on the Moon background is usually illegible on the Day one, and a
|
||||
// picker that lets someone choose an unreadable desktop is not a feature.
|
||||
// Every pair below measures at least 3:1 against the ground it sits on.
|
||||
// This is also GNOME's model, which is the parity being chased.
|
||||
//
|
||||
// Blue is the shipped Prism -- blue leading, orchid following -- and stays
|
||||
// the default.
|
||||
//
|
||||
// The table itself lives in services/ThemeProfileModel.js so a curated
|
||||
// accent and a custom profile are the same kind of record. Its `gnome`
|
||||
// member is the nearest name in GNOME's own accent-color enum, which is a
|
||||
// fixed list of nine we do not get to extend; it is what libadwaita
|
||||
// applications -- Files, Papers, Loupe -- are told to use, so choosing an
|
||||
// accent here recolors them too instead of leaving them in GNOME blue.
|
||||
readonly property var accents: ThemeProfiles.curatedAccents
|
||||
readonly property var activeProfile: ThemeProfiles.activeProfile
|
||||
|
||||
// ThemeProfiles validates every persisted record before it can become
|
||||
// active, so these bindings are both reactive and safe to expose as the
|
||||
// shell-wide colour roles.
|
||||
readonly property color accent: root.activeProfile.accent
|
||||
readonly property color accentSecondary: root.activeProfile.secondary
|
||||
readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue
|
||||
readonly property color cyan: root.dark ? "#86e1fc" : "#007197"
|
||||
readonly property color teal: root.dark ? "#4fd6be" : "#118c74"
|
||||
readonly property color green: root.dark ? "#c3e88d" : "#587539"
|
||||
readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e"
|
||||
readonly property color orange: root.dark ? "#ff966c" : "#b15c00"
|
||||
readonly property color red: root.dark ? "#ff757f" : "#f52a65"
|
||||
readonly property color redDeep: root.dark ? "#c53b53" : "#c64343"
|
||||
readonly property color magenta: root.dark ? "#c099ff" : "#9854f1"
|
||||
readonly property color pink: root.dark ? "#fca7ea" : "#d20065"
|
||||
|
||||
// Semantic aliases — prefer these in widgets so intent survives a repaint.
|
||||
readonly property color ok: green
|
||||
@@ -59,18 +96,28 @@ Singleton {
|
||||
// The dock sits on compositor blur, so its fill is deliberately very
|
||||
// translucent. Popovers are near-opaque because they carry text that must
|
||||
// stay legible. The bar itself has no surface or alpha token.
|
||||
readonly property real dockAlpha: 0.34
|
||||
readonly property real popoverAlpha: 0.92
|
||||
readonly property real overlayAlpha: 0.55
|
||||
readonly property real hoverAlpha: 0.14
|
||||
readonly property real activeAlpha: 0.24
|
||||
// Light surfaces need more opacity for the same sense of a solid panel: the
|
||||
// same 0.34 that reads as glass over a dark desktop reads as haze over a
|
||||
// light one, and text on it stops being legible.
|
||||
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
|
||||
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97
|
||||
|
||||
// Toasts sit a little lighter than a popover you opened on purpose. A
|
||||
// notification arrives unbidden over whatever you were doing, and at full
|
||||
// popover weight it reads as a dialog demanding an answer -- but the 6%
|
||||
// foreground tint it used to have left the text competing with the desktop
|
||||
// behind it. This is the point between the two.
|
||||
readonly property real toastAlpha: root.dark ? 0.86 : 0.94
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
readonly property int barHeight: 36
|
||||
readonly property int barGap: 6 // breathing room below the bar for popovers
|
||||
readonly property int barSideMargin: 10 // inset for floating popovers
|
||||
|
||||
readonly property int dockIconSize: 48
|
||||
readonly property int dockIconSize: DesktopPreferences.get("dockIconSize")
|
||||
readonly property int dockPadding: 8
|
||||
readonly property int dockGap: 8
|
||||
readonly property int dockRadius: 20
|
||||
@@ -79,7 +126,6 @@ Singleton {
|
||||
readonly property int popoverPadding: 14
|
||||
readonly property int popoverWidth: 380
|
||||
readonly property int controlCenterWidth: 430
|
||||
readonly property int controlCenterTopGap: 2
|
||||
|
||||
readonly property int cardRadius: 12
|
||||
readonly property int pillRadius: 999
|
||||
@@ -90,12 +136,12 @@ Singleton {
|
||||
// Adwaita Sans for everything the user reads as text. No exceptions: a
|
||||
// monospaced clock or percentage reads as a terminal readout pasted into a
|
||||
// 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
|
||||
// pragmatic alternative to freedesktop symbolic icons, which ship with a
|
||||
// hardcoded dark fill Qt will not recolour (see widgets/ThemedIcon.qml).
|
||||
readonly property string fontMono: "VictorMono Nerd Font"
|
||||
// hardcoded dark fill Qt will not recolor (see widgets/ThemedIcon.qml).
|
||||
readonly property string fontMono: DesktopPreferences.get("iconFont")
|
||||
|
||||
// Apply to any text whose digits change in place — clocks, percentages,
|
||||
// elapsed timers, dimension readouts. Tabular figures all share one
|
||||
@@ -107,10 +153,13 @@ Singleton {
|
||||
readonly property var tabularFigures: ({
|
||||
"tnum": 1
|
||||
})
|
||||
readonly property int fontSize: 13
|
||||
readonly property int fontSizeSmall: 11
|
||||
readonly property int fontSizeLarge: 16
|
||||
readonly property int fontSizeTitle: 20
|
||||
// The four sizes are a scale, not four independent numbers: they move
|
||||
// together so the relationship between body, caption, heading and title
|
||||
// survives a change to the base rather than drifting apart.
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
// Event-driven only. Nothing in this shell animates while idle — no pulse,
|
||||
@@ -135,7 +184,7 @@ Singleton {
|
||||
}
|
||||
|
||||
// Blend two tokens — used for hover/pressed states so we never invent a
|
||||
// colour that isn't derived from the palette.
|
||||
// color that isn't derived from the palette.
|
||||
function mix(a: color, b: color, ratio: real): color {
|
||||
return Qt.rgba(a.r + (b.r - a.r) * ratio, a.g + (b.g - a.g) * ratio, a.b + (b.b - a.b) * ratio, a.a + (b.a - a.a) * ratio);
|
||||
}
|
||||
@@ -146,7 +195,7 @@ Singleton {
|
||||
// the active workspace pill and slider fills.
|
||||
//
|
||||
// Where the gradient runs left-to-right the blue leads, because the eye
|
||||
// reads left first and blue is the colour that carries meaning.
|
||||
// reads left first and blue is the color that carries meaning.
|
||||
readonly property real prismStart: 0.22
|
||||
readonly property real prismEnd: 0.78
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ singleton PreferenceSchema 1.0 PreferenceSchema.qml
|
||||
singleton HomePreferences 1.0 HomePreferences.qml
|
||||
singleton Settings 1.0 Settings.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import qs.config
|
||||
import qs.services
|
||||
|
||||
ShellRoot {
|
||||
// The isolated screen model begins with both fixture outputs so changing
|
||||
// it below exercises the same reactive topology path as a real hotplug.
|
||||
Component.onCompleted: Displays.screenOverride = ["DP-2", "HDMI-A-1"]
|
||||
|
||||
IpcHandler {
|
||||
target: "displays-test"
|
||||
|
||||
@@ -20,6 +24,9 @@ ShellRoot {
|
||||
mode: monitor ? monitor.mode : "",
|
||||
scale: monitor ? monitor.scale : 0,
|
||||
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,
|
||||
awaiting: Displays.awaitingConfirmation,
|
||||
canConfirm: Displays.canConfirm,
|
||||
@@ -36,8 +43,52 @@ ShellRoot {
|
||||
return Displays.apply(monitor.name, mode, scale, monitor.transform);
|
||||
}
|
||||
|
||||
function transactionStatus(): string {
|
||||
return JSON.stringify({
|
||||
layout: Displays.currentLayout(),
|
||||
primaryFirst: Displays.primaryFirstMonitors.map(monitor => monitor.name),
|
||||
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 {
|
||||
const modes = Displays.normaliseModes([
|
||||
const modes = Displays.normalizeModes([
|
||||
"[email protected]",
|
||||
"[email protected]"
|
||||
]);
|
||||
@@ -49,6 +100,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 {
|
||||
const monitor = Displays.monitors[0];
|
||||
if (!monitor) return false;
|
||||
@@ -71,5 +146,8 @@ ShellRoot {
|
||||
if (monitor) Displays.forget(monitor.name);
|
||||
}
|
||||
function refresh(): void { Displays.refresh(); }
|
||||
function setScreenModel(names: string): void {
|
||||
Displays.screenOverride = JSON.parse(names);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
// 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 behavior 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
|
||||
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 {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
|
||||
@@ -15,7 +15,11 @@ PanelWindow {
|
||||
color: "transparent"
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: Theme.barHeight + Theme.barGap * 2
|
||||
// The gap ALONE, not the bar height plus the gap. exclusiveZone 0 means
|
||||
// "reserve nothing, but respect what others reserved", so this surface
|
||||
// already begins below the bar's zone -- adding the bar height here counted
|
||||
// it twice and left the surface floating 48px under the bar instead of 12.
|
||||
margins.top: Theme.barGap * 2
|
||||
margins.right: Theme.barSideMargin
|
||||
exclusiveZone: 0
|
||||
implicitWidth: 350
|
||||
@@ -25,10 +29,18 @@ PanelWindow {
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
// Deliberately does not read Capture.recordingSeconds (or anything else
|
||||
// that ticks once a second): this array is a plain JS array, not an
|
||||
// identity-preserving model, so any dependency that changes every second
|
||||
// would make the whole thing re-derive every second, and the Repeater
|
||||
// below would destroy and recreate every row -- including one the user
|
||||
// might be hovering or about to click. The set of activities should only
|
||||
// change when an activity actually starts or stops. Elapsed time is
|
||||
// rendered by each row's own Text binding instead, further down.
|
||||
readonly property var activities: {
|
||||
const result = [];
|
||||
if (PrivacyState.recordingActive)
|
||||
result.push({ kind: "recording", glyph: "\u{F044A}", label: "Screen recording", detail: "Panama · " + root.elapsed(), tone: "danger", stoppable: true });
|
||||
result.push({ kind: "recording", glyph: "\u{F044A}", label: "Screen recording", detail: "Panama", tone: "danger", stoppable: true });
|
||||
if (PrivacyState.screenSharingActive)
|
||||
result.push({ kind: "screen", glyph: "\u{F0379}", label: "Screen sharing", detail: PrivacyState.screenSharingApp || "Managed by the application", tone: "warn", stoppable: false });
|
||||
if (PrivacyState.cameraActive)
|
||||
@@ -38,11 +50,12 @@ PanelWindow {
|
||||
return result;
|
||||
}
|
||||
|
||||
function elapsed(): string {
|
||||
const total = Capture.recordingSeconds;
|
||||
const seconds = String(total % 60).padStart(2, "0");
|
||||
const minutes = Math.floor(total / 60) % 60;
|
||||
const hours = Math.floor(total / 3600);
|
||||
// Pure formatter, no ticking property read here -- callers decide what
|
||||
// seconds value to pass, and only they take on the per-second dependency.
|
||||
function formatElapsed(totalSeconds: int): string {
|
||||
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
||||
const minutes = Math.floor(totalSeconds / 60) % 60;
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}` : `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
@@ -164,7 +177,11 @@ PanelWindow {
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: activityRow.modelData.detail
|
||||
// Only this Text re-evaluates every second while
|
||||
// recording -- Capture.recordingSeconds is read
|
||||
// here, not in the parent `activities` array, so
|
||||
// the row itself is never torn down for a tick.
|
||||
text: activityRow.modelData.kind === "recording" ? activityRow.modelData.detail + " · " + root.formatElapsed(Capture.recordingSeconds) : activityRow.modelData.detail
|
||||
color: Theme.fgDim
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
// Bar {}
|
||||
// }
|
||||
//
|
||||
// Layout mirrors GNOME's: workspaces + vitals left, clock + weather centre,
|
||||
// Layout mirrors GNOME's: workspaces + vitals left, clock + weather center,
|
||||
// media + tray + status cluster right.
|
||||
|
||||
import Quickshell
|
||||
@@ -83,7 +83,7 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Centre ──────────────────────────────────────────────────────────
|
||||
// ── Center ──────────────────────────────────────────────────────────
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -108,6 +108,10 @@ PanelWindow {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
HealthIndicator {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
ActivityIndicator {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ Pill {
|
||||
horizontalPadding: 8
|
||||
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.delay: 500
|
||||
ToolTip.text: CalendarAgenda.nextEvent?.summary ?? "Upcoming event"
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
// The calendar that drops out of the clock — GNOME's date menu, minus the
|
||||
// notification list (that lives in its own panel). Month grid, today marked
|
||||
// with the accent, arrows to page through months, current weather at the foot.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
Popover {
|
||||
id: root
|
||||
|
||||
// Popover's container is a plain Item, which does not derive an implicit
|
||||
// size from its children, so the window has to be sized from the body.
|
||||
implicitWidth: body.implicitWidth + contentPadding * 2
|
||||
implicitHeight: body.implicitHeight + contentPadding * 2
|
||||
|
||||
readonly property int cellSize: 34
|
||||
readonly property int cellHeight: 30
|
||||
|
||||
// Hours precision is enough: the only thing that has to change on its own
|
||||
// is which cell counts as "today", and that only moves at midnight.
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Hours
|
||||
}
|
||||
|
||||
readonly property int todayYear: clock.date.getFullYear()
|
||||
readonly property int todayMonth: clock.date.getMonth()
|
||||
readonly property int todayDay: clock.date.getDate()
|
||||
|
||||
// The month currently on screen. Reset to today every time the popover
|
||||
// opens, so it never comes back showing wherever you paged off to.
|
||||
property int viewYear: root.todayYear
|
||||
property int viewMonth: root.todayMonth
|
||||
|
||||
onVisibleChanged: if (visible)
|
||||
root.showToday()
|
||||
|
||||
function showToday(): void {
|
||||
root.viewYear = root.todayYear;
|
||||
root.viewMonth = root.todayMonth;
|
||||
}
|
||||
|
||||
function stepMonth(delta: int): void {
|
||||
const d = new Date(root.viewYear, root.viewMonth + delta, 1);
|
||||
root.viewYear = d.getFullYear();
|
||||
root.viewMonth = d.getMonth();
|
||||
}
|
||||
|
||||
// Six weeks of cells, so the grid height never changes as you page through
|
||||
// months. Days from the neighbouring months fill the edges, dimmed.
|
||||
readonly property var cells: {
|
||||
const first = new Date(root.viewYear, root.viewMonth, 1);
|
||||
const offset = first.getDay(); // 0 = Sunday, matching the header row
|
||||
const inThisMonth = new Date(root.viewYear, root.viewMonth + 1, 0).getDate();
|
||||
const inPrevMonth = new Date(root.viewYear, root.viewMonth, 0).getDate();
|
||||
|
||||
const out = [];
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const n = i - offset + 1;
|
||||
if (n < 1)
|
||||
out.push({
|
||||
day: inPrevMonth + n,
|
||||
current: false
|
||||
});
|
||||
else if (n > inThisMonth)
|
||||
out.push({
|
||||
day: n - inThisMonth,
|
||||
current: false
|
||||
});
|
||||
else
|
||||
out.push({
|
||||
day: n,
|
||||
current: true
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Column {
|
||||
id: body
|
||||
spacing: Theme.itemSpacing
|
||||
|
||||
// ── Month header ────────────────────────────────────────────────────
|
||||
Item {
|
||||
width: root.cellSize * 7
|
||||
height: 28
|
||||
|
||||
CalendarArrow {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
glyph: "\u{F0141}" // md-chevron_left
|
||||
onActivated: root.stepMonth(-1)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Qt.formatDateTime(new Date(root.viewYear, root.viewMonth, 1), "MMMM yyyy")
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
color: Theme.fg
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.showToday()
|
||||
}
|
||||
}
|
||||
|
||||
CalendarArrow {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
glyph: "\u{F0142}" // md-chevron_right
|
||||
onActivated: root.stepMonth(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Weekday header ──────────────────────────────────────────────────
|
||||
Row {
|
||||
Repeater {
|
||||
model: ["S", "M", "T", "W", "T", "F", "S"]
|
||||
|
||||
delegate: Text {
|
||||
required property string modelData
|
||||
|
||||
width: root.cellSize
|
||||
height: 20
|
||||
text: modelData
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.fgMuted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Day grid ────────────────────────────────────────────────────────
|
||||
Grid {
|
||||
columns: 7
|
||||
|
||||
Repeater {
|
||||
model: root.cells
|
||||
|
||||
delegate: Item {
|
||||
id: cell
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.viewMonth === root.todayMonth && root.viewYear === root.todayYear
|
||||
|
||||
width: root.cellSize
|
||||
height: root.cellHeight
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: root.cellHeight - 2
|
||||
height: root.cellHeight - 2
|
||||
radius: width / 2
|
||||
border.width: 0
|
||||
visible: cell.isToday
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: cell.modelData.day
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: cell.isToday ? Font.DemiBold : Font.Normal
|
||||
|
||||
color: {
|
||||
if (cell.isToday)
|
||||
return Theme.bgDark;
|
||||
return cell.modelData.current ? Theme.fg : Theme.fgMuted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Weather ─────────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
width: root.cellSize * 7
|
||||
height: 1
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
visible: Weather.available
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.itemSpacing
|
||||
visible: Weather.available
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Weather.icon
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSizeTitle
|
||||
color: Theme.accentAlt
|
||||
}
|
||||
|
||||
Column {
|
||||
Text {
|
||||
text: Math.round(Weather.temperature) + Weather.unitSuffix
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.fg
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Weather.description
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.fgDim
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// The centre clock, kept in the same place and the same format GNOME had it:
|
||||
// The center clock, kept in the same place and the same format GNOME had it:
|
||||
// weekday, date, 12-hour time with seconds. Clicking it opens the calendar,
|
||||
// exactly like GNOME's date menu.
|
||||
|
||||
@@ -26,11 +26,16 @@ Pill {
|
||||
|
||||
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 {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Qt.formatDateTime(clock.date, root.format)
|
||||
font.family: Theme.fontFamily
|
||||
// Seconds tick once a second in the centre of the bar; without tabular
|
||||
// Seconds tick once a second in the center of the bar; without tabular
|
||||
// figures the whole clock shifts sideways on every digit change.
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
|
||||
@@ -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 Quickshell.Services.Mpris
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
Pill {
|
||||
@@ -34,6 +35,9 @@ Pill {
|
||||
onActivated: if (root.player?.canTogglePlaying)
|
||||
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
|
||||
// switcher, so the whole bar scrolls consistently.
|
||||
onScrolled: delta => {
|
||||
|
||||
@@ -24,6 +24,9 @@ Pill {
|
||||
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
// Without a tracker, volume and muted silently read as zero/false.
|
||||
PwObjectTracker {
|
||||
@@ -59,7 +62,7 @@ Pill {
|
||||
if (!root.wifiNetwork)
|
||||
return "\u{F092D}"; // md-wifi_strength_off
|
||||
|
||||
// NetworkManager reports strength 0-100; some backends normalise to
|
||||
// NetworkManager reports strength 0-100; some backends normalize to
|
||||
// 0-1, so accept either rather than showing one bar forever.
|
||||
const raw = root.wifiNetwork.signalStrength ?? 0;
|
||||
const strength = raw <= 1 ? raw * 100 : raw;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// One glyph + percentage pair inside VitalsWidget. Split out so the three
|
||||
// fields cannot drift apart in spacing, width or colour thresholds.
|
||||
// fields cannot drift apart in spacing, width or color thresholds.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Vitals extension was configured with (_processor_usage_, _memory_usage_,
|
||||
// _gpu#1_usage_).
|
||||
//
|
||||
// Values are colour-coded rather than graphed: at bar size a number you can
|
||||
// Values are color-coded rather than graphed: at bar size a number you can
|
||||
// glance at beats a sparkline you have to squint at.
|
||||
|
||||
import QtQuick
|
||||
@@ -13,6 +13,10 @@ import qs.widgets
|
||||
Pill {
|
||||
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
|
||||
|
||||
Row {
|
||||
|
||||
@@ -10,6 +10,9 @@ import qs.widgets
|
||||
Pill {
|
||||
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
|
||||
visible: Weather.available
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Item {
|
||||
|
||||
implicitWidth: row.implicitWidth + 12
|
||||
// Full bar height makes (0, 0) part of the wheel target. The dots stay
|
||||
// optically centred by the row and active-pill geometry below.
|
||||
// optically centered by the row and active-pill geometry below.
|
||||
implicitHeight: Theme.barHeight
|
||||
|
||||
// Scroll over the padding either side of the row, not just over a dot.
|
||||
@@ -59,7 +59,7 @@ Item {
|
||||
border.width: 0 // QTBUG-137166: rounded rects can lose their corners
|
||||
|
||||
// The prism, as a fill rather than a hairline. This is the one element
|
||||
// in the bar that is always coloured, so it is where the pair belongs.
|
||||
// in the bar that is always colored, so it is where the pair belongs.
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// The floating control panel at the bottom of the capture overlay.
|
||||
//
|
||||
// Layout is a deliberate copy of GNOME 42's screenshot UI: the area segmented
|
||||
// control on top, and below it the shutter dead-centre with "Show Pointer" on
|
||||
// control on top, and below it the shutter dead-center with "Show Pointer" on
|
||||
// the left and the screenshot/screencast switch on the right.
|
||||
|
||||
import QtQuick
|
||||
@@ -13,7 +13,7 @@ Rectangle {
|
||||
|
||||
signal captureRequested
|
||||
|
||||
// Set by the overlay so the shutter can grey out when Window mode has
|
||||
// Set by the overlay so the shutter can gray out when Window mode has
|
||||
// nothing pickable (no Hyprland, or an empty workspace).
|
||||
property bool captureEnabled: true
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// One segment of the Screen / Window / Selection control.
|
||||
//
|
||||
// Icons are Nerd Font glyphs rather than freedesktop icons on purpose: the
|
||||
// Adwaita "symbolic" SVGs are flat black and Qt, unlike GTK, will not recolour
|
||||
// Adwaita "symbolic" SVGs are flat black and Qt, unlike GTK, will not recolor
|
||||
// them, so they'd be invisible on this dark overlay.
|
||||
|
||||
import QtQuick
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// The "you are being recorded" pill.
|
||||
//
|
||||
// GNOME puts a red dot and a timer in the top bar while a screencast runs and
|
||||
// clicking it stops the recording. The bar is another module's window, so this
|
||||
// is its own tiny layer surface parked just below it.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
visible: Capture.recording
|
||||
color: "transparent"
|
||||
|
||||
// Top only: with neither left nor right anchored, layer-shell centres the
|
||||
// surface horizontally.
|
||||
anchors.top: true
|
||||
margins.top: Theme.barGap * 2 + Theme.barHeight + Theme.barGap
|
||||
|
||||
implicitWidth: pill.implicitWidth
|
||||
implicitHeight: pill.implicitHeight
|
||||
exclusiveZone: 0
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-recording"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
function elapsed(): string {
|
||||
const t = Capture.recordingSeconds;
|
||||
const s = ("0" + (t % 60)).slice(-2);
|
||||
const m = Math.floor(t / 60) % 60;
|
||||
const h = Math.floor(t / 3600);
|
||||
return h > 0 ? h + ":" + ("0" + m).slice(-2) + ":" + s : m + ":" + s;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: pill
|
||||
implicitWidth: row.implicitWidth + 12
|
||||
implicitHeight: 34
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
color: Theme.redDeep
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
// Static dot, not a blinking one: nothing in this shell repaints
|
||||
// while idle.
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 10
|
||||
height: 10
|
||||
radius: 5
|
||||
border.width: 0
|
||||
color: Theme.fg
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: win.elapsed()
|
||||
color: Theme.fg
|
||||
// Tabular figures: the timer must not shuffle as it counts.
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stop
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 26
|
||||
height: 26
|
||||
radius: width / 2
|
||||
border.width: 0
|
||||
color: stopMouse.containsMouse ? Theme.alpha(Theme.fg, 0.28) : Theme.alpha(Theme.fg, 0.14)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontMono
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: stopMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
// SIGINT, so wf-recorder finalises the container.
|
||||
onClicked: Capture.stopRecording()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Selection mode: a GNOME-style rubber band over the frozen screen.
|
||||
//
|
||||
// Behaviours copied from GNOME 42: the overlay opens with a sensible centred
|
||||
// Behaviors copied from GNOME 42: the overlay opens with a sensible centered
|
||||
// selection already in place (so the shutter is immediately useful), dragging
|
||||
// inside moves it, dragging a corner resizes from the opposite corner, and
|
||||
// dragging anywhere else starts a fresh band.
|
||||
@@ -52,7 +52,7 @@ Item {
|
||||
root.resetSelection();
|
||||
}
|
||||
|
||||
// Centred, roughly the middle half of the screen.
|
||||
// Centered, roughly the middle half of the screen.
|
||||
function resetSelection(): void {
|
||||
if (root.width <= 0 || root.height <= 0)
|
||||
return;
|
||||
|
||||
@@ -20,8 +20,11 @@ PanelWindow {
|
||||
top: true
|
||||
right: true
|
||||
}
|
||||
// The gap ALONE, not the bar height plus the gap -- see the note in
|
||||
// QuickSettings.qml: exclusiveZone 0 already places this below the bar's
|
||||
// reserved zone, so adding the bar height counted it twice.
|
||||
margins {
|
||||
top: Theme.barHeight + Theme.barGap * 2
|
||||
top: Theme.barGap * 2
|
||||
right: Theme.barSideMargin
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// The month grid from GNOME's date menu.
|
||||
//
|
||||
// Six week rows always, so the grid height never changes as you page through
|
||||
// months and the panel below it never jumps. Days belonging to the neighbouring
|
||||
// months and the panel below it never jumps. Days belonging to the neighboring
|
||||
// months fill the edges, dimmed. Today wears the accent.
|
||||
//
|
||||
// Days become real navigation targets now that Evolution Data Server backs the
|
||||
// panel. Up to three source-coloured markers show whether a date has events.
|
||||
// panel. Up to three source-colored markers show whether a date has events.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
@@ -134,7 +134,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Centred on the row rather than between the pill and the arrows,
|
||||
// Centered on the row rather than between the pill and the arrows,
|
||||
// so it does not shift sideways when the pill appears.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// GNOME's date/time menu: calendar, ongoing activity and messages in one panel,
|
||||
// dropping from the top centre because that is where GNOME put it and the
|
||||
// dropping from the top center because that is where GNOME put it and the
|
||||
// muscle memory is the whole point.
|
||||
//
|
||||
// A layer-shell surface rather than a PopupWindow because the inline reply
|
||||
// fields have to be typeable, and because the clock it hangs from is owned by a
|
||||
// different module.
|
||||
//
|
||||
// Layer-shell centres a surface on whichever axis it is not anchored to, so
|
||||
// Layer-shell centers a surface on whichever axis it is not anchored to, so
|
||||
// anchoring only `top` is what puts it in the middle.
|
||||
|
||||
import QtQuick
|
||||
@@ -28,7 +28,11 @@ PanelWindow {
|
||||
color: "transparent"
|
||||
|
||||
anchors.top: true
|
||||
margins.top: Theme.barHeight + Theme.barGap * 2
|
||||
// The gap ALONE, not the bar height plus the gap. exclusiveZone 0 means
|
||||
// "reserve nothing, but respect what others reserved", so this surface
|
||||
// already begins below the bar's zone -- adding the bar height here counted
|
||||
// it twice and left the surface floating 48px under the bar instead of 12.
|
||||
margins.top: Theme.barGap * 2
|
||||
exclusiveZone: 0
|
||||
|
||||
implicitWidth: 760
|
||||
@@ -87,7 +91,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
// Grows out of the top edge it is anchored to, rather than out of its
|
||||
// own centre — the panel should look like it came from the bar.
|
||||
// own center — the panel should look like it came from the bar.
|
||||
transform: Scale {
|
||||
id: openScale
|
||||
origin.x: surface.width / 2
|
||||
|
||||
@@ -22,10 +22,34 @@ PanelWindow {
|
||||
property var modelData: null
|
||||
screen: root.modelData
|
||||
|
||||
// Which edge this dock lives on, and everything that follows from it. The
|
||||
// bottom case is unchanged in every particular: same anchors, same
|
||||
// geometry, same slide -- so a machine that never touches the setting sees
|
||||
// exactly the dock it had.
|
||||
readonly property string position: Settings.dockPosition
|
||||
readonly property bool vertical: root.position === "left" || root.position === "right"
|
||||
|
||||
// Only on the screens asked for. An empty list means all of them, which is
|
||||
// what a single-monitor machine wants and what an unplugged display should
|
||||
// not be able to change.
|
||||
readonly property bool onThisScreen: {
|
||||
const wanted = Settings.dockScreens;
|
||||
if (!wanted || wanted.length === 0)
|
||||
return true;
|
||||
return wanted.indexOf(String(root.screen?.name ?? "")) >= 0;
|
||||
}
|
||||
|
||||
visible: root.onThisScreen
|
||||
|
||||
// A dock spans the edge it lives on, which means anchoring BOTH ends of
|
||||
// that edge: bottom+left+right across the screen, or top+bottom plus one
|
||||
// side down it. Anchoring only one end leaves the surface free to collapse
|
||||
// to its implicit size on that axis -- a side dock came out one pixel tall.
|
||||
anchors {
|
||||
top: root.vertical
|
||||
bottom: true
|
||||
left: true
|
||||
right: true
|
||||
left: root.position !== "right"
|
||||
right: root.position !== "left"
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
@@ -42,18 +66,31 @@ PanelWindow {
|
||||
// ── Geometry ────────────────────────────────────────────────────────────
|
||||
// Height = room for the tooltip above the bar + the bar + the gap under it.
|
||||
readonly property int revealStripHeight: 3
|
||||
readonly property int bottomMargin: Theme.barGap
|
||||
readonly property int edgeMargin: Theme.barGap
|
||||
readonly property int tooltipSpace: 34
|
||||
|
||||
implicitHeight: tooltipSpace + body.implicitHeight + bottomMargin
|
||||
// Only the axis the dock is thin on gets an implicit size; the other is
|
||||
// spanned by the anchors above. Setting both would fight them.
|
||||
implicitHeight: root.vertical ? 0 : tooltipSpace + body.implicitHeight + edgeMargin
|
||||
implicitWidth: root.vertical ? tooltipSpace + body.implicitWidth + edgeMargin : 0
|
||||
|
||||
// ── Intellihide ─────────────────────────────────────────────────────────
|
||||
// This instance's own monitor, the same lookup Workspaces.qml uses to
|
||||
// scope a per-screen bar to its own screen. Falls back to null when this
|
||||
// Dock is created standalone (no `screen` set) rather than via Variants.
|
||||
readonly property HyprlandMonitor monitor: root.screen ? Hyprland.monitorFor(root.screen) : null
|
||||
|
||||
// Hyprland does not expose live toplevel geometry, so exact overlap cannot
|
||||
// be computed. "Is anything on this workspace at all" is the robust proxy,
|
||||
// and it is what Dash-to-Dock's all-windows intellihide felt like in
|
||||
// practice: an empty workspace keeps the dock out.
|
||||
//
|
||||
// Deliberately this instance's own monitor's active workspace, not the
|
||||
// globally-focused one -- with one Dock per screen, keying off the global
|
||||
// focus would make focusing an empty workspace on monitor A hide the dock
|
||||
// on monitor B even though B's own workspace is still busy.
|
||||
readonly property bool workspaceOccupied: {
|
||||
const ws = Hyprland.focusedWorkspace;
|
||||
const ws = root.monitor ? root.monitor.activeWorkspace : Hyprland.focusedWorkspace;
|
||||
return !!ws && ws.toplevels.values.length > 0;
|
||||
}
|
||||
|
||||
@@ -83,15 +120,31 @@ PanelWindow {
|
||||
onTriggered: root.revealed = false
|
||||
}
|
||||
|
||||
// Other modules (the bar, the capture overlay) read this.
|
||||
onRevealedChanged: ShellState.dockRevealed = revealed
|
||||
// Other modules (the bar, the capture overlay) read this. It is one
|
||||
// shared flag but there is one Dock per monitor, so only the instance on
|
||||
// the currently-focused monitor is allowed to write it -- otherwise
|
||||
// whichever instance last changed reveal state would stomp the others,
|
||||
// and a reader would see an arbitrary monitor's value. This scopes the
|
||||
// flag to mean "is the dock revealed on the monitor the user is on",
|
||||
// which is what a capture overlay or the bar actually care about.
|
||||
// (A true per-monitor flag would need ShellState.dockRevealed itself to
|
||||
// become keyed by screen, which is out of scope here -- see the report.)
|
||||
readonly property bool isFocusedMonitorInstance: root.monitor === null || root.monitor === Hyprland.focusedMonitor
|
||||
|
||||
onRevealedChanged: root._syncShellState()
|
||||
onIsFocusedMonitorInstanceChanged: root._syncShellState()
|
||||
|
||||
function _syncShellState(): void {
|
||||
if (root.isFocusedMonitorInstance)
|
||||
ShellState.dockRevealed = root.revealed;
|
||||
}
|
||||
|
||||
// wantRevealed's first evaluation emits no change signal when it lands on
|
||||
// false (the default), so the initial state has to be taken explicitly —
|
||||
// otherwise a shell started on a busy workspace would leave the dock up.
|
||||
Component.onCompleted: {
|
||||
revealed = wantRevealed;
|
||||
ShellState.dockRevealed = revealed;
|
||||
root._syncShellState();
|
||||
}
|
||||
|
||||
// ── Input region ────────────────────────────────────────────────────────
|
||||
@@ -114,22 +167,86 @@ PanelWindow {
|
||||
id: pointer
|
||||
}
|
||||
|
||||
// Revealed: the dock plus everything between it and the edge, so
|
||||
// crossing the gap does not count as leaving. Hidden: a sliver along
|
||||
// the edge the dock lives on, which is the only thing that can bring
|
||||
// it back -- every other click passes through to the window beneath.
|
||||
Item {
|
||||
id: maskItem
|
||||
x: root.revealed ? body.x : 0
|
||||
y: root.revealed ? body.y : surface.height - root.revealStripHeight
|
||||
width: root.revealed ? body.width : surface.width
|
||||
height: root.revealed ? surface.height - body.y : root.revealStripHeight
|
||||
|
||||
x: {
|
||||
if (!root.revealed)
|
||||
return root.position === "right" ? surface.width - root.revealStripHeight : 0;
|
||||
// A horizontal dock is centred on its edge, so the region has
|
||||
// to start where the body starts -- starting at 0 puts it over
|
||||
// the left of the screen while the pointer that summoned the
|
||||
// dock is in the middle, and the hover drops on the very frame
|
||||
// the dock arrives. A vertical dock reaches from the body out
|
||||
// to its own edge, which is x 0 on the left and the body on
|
||||
// the right.
|
||||
if (!root.vertical)
|
||||
return body.x;
|
||||
return root.position === "right" ? body.x : 0;
|
||||
}
|
||||
y: {
|
||||
if (!root.revealed)
|
||||
return root.vertical ? 0 : surface.height - root.revealStripHeight;
|
||||
return body.y;
|
||||
}
|
||||
width: {
|
||||
if (!root.revealed)
|
||||
return root.vertical ? root.revealStripHeight : surface.width;
|
||||
return root.vertical
|
||||
? (root.position === "right" ? surface.width - body.x : body.x + body.width)
|
||||
: body.width;
|
||||
}
|
||||
height: {
|
||||
if (!root.revealed)
|
||||
return root.vertical ? surface.height : root.revealStripHeight;
|
||||
return root.vertical ? body.height : surface.height - body.y;
|
||||
}
|
||||
}
|
||||
|
||||
DockBody {
|
||||
id: body
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// Slides off the bottom edge when hidden.
|
||||
y: root.revealed ? root.tooltipSpace : surface.height
|
||||
onContextMenuRequested: (anchorItem, entry) => {
|
||||
dockContextMenu.anchorItem = anchorItem;
|
||||
dockContextMenu.entry = entry;
|
||||
dockContextMenu.visible = true;
|
||||
}
|
||||
|
||||
vertical: root.vertical
|
||||
leftSide: root.position === "left"
|
||||
|
||||
// Both axes are computed rather than anchored. Anchoring the centre
|
||||
// on one axis and binding a position on the other looks tidier and
|
||||
// is a conflict: an anchored centre owns that coordinate, so the
|
||||
// binding beside it is fighting for the same value.
|
||||
//
|
||||
// Centred on the long axis; on the short one it sits a tooltip's
|
||||
// width in from the edge when revealed, and off-screen when not.
|
||||
x: {
|
||||
if (!root.vertical)
|
||||
return (surface.width - width) / 2;
|
||||
if (root.position === "left")
|
||||
return root.revealed ? root.tooltipSpace : -width;
|
||||
return root.revealed ? surface.width - width - root.tooltipSpace : surface.width;
|
||||
}
|
||||
y: {
|
||||
if (root.vertical)
|
||||
return (surface.height - height) / 2;
|
||||
return root.revealed ? root.tooltipSpace : surface.height;
|
||||
}
|
||||
opacity: root.revealed ? 1 : 0
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: root.revealed ? Theme.durDockReveal : Theme.durNormal
|
||||
easing.type: root.revealed ? Easing.OutQuint : Easing.InCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Asymmetric on purpose. Revealing is a response to something the
|
||||
// user just did, so it has to feel immediate — any delay there
|
||||
// reads as lag. Hiding is not a response to anything, so it can
|
||||
@@ -149,4 +266,8 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DockContextMenu {
|
||||
id: dockContextMenu
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,30 +111,52 @@ Rectangle {
|
||||
inset: parent.radius
|
||||
}
|
||||
|
||||
implicitWidth: row.implicitWidth + Theme.dockPadding * 2
|
||||
implicitHeight: row.implicitHeight + Theme.dockPadding * 2
|
||||
implicitWidth: strip.implicitWidth + Theme.dockPadding * 2
|
||||
implicitHeight: strip.implicitHeight + Theme.dockPadding * 2
|
||||
|
||||
// The item the tooltip is currently describing, or null.
|
||||
property Item hoveredItem: null
|
||||
signal contextMenuRequested(Item anchorItem, var entry)
|
||||
|
||||
Row {
|
||||
id: row
|
||||
// Set by the Dock. A side dock runs the same strip down the screen instead
|
||||
// of across it.
|
||||
property bool vertical: false
|
||||
|
||||
// Which way a tooltip points on a side dock: away from the screen edge, so
|
||||
// it never opens off-screen.
|
||||
property bool leftSide: true
|
||||
|
||||
// Explicit rather than left to Grid's wrapping. This is always one line, so
|
||||
// saying how many cells it holds is both simpler to read and immune to
|
||||
// Grid's default column count quietly wrapping a long dock.
|
||||
readonly property int cellCount: 2 + (root.items ? root.items.length : 0)
|
||||
|
||||
// A Grid rather than a Row so one declaration serves both orientations.
|
||||
// Row and Column would each need their own children, and the cross-axis
|
||||
// anchors that centre items in a Row (verticalCenter) are the wrong axis in
|
||||
// a Column -- Grid centres through its own alignment properties instead,
|
||||
// which is the same result without the anchors positioners disallow.
|
||||
Grid {
|
||||
id: strip
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.dockGap
|
||||
rows: root.vertical ? root.cellCount : 1
|
||||
columns: root.vertical ? 1 : root.cellCount
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
verticalItemAlignment: Grid.AlignVCenter
|
||||
|
||||
ShowAppsButton {
|
||||
id: showApps
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onEntered: root.hoveredItem = showApps
|
||||
onExited: if (root.hoveredItem === showApps)
|
||||
root.hoveredItem = null
|
||||
}
|
||||
|
||||
// Separator between the launcher and the apps, as in GNOME's dash.
|
||||
// Separator between the launcher and the apps, as in GNOME's dash. It
|
||||
// turns with the dock: a hairline across a column, down a row.
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 1
|
||||
height: Theme.dockIconSize * 0.7
|
||||
width: root.vertical ? Theme.dockIconSize * 0.7 : 1
|
||||
height: root.vertical ? 1 : Theme.dockIconSize * 0.7
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.14)
|
||||
}
|
||||
@@ -146,10 +168,10 @@ Rectangle {
|
||||
id: dockItem
|
||||
required property var modelData
|
||||
app: modelData
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onEntered: root.hoveredItem = dockItem
|
||||
onExited: if (root.hoveredItem === dockItem)
|
||||
root.hoveredItem = null
|
||||
onContextMenuRequested: root.contextMenuRequested(dockItem, dockItem.entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,16 +194,26 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Centred on the hovered item, clamped inside the dock. Reads the item's
|
||||
// x directly (rather than mapToItem) so the binding re-evaluates when
|
||||
// the row reflows.
|
||||
// Centred on the hovered item along the dock's own axis, and placed just
|
||||
// outside the edge it lives on. Reads the item's position directly
|
||||
// (rather than mapToItem) so the binding re-evaluates when the strip
|
||||
// reflows.
|
||||
x: {
|
||||
if (!root.hoveredItem)
|
||||
return 0;
|
||||
const centre = row.x + root.hoveredItem.x + root.hoveredItem.width / 2;
|
||||
if (root.vertical)
|
||||
return root.leftSide ? root.width + 8 : -width - 8;
|
||||
const centre = strip.x + root.hoveredItem.x + root.hoveredItem.width / 2;
|
||||
return Math.max(4, Math.min(root.width - width - 4, centre - width / 2));
|
||||
}
|
||||
y: -height - 8
|
||||
y: {
|
||||
if (!root.hoveredItem)
|
||||
return -height - 8;
|
||||
if (!root.vertical)
|
||||
return -height - 8;
|
||||
const centre = strip.y + root.hoveredItem.y + root.hoveredItem.height / 2;
|
||||
return Math.max(4, Math.min(root.height - height - 4, centre - height / 2));
|
||||
}
|
||||
|
||||
width: tipLabel.implicitWidth + Theme.popoverPadding * 2
|
||||
height: tipLabel.implicitHeight + 8
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// The dock's app menu. Desktop-entry actions stay first; the shell-owned
|
||||
// configuration route is deliberately last so it never displaces app actions.
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.modules.bar
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
PopupWindow {
|
||||
id: root
|
||||
|
||||
property Item anchorItem: null
|
||||
property var entry: null
|
||||
|
||||
anchor.item: root.anchorItem
|
||||
anchor.edges: Edges.Top | Edges.Left
|
||||
anchor.gravity: Edges.Top | Edges.Right
|
||||
anchor.margins.bottom: 8
|
||||
|
||||
implicitWidth: Math.max(menu.implicitWidth + Theme.popoverPadding * 2, 240)
|
||||
implicitHeight: menu.implicitHeight + Theme.popoverPadding * 2
|
||||
color: "transparent"
|
||||
visible: false
|
||||
grabFocus: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 1
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: parent.radius
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menu
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.popoverPadding
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
id: applicationActions
|
||||
model: root.entry ? root.entry.actions : []
|
||||
|
||||
delegate: TrayMenuRow {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
label: modelData.name
|
||||
onActivated: {
|
||||
modelData.execute();
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
anchors.margins: 3
|
||||
visible: applicationActions.count > 0
|
||||
border.width: 0
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
}
|
||||
|
||||
TrayMenuRow {
|
||||
width: parent.width
|
||||
label: "Dock settings"
|
||||
onActivated: {
|
||||
ShellState.openSettings("desktop");
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// One dock icon: the app, its running-window dots, hover scale and click
|
||||
// behaviour. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
|
||||
// behavior. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
|
||||
// underneath, grow-on-hover, click to focus / cycle / launch.
|
||||
|
||||
import Quickshell
|
||||
@@ -24,6 +24,7 @@ Item {
|
||||
// Emitted so DockBody can drive the single shared tooltip.
|
||||
signal entered
|
||||
signal exited
|
||||
signal contextMenuRequested
|
||||
|
||||
// The icon may grow past the cell on hover; the cell itself stays a fixed
|
||||
// size so the row doesn't reflow.
|
||||
@@ -72,7 +73,7 @@ Item {
|
||||
}
|
||||
|
||||
// Last resort for an app with no resolvable icon: an initial, which is
|
||||
// still recognisable, unlike the theme's broken-icon placeholder.
|
||||
// still recognizable, unlike the theme's broken-icon placeholder.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: root.iconSource === ""
|
||||
@@ -115,7 +116,7 @@ Item {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
@@ -126,6 +127,10 @@ Item {
|
||||
root.launch();
|
||||
return;
|
||||
}
|
||||
if (mev.button === Qt.RightButton) {
|
||||
root.contextMenuRequested();
|
||||
return;
|
||||
}
|
||||
if (root.running)
|
||||
root.focusNext();
|
||||
else
|
||||
@@ -155,7 +160,7 @@ Item {
|
||||
return;
|
||||
|
||||
// Quickshell reports the address without the 0x prefix Hyprland's
|
||||
// window selector expects; normalise so either form works.
|
||||
// window selector expects; normalize so either form works.
|
||||
if (next.address) {
|
||||
const addr = next.address.startsWith("0x") ? next.address : "0x" + next.address;
|
||||
Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addr}" })`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// One notification, rendered GNOME-style: app icon, app name, summary, body,
|
||||
// optional image, action buttons and an inline reply box.
|
||||
//
|
||||
// Shared by the toasts and the notification centre so a notification looks the
|
||||
// Shared by the toasts and the notification center so a notification looks the
|
||||
// same wherever you meet it.
|
||||
//
|
||||
// IconButton/ThemedIcon come from the quicksettings module — they belong in
|
||||
@@ -14,6 +14,7 @@ import Quickshell.Services.Notifications
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.quicksettings
|
||||
import qs.modules.bar
|
||||
import qs.widgets
|
||||
|
||||
Rectangle {
|
||||
@@ -21,7 +22,7 @@ Rectangle {
|
||||
|
||||
required property var notification
|
||||
|
||||
// The centre renders a denser card than a toast banner.
|
||||
// The center renders a denser card than a toast banner.
|
||||
property bool compact: false
|
||||
|
||||
signal dismissed
|
||||
@@ -35,7 +36,17 @@ Rectangle {
|
||||
implicitHeight: Math.max(layout.implicitHeight + 24, image.visible ? 96 : 0)
|
||||
radius: Theme.cardRadius
|
||||
border.width: 0
|
||||
color: hover.containsMouse ? Theme.alpha(Theme.fg, 0.1) : Theme.alpha(Theme.fg, 0.06)
|
||||
// A real popover surface, the same one the date menu and clipboard panel
|
||||
// use, rather than a 6% foreground tint.
|
||||
//
|
||||
// A tint that faint has nothing behind it: the toast window is transparent,
|
||||
// so the card was sitting directly on whatever happened to be on screen and
|
||||
// the text competed with it. Notifications are the one surface someone reads
|
||||
// without having chosen to look at it, so it has to be legible over a
|
||||
// bright photo and a white document alike.
|
||||
color: hover.containsMouse
|
||||
? Theme.alpha(Theme.mix(Theme.bgPopover, Theme.fg, 0.06), Theme.toastAlpha)
|
||||
: Theme.alpha(Theme.bgPopover, Theme.toastAlpha)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
@@ -43,7 +54,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Critical notifications get a red edge rather than a colour wash, so the
|
||||
// Critical notifications get a red edge rather than a color wash, so the
|
||||
// text contrast never changes.
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
@@ -117,11 +128,44 @@ Rectangle {
|
||||
onClicked: root.dismissed()
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: settingsMenuButton
|
||||
anchors.right: closeButton.left
|
||||
anchors.rightMargin: 2
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 6
|
||||
size: 24
|
||||
iconSize: 14
|
||||
tint: Theme.fgDim
|
||||
icon: "view-more-symbolic"
|
||||
iconFallback: "open-menu-symbolic"
|
||||
onClicked: settingsMenu.visible = !settingsMenu.visible
|
||||
}
|
||||
|
||||
Popover {
|
||||
id: settingsMenu
|
||||
anchorItem: settingsMenuButton
|
||||
|
||||
// A Column already measures itself from its children, and in Qt 6 both
|
||||
// implicit sizes are read-only on a positioner -- assigning them makes
|
||||
// the whole shell fail to load rather than just this menu.
|
||||
Column {
|
||||
TrayMenuRow {
|
||||
id: notificationSettings
|
||||
label: "Notification settings"
|
||||
onActivated: {
|
||||
ShellState.openSettings("notifications");
|
||||
settingsMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
anchors.left: appIcon.visible ? appIcon.right : parent.left
|
||||
anchors.leftMargin: appIcon.visible ? 10 : 14
|
||||
anchors.right: image.visible ? image.left : closeButton.left
|
||||
anchors.right: image.visible ? image.left : settingsMenuButton.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 12
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
// GNOME's message tray: everything that has arrived, grouped by app.
|
||||
//
|
||||
// Drops from the top centre because that is where GNOME's tray lives and the
|
||||
// muscle memory is the whole point. Layer-shell centres a surface on whichever
|
||||
// axis it is not anchored to, so anchoring only `top` does it.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.quicksettings
|
||||
import qs.widgets
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
visible: ShellState.notificationsOpen
|
||||
color: "transparent"
|
||||
|
||||
anchors.top: true
|
||||
margins.top: Theme.barHeight + Theme.barGap * 2
|
||||
exclusiveZone: 0
|
||||
|
||||
implicitWidth: 440
|
||||
implicitHeight: surface.implicitHeight
|
||||
|
||||
WlrLayershell.namespace: "qs-popover-notifications"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
|
||||
onVisibleChanged: {
|
||||
if (root.visible)
|
||||
Notifs.markAllRead();
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [root]
|
||||
active: root.visible
|
||||
onCleared: ShellState.close()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
implicitHeight: content.implicitHeight + Theme.popoverPadding * 2
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
// The prism edge — see widgets/PrismEdge.qml. Sits just inside the 1px
|
||||
// border so the two don't fight for the same row of pixels.
|
||||
PrismEdge {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 1
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
inset: parent.radius
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: ShellState.close()
|
||||
|
||||
Column {
|
||||
id: content
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.popoverPadding
|
||||
spacing: Theme.itemSpacing
|
||||
|
||||
// ── Header ──────────────────────────────────────────────────
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Notifications"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
IconButton {
|
||||
size: 30
|
||||
iconSize: 16
|
||||
icon: "notifications-disabled-symbolic"
|
||||
tint: Notifs.doNotDisturb ? Theme.accent : Theme.fgDim
|
||||
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: clearLabel.implicitWidth + 20
|
||||
height: 28
|
||||
radius: Theme.pillRadius
|
||||
border.width: 0
|
||||
visible: Notifs.hasNotifications
|
||||
color: clearMouse.containsMouse ? Theme.alpha(Theme.fg, 0.16) : Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.durFast
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: clearLabel
|
||||
anchors.centerIn: parent
|
||||
text: "Clear all"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: Notifs.dismissAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ─────────────────────────────────────────────
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 120
|
||||
visible: !Notifs.hasNotifications
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Notifs.doNotDisturb ? "Do Not Disturb is on" : "No notifications"
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
// ── Grouped history ─────────────────────────────────────────
|
||||
ScrollColumn {
|
||||
width: parent.width
|
||||
maxHeight: 620
|
||||
spacing: Theme.itemSpacing
|
||||
visible: Notifs.hasNotifications
|
||||
|
||||
Repeater {
|
||||
model: Notifs.groups
|
||||
|
||||
Column {
|
||||
id: group
|
||||
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 26
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: group.modelData.app
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.DemiBold
|
||||
}
|
||||
|
||||
IconButton {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: 24
|
||||
iconSize: 13
|
||||
tint: Theme.fgDim
|
||||
icon: "edit-clear-all-symbolic"
|
||||
iconFallback: "window-close-symbolic"
|
||||
onClicked: Notifs.dismissApp(group.modelData.app)
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: group.modelData.items
|
||||
|
||||
NotificationCard {
|
||||
required property var modelData
|
||||
|
||||
width: group.width
|
||||
compact: true
|
||||
notification: modelData
|
||||
onDismissed: Notifs.dismiss(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// A single banner: a notification card that slides in and times itself out.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Services.Notifications
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
@@ -14,28 +13,25 @@ Item {
|
||||
// into a keyboard focus request on the layer surface.
|
||||
signal replyFocusChanged(bool focused)
|
||||
|
||||
// Mirrors the signal above so the dismiss timer below can read it.
|
||||
property bool replyFocused: false
|
||||
|
||||
implicitHeight: card.implicitHeight
|
||||
|
||||
// Critical notifications stay until dismissed (the setting is 0). An app
|
||||
// asking for 0 means "never expire" per the freedesktop spec; -1 means
|
||||
// "server decides", which is our default.
|
||||
readonly property int timeoutMs: {
|
||||
if (root.notification.urgency === NotificationUrgency.Critical)
|
||||
return Settings.notificationTimeoutCriticalMs;
|
||||
if (root.notification.expireTimeout === 0)
|
||||
return 0;
|
||||
if (root.notification.expireTimeout > 0)
|
||||
return Math.round(root.notification.expireTimeout * 1000);
|
||||
return Settings.notificationTimeoutMs;
|
||||
}
|
||||
// "server decides", which is our default. Shared with Notifs.qml, which
|
||||
// gives a DND-hidden transient notification the same lifetime.
|
||||
readonly property int timeoutMs: Notifs.notificationTimeoutMs(root.notification)
|
||||
|
||||
HoverHandler {
|
||||
id: hover
|
||||
}
|
||||
|
||||
Timer {
|
||||
// Hovering holds the banner open; the countdown restarts on leave.
|
||||
running: root.timeoutMs > 0 && !hover.hovered
|
||||
// Hovering, or actively typing a reply, holds the banner open; the
|
||||
// countdown restarts (from the top, same as hover) once both let go.
|
||||
running: root.timeoutMs > 0 && !hover.hovered && !root.replyFocused
|
||||
interval: root.timeoutMs
|
||||
onTriggered: Notifs.dropPopup(root.notification)
|
||||
}
|
||||
@@ -45,7 +41,10 @@ Item {
|
||||
width: parent.width
|
||||
notification: root.notification
|
||||
onDismissed: Notifs.dropPopup(root.notification)
|
||||
onReplyFocusChanged: focused => root.replyFocusChanged(focused)
|
||||
onReplyFocusChanged: focused => {
|
||||
root.replyFocused = focused;
|
||||
root.replyFocusChanged(focused);
|
||||
}
|
||||
|
||||
// Slide in from the right edge. Runs once, on creation.
|
||||
NumberAnimation on x {
|
||||
|
||||
@@ -17,7 +17,11 @@ PanelWindow {
|
||||
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: Theme.barHeight + Theme.barGap * 2
|
||||
// The gap ALONE, not the bar height plus the gap. exclusiveZone 0 means
|
||||
// "reserve nothing, but respect what others reserved", so this surface
|
||||
// already begins below the bar's zone -- adding the bar height here counted
|
||||
// it twice and left the surface floating 48px under the bar instead of 12.
|
||||
margins.top: Theme.barGap * 2
|
||||
margins.right: Theme.barSideMargin
|
||||
exclusiveZone: 0
|
||||
|
||||
@@ -45,7 +49,17 @@ PanelWindow {
|
||||
spacing: Theme.itemSpacing
|
||||
|
||||
Repeater {
|
||||
model: Notifs.popups.slice(0, Settings.maxVisibleToasts)
|
||||
// Notifs.popups.slice() is a fresh array on every change (a new
|
||||
// arrival, a dismissal, a sibling toast timing out). Handing that
|
||||
// straight to Repeater would reset the model and rebuild every
|
||||
// delegate each time, blowing away whichever toast has a reply
|
||||
// field mid-typing. ScriptModel diffs by object identity
|
||||
// (Notification instances are unique QObjects), so only genuinely
|
||||
// added/removed notifications add/remove delegates — unrelated
|
||||
// toasts, and their slide-in animations, are untouched.
|
||||
model: ScriptModel {
|
||||
values: Notifs.popups.slice(0, Settings.maxVisibleToasts)
|
||||
}
|
||||
|
||||
Toast {
|
||||
required property var modelData
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// 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 {
|
||||
item: inputMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: inputMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: event => {
|
||||
if (event.button !== Qt.RightButton)
|
||||
return;
|
||||
ShellState.openSettings("accessibility");
|
||||
OsdState.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -176,34 +176,34 @@ Item {
|
||||
height: aspect > 0 ? width / aspect : parent.height
|
||||
opacity: hasContent ? 1 : 0
|
||||
|
||||
// The capture context isn't ready the instant the layer surface is
|
||||
// told to become visible — it needs the surface to actually map,
|
||||
// which takes a frame or two. Calling captureFrame() before then
|
||||
// logs "no recording context is ready" and yields nothing, so
|
||||
// retry a few times and then give up quietly (the caption and app
|
||||
// icon are still shown, so a missing thumbnail is cosmetic).
|
||||
property int captureAttempts: 0
|
||||
// A ScreencopyView cannot tell us that its recording context is
|
||||
// ready before a capture. The enclosing overview gets that
|
||||
// context only after its first rendered frame, so defer the first
|
||||
// one-shot capture to that event. Retrying before then only emits
|
||||
// "no recording context is ready" warnings; if a capture later
|
||||
// cannot produce content, the app icon remains the fallback.
|
||||
property bool recordingReady: false
|
||||
|
||||
function tryCapture(): void {
|
||||
captureAttempts = 0;
|
||||
captureRetry.restart();
|
||||
}
|
||||
|
||||
// `shot`, not `parent`: Timer is a QtObject, so `parent` does not
|
||||
// resolve to the enclosing ScreencopyView.
|
||||
Timer {
|
||||
id: captureRetry
|
||||
interval: 80
|
||||
repeat: true
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (shot.hasContent || shot.captureAttempts >= 6) {
|
||||
stop();
|
||||
if (shot.hasContent)
|
||||
return;
|
||||
if (!shot.recordingReady) {
|
||||
frameReady.restart();
|
||||
return;
|
||||
}
|
||||
shot.captureAttempts++;
|
||||
shot.captureFrame();
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: frameReady
|
||||
running: false
|
||||
onTriggered: {
|
||||
running = false;
|
||||
if (shot.hasContent)
|
||||
return;
|
||||
shot.recordingReady = true;
|
||||
shot.tryCapture();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: tryCapture()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// One workspace in the overview: a proportional card holding live thumbnails
|
||||
// of its windows, labelled underneath. The focused workspace is outlined in the
|
||||
// accent colour, as GNOME outlines the current workspace.
|
||||
// of its windows, labeled underneath. The focused workspace is outlined in the
|
||||
// accent color, as GNOME outlines the current workspace.
|
||||
//
|
||||
// A card with a null `workspace` is the trailing "new workspace" affordance
|
||||
// that dynamic-workspace setups (GNOME's, and Hyprland's) always show.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// The authentication prompt.
|
||||
//
|
||||
// Deliberately a Panama surface rather than the compositor's stock agent: this
|
||||
// is the window that asks for the password to everything, and it was the one
|
||||
// window on the desktop that looked like it belonged to something else.
|
||||
//
|
||||
// Two things here are security, not styling. It takes EXCLUSIVE keyboard focus,
|
||||
// so keystrokes cannot reach the window underneath while a password is being
|
||||
// typed. And the field is cleared on every exit path, including the ones nobody
|
||||
// plans for.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.settings
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
visible: Polkit.active
|
||||
color: "transparent"
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
exclusiveZone: 0
|
||||
|
||||
WlrLayershell.namespace: "qs-polkit"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
// Exclusive, not OnDemand: a password prompt that lets keystrokes through
|
||||
// to whatever is behind it is a keylogger with extra steps.
|
||||
WlrLayershell.keyboardFocus: Polkit.active
|
||||
? WlrKeyboardFocus.Exclusive
|
||||
: WlrKeyboardFocus.None
|
||||
|
||||
onVisibleChanged: {
|
||||
if (root.visible) {
|
||||
field.text = "";
|
||||
field.forceActiveFocus();
|
||||
} else {
|
||||
field.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
// Dims what is behind, and swallows clicks so nothing outside the dialog
|
||||
// can be operated while it is waiting.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
// Clicking outside does nothing on purpose. Dismissing an
|
||||
// authentication request by misclick, and having the thing that
|
||||
// asked report a mysterious failure, is worse than an explicit
|
||||
// Cancel.
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 420
|
||||
implicitHeight: layout.implicitHeight + 44
|
||||
radius: Theme.popoverRadius
|
||||
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
||||
border.width: 1
|
||||
border.color: Theme.alpha(Theme.fg, 0.1)
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 44
|
||||
spacing: 14
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: "Authentication required"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.DemiBold
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: Polkit.message
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: Polkit.users.length > 1
|
||||
text: "Authenticating as " + Polkit.chosenUser
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
PasswordField {
|
||||
id: field
|
||||
width: parent.width
|
||||
placeholder: "Password"
|
||||
enabled: !Polkit.authenticating
|
||||
onAccepted: {
|
||||
if (field.text !== "")
|
||||
Polkit.submit(field.text);
|
||||
field.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: Polkit.failureText !== ""
|
||||
text: Polkit.failureText
|
||||
color: Theme.danger
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
text: "Cancel"
|
||||
onClicked: Polkit.cancel()
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: Polkit.authenticating ? "Checking…" : "Authenticate"
|
||||
tone: "accent"
|
||||
enabled: !Polkit.authenticating
|
||||
onClicked: {
|
||||
if (field.text !== "")
|
||||
Polkit.submit(field.text);
|
||||
field.text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Escape cancels, which is what every other dialog on this desktop does.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.onEscapePressed: Polkit.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
module qs.modules.polkit
|
||||
PolkitPrompt 1.0 PolkitPrompt.qml
|
||||
@@ -6,6 +6,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -16,23 +17,12 @@ Item {
|
||||
|
||||
implicitHeight: list.implicitHeight
|
||||
|
||||
readonly property var nodes: {
|
||||
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 nodes: root.output ? AudioDevices.outputs : AudioDevices.inputs
|
||||
|
||||
readonly property var current: root.output ? Pipewire.defaultAudioSink : Pipewire.defaultAudioSource
|
||||
readonly property var current: AudioDevices.current(root.output)
|
||||
|
||||
function select(node): void {
|
||||
if (root.output)
|
||||
Pipewire.preferredDefaultAudioSink = node;
|
||||
else
|
||||
Pipewire.preferredDefaultAudioSource = node;
|
||||
AudioDevices.select(root.output, node)
|
||||
}
|
||||
|
||||
ScrollColumn {
|
||||
@@ -52,7 +42,7 @@ Item {
|
||||
implicitHeight: 38
|
||||
icon: root.output ? "audio-speakers-symbolic" : "audio-input-microphone-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
|
||||
onClicked: root.select(nodeRow.modelData)
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ Item {
|
||||
|
||||
width: parent.width
|
||||
// BlueZ reports a plain freedesktop name ("audio-headphones");
|
||||
// the symbolic variant is the one that can be recoloured.
|
||||
// the symbolic variant is the one that can be recolored.
|
||||
icon: deviceRow.modelData.icon !== "" ? deviceRow.modelData.icon + "-symbolic" : "bluetooth-symbolic"
|
||||
iconFallback: "bluetooth-symbolic"
|
||||
label: deviceRow.modelData.name || deviceRow.modelData.address
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// at startup — backlight devices do not appear and disappear.
|
||||
// Two exist and they are not interchangeable:
|
||||
//
|
||||
// The kernel backlight class, driven by brightnessctl. Laptop panels have it;
|
||||
// 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 Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.widgets
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool available: false
|
||||
property real value: 0
|
||||
property bool backlightAvailable: false
|
||||
property real backlightValue: 0
|
||||
|
||||
visible: root.available
|
||||
implicitHeight: root.available ? 32 : 0
|
||||
readonly property int rowCount: (root.backlightAvailable ? 1 : 0) + Brightness.displays.length
|
||||
readonly property bool labeled: 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
|
||||
Process {
|
||||
@@ -34,20 +54,87 @@ Item {
|
||||
const fields = line.split(",");
|
||||
if (fields.length < 5 || fields[1] !== "backlight")
|
||||
continue;
|
||||
root.available = true;
|
||||
root.value = parseInt(fields[3]) / 100;
|
||||
root.backlightAvailable = true;
|
||||
root.backlightValue = parseInt(fields[3]) / 100;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function apply(v: real): void {
|
||||
root.value = v;
|
||||
function applyBacklight(v: real): void {
|
||||
root.backlightValue = v;
|
||||
// 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)) + "%"]);
|
||||
}
|
||||
|
||||
Column {
|
||||
id: rows
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: 4
|
||||
|
||||
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 unlabeled slider.
|
||||
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
||||
value: modelData.value / 100
|
||||
onMoved: v => Brightness.set(modelData.bus, Math.round(v * 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BrightnessRow: Item {
|
||||
id: row
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
signal moved(real value)
|
||||
|
||||
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.labeled
|
||||
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 center 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.
|
||||
// recoloring to be visible — see ThemedIcon.
|
||||
ThemedIcon {
|
||||
id: glyph
|
||||
anchors.left: parent.left
|
||||
@@ -63,7 +150,9 @@ Item {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
value: root.value
|
||||
onMoved: v => root.apply(v)
|
||||
value: row.value
|
||||
onMoved: v => row.moved(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,30 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: restingCard
|
||||
|
||||
// Shown when the section is closed; the expanded grid below replaces
|
||||
// it when open.
|
||||
readonly property bool shown: root.hasSelection && !root.expanded
|
||||
|
||||
width: parent.width
|
||||
height: visible ? restingGrid.implicitHeight + 20 : 0
|
||||
visible: root.hasSelection && !root.expanded
|
||||
|
||||
// Animated, and on the same curve and duration as the Section that
|
||||
// replaces it. Previously this hid the instant `expanded` flipped:
|
||||
// a Column skips invisible children, so everything above snapped up
|
||||
// while the expanded grid was still sliding open underneath. That
|
||||
// read as a glitch rather than as an animation, and only here,
|
||||
// because this is the only control with a resting and an expanded
|
||||
// form that swap.
|
||||
height: restingCard.shown ? restingGrid.implicitHeight + 20 : 0
|
||||
visible: height > 0
|
||||
clip: true
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.durNormal
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
radius: Theme.cardRadius + 2
|
||||
color: Theme.alpha(Theme.warn, 0.028)
|
||||
border.width: 1
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// The three system power profiles, as an expandable list.
|
||||
//
|
||||
// A row per profile rather than a cycling button: there are three, and cycling
|
||||
// through them means passing through one you did not want on a machine where
|
||||
// the change is immediate and audible. The same shape the audio device lists
|
||||
// use, so the panel reads consistently.
|
||||
//
|
||||
// The daemon owns the profile -- it survives a shell restart and anything else
|
||||
// on the system can change it -- so this reads back rather than assuming, the
|
||||
// same as monitor brightness.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: PowerProfiles.profiles
|
||||
|
||||
RowButton {
|
||||
required property var modelData
|
||||
|
||||
width: root.width
|
||||
icon: {
|
||||
switch (modelData) {
|
||||
case "power-saver": return "power-profile-power-saver-symbolic";
|
||||
case "performance": return "power-profile-performance-symbolic";
|
||||
default: return "power-profile-balanced-symbolic";
|
||||
}
|
||||
}
|
||||
iconFallback: "preferences-system-power-symbolic"
|
||||
label: PowerProfiles.label(modelData)
|
||||
sublabel: PowerProfiles.detail(modelData)
|
||||
selected: modelData === PowerProfiles.active
|
||||
dimmed: PowerProfiles.busy
|
||||
onClicked: PowerProfiles.set(modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Only when it is true. A machine that cannot deliver the profile it is set
|
||||
// to is the one case where the label alone is misleading.
|
||||
Text {
|
||||
visible: PowerProfiles.degraded !== ""
|
||||
width: root.width
|
||||
text: "Limited right now: " + PowerProfiles.degraded
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
leftPadding: 10
|
||||
topPadding: 4
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,21 @@ PanelWindow {
|
||||
|
||||
// Hangs directly beneath the top-right status cluster. Two logical pixels
|
||||
// preserve the Prism edge without making the surface feel detached.
|
||||
//
|
||||
// The margin is the gap ALONE, not the bar height plus the gap. An
|
||||
// exclusiveZone of 0 means "reserve nothing, but respect what others
|
||||
// reserved", so this surface already begins below the bar's 36px zone;
|
||||
// adding the bar height here counted it twice and left the panel floating
|
||||
// 38px under the bar instead of 2px. It also means the panel follows the
|
||||
// bar on its own -- if the bar ever stops reserving space, this closes up
|
||||
// against the top edge rather than hanging under nothing.
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: Theme.barHeight + Theme.controlCenterTopGap
|
||||
// The same gap every other popover uses. This had its own constant set to
|
||||
// 2, which left the widest surface in the shell hanging ten pixels higher
|
||||
// than the date menu beside it -- an early value nothing else converged on
|
||||
// rather than a decision; it carried no reason, while barGap does.
|
||||
margins.top: Theme.barGap * 2
|
||||
margins.right: Theme.barSideMargin
|
||||
exclusiveZone: 0
|
||||
|
||||
@@ -45,6 +57,9 @@ PanelWindow {
|
||||
if (root.visible) {
|
||||
KdeConnect.refresh();
|
||||
HomeAssistant.refresh();
|
||||
// The daemon owns the power profile and anything on the system can
|
||||
// change it, so the panel asks rather than trusting what it last saw.
|
||||
PowerProfiles.refresh();
|
||||
openAnim.restart();
|
||||
} else {
|
||||
panel.reset();
|
||||
|
||||
@@ -107,6 +107,27 @@ Item {
|
||||
onExpanded: root.expand("wifi")
|
||||
}
|
||||
|
||||
// Only present when there is a wired device at all -- most machines
|
||||
// this runs on have one, but a laptop without a dock does not, and
|
||||
// an Ethernet tile there would be a control for absent hardware.
|
||||
Toggle {
|
||||
width: root.cellWidth
|
||||
visible: Connectivity.wiredDevice !== null
|
||||
icon: Connectivity.wiredOn
|
||||
? "network-wired-symbolic"
|
||||
: "network-wired-disconnected-symbolic"
|
||||
label: "Ethernet"
|
||||
active: Connectivity.wiredOn
|
||||
enabled: Connectivity.wiredAvailable
|
||||
sublabel: {
|
||||
if (Connectivity.wiredOn)
|
||||
return Connectivity.wiredDevice.linkSpeed > 0
|
||||
? Connectivity.wiredDevice.linkSpeed + " Mb/s" : "Connected";
|
||||
return "Off";
|
||||
}
|
||||
onToggled: Connectivity.setWired(!Connectivity.wiredOn)
|
||||
}
|
||||
|
||||
Toggle {
|
||||
width: root.cellWidth
|
||||
icon: root.btAdapter && root.btAdapter.enabled ? "bluetooth-active-symbolic" : "bluetooth-disabled-symbolic"
|
||||
@@ -130,6 +151,21 @@ Item {
|
||||
onToggled: Caffeine.toggle()
|
||||
}
|
||||
|
||||
Toggle {
|
||||
width: root.cellWidth
|
||||
icon: ColorScheme.dark ? "weather-clear-night-symbolic" : "weather-clear-symbolic"
|
||||
label: "Appearance"
|
||||
sublabel: ColorScheme.dark ? "Dark" : "Light"
|
||||
// "active" reads as the non-default state across this grid, and
|
||||
// dark is what Panama ships, so light is the lit one.
|
||||
active: !ColorScheme.dark
|
||||
// Writes the preference and stops. ColorScheme propagates it to
|
||||
// GTK, the portal, the terminals, the launcher, btop, tmux and
|
||||
// the lock screen; nothing here needs to know that list.
|
||||
onToggled: SystemSettings.commitPreference("colorScheme",
|
||||
ColorScheme.dark ? "light" : "dark")
|
||||
}
|
||||
|
||||
Toggle {
|
||||
width: root.cellWidth
|
||||
icon: "night-light-symbolic"
|
||||
@@ -190,6 +226,38 @@ Item {
|
||||
color: Theme.alpha(Theme.fg, 0.1)
|
||||
}
|
||||
|
||||
// ── Power ───────────────────────────────────────────────────────────
|
||||
// Only where a power-profiles daemon is actually running. A desktop
|
||||
// without one should not show a control that cannot do anything.
|
||||
RowButton {
|
||||
visible: PowerProfiles.available
|
||||
width: content.width
|
||||
icon: {
|
||||
switch (PowerProfiles.active) {
|
||||
case "power-saver": return "power-profile-power-saver-symbolic";
|
||||
case "performance": return "power-profile-performance-symbolic";
|
||||
default: return "power-profile-balanced-symbolic";
|
||||
}
|
||||
}
|
||||
iconFallback: "preferences-system-power-symbolic"
|
||||
label: "Power profile"
|
||||
sublabel: PowerProfiles.degraded !== ""
|
||||
? PowerProfiles.label(PowerProfiles.active) + " · limited"
|
||||
: PowerProfiles.label(PowerProfiles.active)
|
||||
selected: root.expandedSection === "power"
|
||||
onClicked: root.expand("power")
|
||||
}
|
||||
|
||||
Section {
|
||||
width: content.width
|
||||
expanded: root.expandedSection === "power"
|
||||
|
||||
PowerProfileList {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sliders ─────────────────────────────────────────────────────────
|
||||
AudioSlider {
|
||||
width: content.width
|
||||
@@ -257,15 +325,45 @@ Item {
|
||||
width: content.width
|
||||
height: 36
|
||||
|
||||
ThemedIcon {
|
||||
// The account's own picture, the same file the lock screen and the
|
||||
// login screen read. A generic glyph sat here while a real avatar
|
||||
// was already set, which made the desktop look like it did not know
|
||||
// whose it was.
|
||||
Item {
|
||||
id: avatar
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
height: 22
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
visible: UserAccounts.avatarUrl !== ""
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: UserAccounts.avatarUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
// Replaced in place when the picture changes, so the
|
||||
// cache has to be told to let go of the old one.
|
||||
cache: false
|
||||
asynchronous: true
|
||||
sourceSize.width: 44
|
||||
sourceSize.height: 44
|
||||
}
|
||||
}
|
||||
|
||||
ThemedIcon {
|
||||
anchors.centerIn: parent
|
||||
visible: UserAccounts.avatarUrl === ""
|
||||
size: 20
|
||||
icon: "avatar-default-symbolic"
|
||||
iconFallback: "user-info-symbolic"
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: avatar.right
|
||||
@@ -273,7 +371,9 @@ Item {
|
||||
anchors.right: actions.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Quickshell.env("USER") || "user"
|
||||
text: UserAccounts.me
|
||||
? UserAccounts.displayName(UserAccounts.me)
|
||||
: (Quickshell.env("USER") || "user")
|
||||
color: Theme.fg
|
||||
elide: Text.ElideRight
|
||||
font.family: Theme.fontFamily
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user