Compare commits
441
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a69cacc006 | ||
|
|
28e387868f | ||
|
|
1f65e09865 | ||
|
|
dce00e45d1 | ||
|
|
0d1841cf86 | ||
|
|
ba4e5e6677 | ||
|
|
1ffd05f0e1 | ||
|
|
92c60c3ecd | ||
|
|
31fc1d3eaa | ||
|
|
8264ca6320 | ||
|
|
eab4280969 | ||
|
|
1c3cd7ac72 | ||
|
|
8652f92aae | ||
|
|
4abf3f8acd | ||
|
|
3f8539176a | ||
|
|
18ecc2244f | ||
|
|
94a4314730 | ||
|
|
27acbe7a95 | ||
|
|
fc8f226747 | ||
|
|
98e29eb8f3 | ||
|
|
00e11e0b8d | ||
|
|
ac25fd776c | ||
|
|
51e2c8418a | ||
|
|
d267da58ad | ||
|
|
20b41c3976 | ||
|
|
bfb37afd69 | ||
|
|
b361db8486 | ||
|
|
12b858371e | ||
|
|
77f625d7bb | ||
|
|
34e295d184 | ||
|
|
e19fb00276 | ||
|
|
37688f702a | ||
|
|
71f4f0d24b | ||
|
|
3839183401 | ||
|
|
68b6664111 | ||
|
|
803362ebad | ||
|
|
6a7bdcb835 | ||
|
|
37fd5e890e | ||
|
|
a19c6dc3ef | ||
|
|
6d71a77798 | ||
|
|
d8144a66c1 | ||
|
|
43d1e15858 | ||
|
|
1192ad64dc | ||
|
|
044139ed63 | ||
|
|
0d894afe3e | ||
|
|
54dfa977ee | ||
|
|
5f09c5ee38 | ||
|
|
f33da41cc6 | ||
|
|
9b338608ef | ||
|
|
cc7d91d09c | ||
|
|
ada0faf1d1 | ||
|
|
045a774847 | ||
|
|
beed44dd87 | ||
|
|
8105849151 | ||
|
|
bd2fd12de2 | ||
|
|
06c53d6c21 | ||
|
|
f9e5d3f470 | ||
|
|
88371d19f0 | ||
|
|
8b1205e4b8 | ||
|
|
be0e55214b | ||
|
|
9ffaf45a4d | ||
|
|
e1ff25fc66 | ||
|
|
6f0ce639d9 | ||
|
|
4ec8bd94d9 | ||
|
|
5a0643357f | ||
|
|
b30bf40407 | ||
|
|
aba2d16ffa | ||
|
|
b8f88a91f3 | ||
|
|
d5b6e62515 | ||
|
|
07db1068f1 | ||
|
|
ffce48964e | ||
|
|
b58371bb35 | ||
|
|
9bc68ba358 | ||
|
|
1f694b00b6 | ||
|
|
f8f5b25510 | ||
|
|
4d7a194300 | ||
|
|
cb7c09d208 | ||
|
|
7578348db1 | ||
|
|
5490fd285d | ||
|
|
50077a0c31 | ||
|
|
14fc4fc8a3 | ||
|
|
e723fabed3 | ||
|
|
c0eee076b4 | ||
|
|
31668619a4 | ||
|
|
9530e32326 | ||
|
|
153554b5df | ||
|
|
44124d72fa | ||
|
|
3d21e20041 | ||
|
|
dfc0c49877 | ||
|
|
c2eddc2c5b | ||
|
|
6ad2e017e8 | ||
|
|
12f6b2a310 | ||
|
|
6510fdda0f | ||
|
|
36fdd4e076 | ||
|
|
d865cb74a1 | ||
|
|
a95c480c6a | ||
|
|
78db2d5689 | ||
|
|
669a228286 | ||
|
|
89761a7da3 | ||
|
|
8b96d907a1 | ||
|
|
7a5e990439 | ||
|
|
41dd91eb75 | ||
|
|
4910bd659c | ||
|
|
8d66247b7c | ||
|
|
68cbf892e9 | ||
|
|
05fd5346db | ||
|
|
333ab9f6b7 | ||
|
|
e1a04d2d70 | ||
|
|
202b5b89ac | ||
|
|
9202697734 | ||
|
|
9fbbdd902b | ||
|
|
6ae8265730 | ||
|
|
317b7a0962 | ||
|
|
50a99a5ad0 | ||
|
|
bc6d63b70f | ||
|
|
3c359f3f7e | ||
|
|
e446a1072c | ||
|
|
3c45d63085 | ||
|
|
32bebc2b07 | ||
|
|
12d538c4af | ||
|
|
c02329ac3c | ||
|
|
4e978bf3b7 | ||
|
|
1e430881e5 | ||
|
|
f42b3cfe0e | ||
|
|
51ceb19480 | ||
|
|
25e2328658 | ||
|
|
9f563c8f94 | ||
|
|
7d633eb06e | ||
|
|
6d1f3f3763 | ||
|
|
de923cb4d5 | ||
|
|
bd9a55c8eb | ||
|
|
cab7699711 | ||
|
|
86825e7327 | ||
|
|
89417cd6d4 | ||
|
|
41b86c7a9e | ||
|
|
185d7edaa5 | ||
|
|
538c0a887c | ||
|
|
033d5b21f8 | ||
|
|
7cd4131327 | ||
|
|
b280bd02d7 | ||
|
|
4c77bc2f61 | ||
|
|
9092a80f66 | ||
|
|
62dce86b4e | ||
|
|
725e274ef4 | ||
|
|
f09763ef5d | ||
|
|
215da285f3 | ||
|
|
48f7c1e962 | ||
|
|
f457c1eb9f | ||
|
|
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 | ||
|
|
c2cd79b547 | ||
|
|
0a18290137 | ||
|
|
efc53435a2 | ||
|
|
67674d297e | ||
|
|
2d4b126f14 | ||
|
|
5671324eb2 | ||
|
|
83391e0453 | ||
|
|
172b099a04 | ||
|
|
cae72b5179 | ||
|
|
b3b8e0d66d | ||
|
|
787d2b121a | ||
|
|
1b323c2fa5 | ||
|
|
1ca571458c | ||
|
|
768801dbe4 | ||
|
|
375ecfcd95 | ||
|
|
ce95b34d19 | ||
|
|
9d430a3079 | ||
|
|
bac68d2bfb | ||
|
|
634a9ebe07 | ||
|
|
150f3cdb09 | ||
|
|
6377bfb8fd | ||
|
|
2bc12e6022 | ||
|
|
fe7c85e471 | ||
|
|
8a04e4f9d1 | ||
|
|
5347954a88 | ||
|
|
00a81edadd | ||
|
|
4bd3638012 | ||
|
|
7f540a58e5 | ||
|
|
7ac5355a77 | ||
|
|
a9eb90f457 | ||
|
|
b5c9156e64 | ||
|
|
3213a0989a | ||
|
|
fbf729b25c | ||
|
|
e37df9d838 | ||
|
|
673aac6aba | ||
|
|
c8eb344a0b | ||
|
|
65966200fe | ||
|
|
2130d6a1a5 | ||
|
|
9a99c6064e | ||
|
|
4f93522f7d | ||
|
|
a325ec807e | ||
|
|
c2b19f7545 | ||
|
|
925fc0276f | ||
|
|
65a8197419 | ||
|
|
ff67387c42 | ||
|
|
c42794c5e2 | ||
|
|
0964af7b73 | ||
|
|
15fb008954 | ||
|
|
f4c3ffed43 | ||
|
|
5f965daa6a | ||
|
|
5940d6b4bd | ||
|
|
190e3200e0 | ||
|
|
49ec4d8f6a | ||
|
|
173c00fca7 | ||
|
|
3a903934c0 | ||
|
|
dc6da5af34 | ||
|
|
922395d2b4 | ||
|
|
a5450fdb6d | ||
|
|
1ce5c732b7 | ||
|
|
f9cc15fb96 | ||
|
|
2728fb08fb | ||
|
|
f27a1dca11 | ||
|
|
03914e23af | ||
|
|
6a1bec849d | ||
|
|
fb27c5b309 | ||
|
|
d37d4a87de | ||
|
|
530a5590a6 | ||
|
|
e92c2ee5f9 | ||
|
|
5248883e4b | ||
|
|
67033f2a31 | ||
|
|
2dc9676822 | ||
|
|
89666e7485 | ||
|
|
7123bb4885 | ||
|
|
7461c88b98 | ||
|
|
11202d06d9 | ||
|
|
ac57cfa319 | ||
|
|
4c529afae7 | ||
|
|
372a0f60e5 | ||
|
|
19b8ec2542 | ||
|
|
210bdbdddc | ||
|
|
47137191dd | ||
|
|
ef2d49fbaa | ||
|
|
065b7747cf | ||
|
|
cfc2b82c06 | ||
|
|
e985792472 | ||
|
|
d9ee1ddce4 | ||
|
|
4fcfb9c45e | ||
|
|
50ea51263a | ||
|
|
53bec8ddbc | ||
|
|
b8ce2aef5c | ||
|
|
7c55264138 | ||
|
|
6150274903 | ||
|
|
c0ba11768a | ||
|
|
fba92951f9 | ||
|
|
11863984ca | ||
|
|
e4f1fe8e05 | ||
|
|
18cfd8b174 |
@@ -0,0 +1,236 @@
|
|||||||
|
# Panama server role: headless installs and versioned containers
|
||||||
|
|
||||||
|
2026-08-25. Designed with Gib across two interview rounds; every decision below
|
||||||
|
was made explicitly, none inferred.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Panama becomes the one repo for every Fedora machine Gib runs. A desktop gets
|
||||||
|
what it gets today. A server — a Hetzner VPS, a future home server — gets the
|
||||||
|
same shell environment (bash profile, prompt, tmux-on-ssh, agents) minus
|
||||||
|
everything that needs a screen, plus rootless podman, systemd user units, and a
|
||||||
|
versioned catalog of compose services that `panama server enable` brings up.
|
||||||
|
The repo stays public; secrets never enter it.
|
||||||
|
|
||||||
|
## Decisions of record
|
||||||
|
|
||||||
|
| Decision | Choice |
|
||||||
|
|---|---|
|
||||||
|
| Repo | One public monorepo. No companion repo. `.gitignore` + a secrets contract keep it safe |
|
||||||
|
| Role | Asked first in the interview (`desktop` / `server`), persisted to `~/.local/state/panama/role`; `--server` on `boot`/`install` presets it |
|
||||||
|
| Layout | Top-level `server/`, services under `server/containers/<Name>/` (TitleCase, matching junior) |
|
||||||
|
| Tracked per service | `compose.yml`, `podman-<name>.service`, `.env.example`. Nothing else |
|
||||||
|
| `~/Server` | Real directories. Only `compose.yml` is symlinked into them; units symlink into `~/.config/systemd/user/`. Live `.env` and data stay outside the repo tree entirely |
|
||||||
|
| Data dirs | Standardized to `./data/`, applied per service at cutover (a stop/rename/start), never at import |
|
||||||
|
| Enablement | `panama server` subcommand family, not the interview |
|
||||||
|
| Units | Checked in per service (they legitimately differ), installed as symlinks so edits flow with `panama update` |
|
||||||
|
| Updates | `panama update` relinks and `daemon-reload`s but never restarts a running service; it names services whose definitions changed |
|
||||||
|
| Nightly image updates | junior's `update-containers.sh` + `podman-update.timer` adopted into the repo (it replaced watchtower for cause) |
|
||||||
|
| Codex | Installed everywhere, same pattern as Claude Code |
|
||||||
|
| Ports | 80/443/81 only. Anything else is opened by hand and documented in the service's folder |
|
||||||
|
| VPS bootstrap | `boot` run as root walks user-creation/keys/sshd-hardening, each step skipped when already true, then re-runs as the user |
|
||||||
|
| Out of scope | server.gib (docker; translate later), backup and verify-rootless (own design later), cutover of junior's live services (later, per service) |
|
||||||
|
|
||||||
|
## 1. Machine role
|
||||||
|
|
||||||
|
- New interview question, asked first: "What is this machine?" — `desktop` or
|
||||||
|
`server` via `gum choose`. `./install --server` (and `boot --server`) records
|
||||||
|
the answer without asking.
|
||||||
|
- Persisted to `~/.local/state/panama/role`, plain text, one word. This is the
|
||||||
|
first durable answer Panama keeps, and deliberately so: the transient-answers
|
||||||
|
principle exists to keep *personal* data out of durable paths, and a machine
|
||||||
|
role is a fact about the machine. `install`, `panama update`, `panama doctor`
|
||||||
|
and future migrations read it. A machine with no role file is a desktop —
|
||||||
|
every machine that predates this work is one.
|
||||||
|
- On role=server the interview skips: hardware probes (NVIDIA, Secure Boot,
|
||||||
|
firmware — and their bootstrap tools; only `gum` is bootstrapped), the
|
||||||
|
debloat question, and the extras checklist. It keeps: hostname, git
|
||||||
|
identity, gh sign-in, SSH key, user-content.
|
||||||
|
|
||||||
|
## 2. Stages by role
|
||||||
|
|
||||||
|
Role=desktop is exactly today's behavior. Role=server runs:
|
||||||
|
|
||||||
|
| Stage | Server behavior |
|
||||||
|
|---|---|
|
||||||
|
| `install-packages` | Core + server lists (§3). No flatpaks, no COPRs, no desktop repos. Claude Code and Codex install for both roles |
|
||||||
|
| `link-dotfiles` | Universal dot dirs only: `btop`, `nvim`, `ohmyposh`, `tmux`, `vim`, `panama`. Desktop-only (everything else: `hypr`, `quickshell`, `kitty`, `ghostty`, `espanso`, `gtk-*`, `uwsm`, `vicinae`, `wofi`, `xdg-desktop-portal`) is skipped via an explicit classification list in the stage — a new dot dir must be classified or the contract fails. Bash profile links as today; Firefox/wallpaper/cursor seeding skipped |
|
||||||
|
| `link-user` | Unchanged (SSH aliases and agent rules are exactly what a server wants) |
|
||||||
|
| `setup-identity` | Unchanged |
|
||||||
|
| `setup-server` (new) | §4 |
|
||||||
|
| `link-server` (new) | Creates `~/Server`, then delegates to `panama-server relink` so link repair has one owner |
|
||||||
|
| Skipped | `link-skills` (all three shipped skills are desktop-specific today; revisit when a server skill exists), `change-settings`, `link-vicinae-scripts`, `install-hardware` |
|
||||||
|
|
||||||
|
Migrations run on servers too — they are self-guarding no-ops where they do
|
||||||
|
not apply. `panama doctor` learns the role: on a server it reports service
|
||||||
|
health via `panama-server status` instead of probing desktop units.
|
||||||
|
|
||||||
|
## 3. Package restructure
|
||||||
|
|
||||||
|
`setup/packages/` gains `core-packages` (both roles) and `server-packages`;
|
||||||
|
`initial-packages` keeps what is desktop-only. Nothing is added or dropped for
|
||||||
|
desktops — the union of core + desktop lists equals today's set.
|
||||||
|
|
||||||
|
- **core-packages**: awk, bat, btop, curl, dnf5-plugins, eza, fontconfig, fzf,
|
||||||
|
gh, git-all, gum, jq, neovim, oh-my-posh, openssh, openssl, podman,
|
||||||
|
podman-compose, python3-dnf, ripgrep, tmux, unzip, wget, zoxide.
|
||||||
|
(`podman` moves here from development-packages; `podman-compose` is what
|
||||||
|
provides `podman compose` on junior — verified, 1.6.0.)
|
||||||
|
- **server-packages**: nvm (Node via nvm, as on desktop — agents and MCP
|
||||||
|
servers want it, and Codex installs through npm).
|
||||||
|
- **initial-packages** (desktop-only remainder): cargo, rustup, flatpak, fwupd,
|
||||||
|
kitty, ksshaskpass, libselinux-utils, pciutils, python3-neovim,
|
||||||
|
wireguard-tools, vim-X11 — plus everything else currently there that the
|
||||||
|
core list did not claim.
|
||||||
|
- Codex CLI: installed in `install-packages` beside the Claude Code step —
|
||||||
|
skip when `codex` is on PATH, `npm install -g @openai/codex` (after nvm/node
|
||||||
|
is available), soft-fail like Claude Code does.
|
||||||
|
- The package-hash mechanic in `install` is untouched; new files under
|
||||||
|
`-maxdepth 1` join the hash automatically.
|
||||||
|
|
||||||
|
## 4. `setup-server` stage
|
||||||
|
|
||||||
|
Idempotent, like every stage; each item checks before acting:
|
||||||
|
|
||||||
|
1. `loginctl enable-linger $USER`.
|
||||||
|
2. `/etc/sysctl.d/99-rootless-ports.conf` → `net.ipv4.ip_unprivileged_port_start=80`,
|
||||||
|
applied with `sysctl --system` (junior already has exactly this file).
|
||||||
|
3. firewalld: open 80, 443, 81 (NPM admin portal), permanent + runtime.
|
||||||
|
Nothing else — reverse-proxy philosophy, ports open only when unavoidable.
|
||||||
|
4. `podman network create nginx-bridge` if absent (every compose expects it,
|
||||||
|
`external: true`).
|
||||||
|
5. Nightly image updates: `update-containers.sh` is adopted into the repo as
|
||||||
|
`server/scripts/update-containers` (no `.sh`, per repo rule — junior's copy
|
||||||
|
is imported and its `ExecStart`/log paths updated), with
|
||||||
|
`server/systemd/podman-update.service` and `.timer` symlinked into
|
||||||
|
`~/.config/systemd/user/` and the timer enabled. Logs go to
|
||||||
|
`~/Server/logs/` as today.
|
||||||
|
|
||||||
|
## 5. The `panama server` command
|
||||||
|
|
||||||
|
`bin/panama-server`, surfaced as `panama server <verb>` (README's subcommand
|
||||||
|
table and its contract update with it):
|
||||||
|
|
||||||
|
- `list` — every service in `server/containers/`, with enabled/active state.
|
||||||
|
- `enable <Name>` — create `~/Server/<Name>/`; symlink `compose.yml`; seed
|
||||||
|
`~/Server/<Name>/.env` from `.env.example` when the example exists and the
|
||||||
|
live file does not; if the seeded `.env` still contains placeholder values,
|
||||||
|
stop there and say what to fill in — do **not** start; otherwise symlink the
|
||||||
|
unit, `daemon-reload`, `enable --now`.
|
||||||
|
- `disable <Name>` — `disable --now`, remove the unit symlink,
|
||||||
|
`daemon-reload`. `~/Server/<Name>` and its data are never touched.
|
||||||
|
- `status [<Name>]` — unit state plus `podman compose ps` per service.
|
||||||
|
- `relink` — for every service whose unit symlink exists: refresh both
|
||||||
|
symlinks, `daemon-reload`, and report services whose definitions changed
|
||||||
|
since the last run (so the operator restarts deliberately). Called by the
|
||||||
|
`link-server` stage and by `panama update` on role=server.
|
||||||
|
|
||||||
|
Live `.env` files live in `~/Server/<Name>/`, not in the repo tree — the repo
|
||||||
|
carries only `.env.example`. `.gitignore` still covers `.env` everywhere under
|
||||||
|
`server/` as belt and braces.
|
||||||
|
|
||||||
|
## 6. VPS bootstrap (`boot` as root)
|
||||||
|
|
||||||
|
`boot` gains a root path. When `EUID == 0`, before anything else, walk these
|
||||||
|
steps, each checked and skipped when already true:
|
||||||
|
|
||||||
|
1. Ask for a username (default `gib`). Create it with `wheel` if absent; if it
|
||||||
|
exists, use it.
|
||||||
|
2. Ensure the user has a password (needed for sudo) — `passwd` interactively
|
||||||
|
if none is set.
|
||||||
|
3. Copy root's `authorized_keys` only after OpenSSH parses every key line. The
|
||||||
|
destination directory and file are created as the target UID at `0700/0600`
|
||||||
|
and revalidated before hardening is available.
|
||||||
|
4. Offer sshd hardening (yes/no, default yes): atomically install
|
||||||
|
`/etc/ssh/sshd_config.d/00-panama.conf` with `PermitRootLogin no`,
|
||||||
|
`PasswordAuthentication no`, and `KbdInteractiveAuthentication no`. Run
|
||||||
|
`sshd -t` plus effective root/target `sshd -T -C` checks before reloading the
|
||||||
|
detected unit. Restore a prior regular file with metadata on failure. A
|
||||||
|
missing unit or unsupported existing drop-in leaves SSH unchanged and
|
||||||
|
bootstrap continues. The later binding transaction design owns the details.
|
||||||
|
5. Move/clone the checkout under the user's home, chown it, and re-exec
|
||||||
|
`install --server` as that user.
|
||||||
|
|
||||||
|
Run as a normal user, `boot` behaves exactly as today (plus passing `--server`
|
||||||
|
through).
|
||||||
|
|
||||||
|
## 7. Importing junior's services (the first population)
|
||||||
|
|
||||||
|
A one-time operation, performed against junior over SSH, that **never stops,
|
||||||
|
starts, or restarts anything**. For each dedicated compose dir:
|
||||||
|
|
||||||
|
1. Copy `compose.yml` into `server/containers/<Name>/`.
|
||||||
|
2. Take the **installed** unit (`~/.config/systemd/user/podman-<name>.service`)
|
||||||
|
as truth; diff against the copy in the service dir and flag any divergence.
|
||||||
|
3. Move inline secrets out: secret-bearing values in the repo copy of
|
||||||
|
`compose.yml` become `${VAR}` interpolations; extracted values are written
|
||||||
|
to `~/Server/<Name>/.env` **on junior** (inert until cutover — the live
|
||||||
|
compose still carries them inline); `.env.example` with placeholder values
|
||||||
|
is committed. Services already using `.env`/`env_file` get an
|
||||||
|
`.env.example` generated from the live `.env` with values blanked.
|
||||||
|
4. Verify without touching the stack: `podman compose config` rendered from
|
||||||
|
the repo copy (with the live `.env`) must match the render of the live
|
||||||
|
compose. Volume paths are kept **verbatim** at import (`volumes/`,
|
||||||
|
`volume/`, `pg_data/` and all) so this diff can be exact; the `data/`
|
||||||
|
standardization happens per service at cutover.
|
||||||
|
5. Commit. The secrets contract (§8) gates every one of these commits.
|
||||||
|
|
||||||
|
**Import list (23):** Adminer, Agentchat, Authentik, Beszel, CalCom,
|
||||||
|
ConvexMonorepo, Documenso, Gitea, Infisical, ISponsorBlockTV, LashAddict, N8n,
|
||||||
|
Nginx_Proxy_Manager, Penpot, Portainer, PostgreSQL, Rustdesk, Spoon, StPeteIT,
|
||||||
|
Tools, Uptime, UseSend, Vaultwarden. CalCom, Documenso, Penpot and N8n are
|
||||||
|
currently parked (dir present, unit not active) — imported anyway, noted as
|
||||||
|
parked in the catalog.
|
||||||
|
|
||||||
|
**Excluded:** Watchtower (retired — replaced by the update timer after it took
|
||||||
|
gitea down; its dir stays on junior until cleanup), Completeuphoria,
|
||||||
|
sierraandtyler, Bang (source checkouts deployed by CI from their own repos —
|
||||||
|
Panama owning a second copy of their compose would drift), `ci/`, `logs/`,
|
||||||
|
and junior's loose scripts other than `update-containers.sh` (§4).
|
||||||
|
|
||||||
|
**Cutover** (per service, later, not this work): stop → rename the volume dir
|
||||||
|
to `data/` and update the repo compose to match → replace live compose with
|
||||||
|
the symlink, installed unit with the symlink → `daemon-reload` → start →
|
||||||
|
verify. Ten minutes a service, done when Gib chooses, favoring quiet hours.
|
||||||
|
|
||||||
|
## 8. Contracts and docs
|
||||||
|
|
||||||
|
New `tests/server/`, same discipline as the rest:
|
||||||
|
|
||||||
|
- `compose-secrets-contract` — fails when any tracked file under `server/`
|
||||||
|
contains an inline secret-looking value (the `user-content-contract`
|
||||||
|
pattern), when `.gitignore` stops covering `.env` under `server/`, or when
|
||||||
|
`git ls-files` shows a tracked `.env`.
|
||||||
|
- `containers-shape-contract` — every `server/containers/<Name>/` carries
|
||||||
|
`compose.yml` + `podman-<name>.service`; every unit's `WorkingDirectory` is
|
||||||
|
`%h/Server/<Name>`; every `${VAR}` used in a compose file appears in that
|
||||||
|
service's `.env.example`.
|
||||||
|
- `panama-server-contract` — throwaway `HOME`, stubbed `systemctl`/`podman` on
|
||||||
|
PATH: `enable` creates the right symlinks and seeds `.env`; placeholder
|
||||||
|
values block start; `disable` leaves data alone; `relink` reports changed
|
||||||
|
definitions.
|
||||||
|
- `role-contract` — the interview asks role first, `--server` presets it, the
|
||||||
|
role file is written, stages filter correctly per role (via the stage
|
||||||
|
scripts' static/stubbed seams).
|
||||||
|
- `dotfile-classification-contract` — every dir in `config/dot/` appears in
|
||||||
|
exactly one of link-dotfiles' universal/desktop lists.
|
||||||
|
- README: layout section gains `server/`, the subcommand table gains
|
||||||
|
`panama server`, the pinned contract count moves — `readme-contract` and the
|
||||||
|
interview/link-dotfiles contracts that assert today's behavior update in the
|
||||||
|
same change.
|
||||||
|
|
||||||
|
## Phasing
|
||||||
|
|
||||||
|
1. **Role plumbing** — interview question, role file, `--server`, package
|
||||||
|
split, stage filters, `setup-server`, boot's root path.
|
||||||
|
2. **Server skeleton** — `server/` tree, `panama-server`, `link-server`,
|
||||||
|
update-timer adoption, contracts, README/docs.
|
||||||
|
3. **Import** — junior's 23 services into `server/containers/` per §7.
|
||||||
|
4. **Later, separately** — per-service cutover on junior; backup and
|
||||||
|
verify-rootless design; server.gib docker→podman translation; a real
|
||||||
|
end-to-end test on a fresh VPS.
|
||||||
|
|
||||||
|
Testing note: contracts for phases 1–2 run with stubs and throwaway HOMEs, but
|
||||||
|
per standing practice all suite runs batch at the end of the session with
|
||||||
|
Gib's go-ahead. Phase 3 touches junior only with read commands plus the inert
|
||||||
|
`.env` writes described in §7.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(coredumpctl list:*)",
|
||||||
|
"Bash(coredumpctl info:*)",
|
||||||
|
"Bash(journalctl:*)",
|
||||||
|
"Bash(rpm -q:*)",
|
||||||
|
"Bash(panama doctor:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
name: panama
|
||||||
|
description: Use when working on the Panama repository itself — editing anything under config/, setup/, bin/, tests/, or the Quickshell shell and Hyprland Lua it ships. Covers the live-desktop hot-reload rules, the contract-test discipline, the fixture seams, and where the real documentation lives.
|
||||||
|
---
|
||||||
|
|
||||||
|
# panama
|
||||||
|
|
||||||
|
You are editing a running desktop, not a codebase that gets deployed later. Every dotfile in this
|
||||||
|
repository is symlinked into `~/.config`, so a save is live the moment it lands. That single fact
|
||||||
|
drives every rule below.
|
||||||
|
|
||||||
|
Read before large work, in this order:
|
||||||
|
|
||||||
|
- `README.md` — layout, the `panama` command, how installing and updating work
|
||||||
|
- `config/dot/hypr/README.md` — the compositor config is **Lua, not hyprlang**; read "The one
|
||||||
|
thing to know first" and "Never use `hyprctl keyword`" before touching it
|
||||||
|
- `config/dot/quickshell/modules/settings/README.md` — adding a setting, setting ownership,
|
||||||
|
"Things that will bite you", and where state lives; the most load-bearing document here
|
||||||
|
- `docs/settings.md` — generated reference for every settings key (never edit by hand)
|
||||||
|
|
||||||
|
## The live-desktop covenant
|
||||||
|
|
||||||
|
Editing `config/dot/quickshell/**` hot-reloads the live shell on every save.
|
||||||
|
|
||||||
|
1. Every save must leave valid QML. An intermediate broken save is a broken desktop, not a broken
|
||||||
|
build. Convert a component and its body in ONE edit, never two.
|
||||||
|
2. After each batch of saves, check the journal for errors AND "Unable to assign" warnings:
|
||||||
|
`journalctl --user -u panama-quickshell.service --since '-2 minutes' --no-pager`
|
||||||
|
3. Every `.qml` file in `config/dot/quickshell/modules/*` must be registered in that directory's
|
||||||
|
`qmldir` in the same save batch. An unregistered component fails the whole configuration and
|
||||||
|
takes down the bar, dock, and settings together. `tests/quickshell/qmldir-registration-contract`
|
||||||
|
is pure file inspection — run it before the change lands.
|
||||||
|
4. Never `qs kill` from a copied configuration: Quickshell derives the shell ID from config
|
||||||
|
content, so a content-identical harness can share the live shell's ID and kill the desktop.
|
||||||
|
Harnesses use a distinct entry file, `qs -p`, the PID from `qs list --all`, and `kill` that PID.
|
||||||
|
5. Never `hyprctl keyword` — Lua-configured Hyprland refuses it, prints to stdout, and exits 0.
|
||||||
|
`hyprctl eval` also exits 0 on errors. The only proof a write landed is `hyprctl getoption`
|
||||||
|
reading it back. Batch `hyprctl reload` to one per verified change-set, and say when you do it.
|
||||||
|
6. IPC: annotate every parameter and return type or Quickshell silently skips registration; never
|
||||||
|
duplicate an `IpcHandler` target — one silently shadows the other.
|
||||||
|
7. In `config/dot/quickshell/config/PreferenceSchema.qml`, `readAs` describes the compositor's
|
||||||
|
ANSWER shape, not the setting's; a wrong one makes every successful write look rejected.
|
||||||
|
8. No continuously repainting animations (pulse, shimmer, spinners) — they peg the GPU on
|
||||||
|
high-refresh displays.
|
||||||
|
9. `hyprlock.conf`, `hypridle.conf`, `hyprpaper.conf`, `hyprtoolkit.conf` never moved to Lua. Do
|
||||||
|
not "fix" them. Do not install a notification daemon — the shell is one.
|
||||||
|
10. Scripts carry no `.sh` extension (shebang + `chmod +x`), with one deliberate exception:
|
||||||
|
`migrations/*.sh`, which `bin/panama-migrate` globs. No secrets under `user/` — it is
|
||||||
|
world-readable and `tests/setup/user-content-contract` greps for key material.
|
||||||
|
|
||||||
|
## The contract discipline
|
||||||
|
|
||||||
|
Every executable contract under `tests/` is classified in `tests/contracts.manifest`. The rules:
|
||||||
|
|
||||||
|
- `panama contracts <file>` names the contracts that mention a file; run those after touching it.
|
||||||
|
- During a desktop session, run `panama test --safe [pattern]`. `--safe` selects only contracts
|
||||||
|
classified as `hermetic`.
|
||||||
|
- A plain full run prompts in a terminal before any selected non-hermetic contract starts. In
|
||||||
|
automation, grant every required capability with repeatable flags, for example
|
||||||
|
`panama test --allow live-compositor --allow live-desktop keybinds`.
|
||||||
|
- Each contract has a 180-second outer timeout by default. Override it with a positive
|
||||||
|
`PANAMA_TEST_TIMEOUT_SECONDS` value. Failures print the contract's captured stdout and stderr.
|
||||||
|
Successful stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||||
|
- Contracts run directly too: `tests/setup/interview-contract`.
|
||||||
|
- After changing `PreferenceSchema.qml` or `services/SettingsRoutes.qml`, regenerate:
|
||||||
|
`config/dot/quickshell/scripts/panama-settings-docs` (writes `docs/settings.md`) and
|
||||||
|
`config/dot/quickshell/scripts/panama-settings-commands` (writes the launcher deep links).
|
||||||
|
Both take `--check`; `tests/quickshell/settings-docs-contract` fails when stale.
|
||||||
|
- `README.md` pins the contract count and the `panama` subcommand list
|
||||||
|
(`tests/setup/readme-contract`); adding a contract or subcommand means updating it.
|
||||||
|
|
||||||
|
## Seams for testing
|
||||||
|
|
||||||
|
Helpers are built hermetic: validated inputs, JSON out, secrets on stdin only (never argv).
|
||||||
|
Contracts reach them through env seams — `PANAMA_NETWORK_HELPER`, `PANAMA_FINGERPRINT_FIXTURE`,
|
||||||
|
`PANAMA_EXTRAS_DIR`, and the `PANAMA_*_STATIC_ONLY` family that cuts a contract to its
|
||||||
|
file-inspection half. Prefer exercising a helper through its contract's stubbed PATH over running
|
||||||
|
it against the real system.
|
||||||
|
|
||||||
|
## Safety idioms (contract-enforced)
|
||||||
|
|
||||||
|
- Destructive actions are two-stage: `ConfirmAction` with a unique `actionId`, arbitrated through
|
||||||
|
`ShellState.armedConfirm` — one armed confirm app-wide. Danger tone marks only the confirming
|
||||||
|
press.
|
||||||
|
- Failures render through `ErrorRow`; honest empty states through `NotMeasuredRow` with a
|
||||||
|
`because:`; paragraph-length notes through `SettingsNote`.
|
||||||
|
- Stored user actions (custom shortcuts, gestures, window rules) are DATA — an enum kind and a
|
||||||
|
validated target resolved through whitelist tables in `config/dot/hypr/actions.lua`. Nothing in
|
||||||
|
`~/.config/panama/settings.json` may ever be an executable command; the contracts
|
||||||
|
(`tests/quickshell/settings-idiom-contract`, `tests/quickshell/keybind-rebind-contract`) pin all
|
||||||
|
of this.
|
||||||
|
|
||||||
|
## Root work
|
||||||
|
|
||||||
|
Never bare `sudo`. Load the `panama-sudo` skill first. Migrations already follow the rule.
|
||||||
|
|
||||||
|
## Initial bootstrap trust
|
||||||
|
|
||||||
|
Use the complete command in `README.md` for a new machine. It downloads `boot`
|
||||||
|
from the documented commit URL into a private temporary directory, enforces the
|
||||||
|
documented curl time and byte limits, verifies the committed blob with
|
||||||
|
`sha256sum -c`, and passes both pins to the verified script. Desktop and server
|
||||||
|
bootstrap use the same commit and digest. Never substitute a branch URL, pipe a
|
||||||
|
response into Bash, or invent a newer pin from an uncommitted file.
|
||||||
|
|
||||||
|
`boot` validates the full lowercase commit and SHA-256 before Git or install. A
|
||||||
|
fresh destination fetches only that revision and creates a tracked local `main`
|
||||||
|
after HEAD matches. An existing checkout must be clean and an ancestor of the
|
||||||
|
requested revision; it advances with fast-forward only. Dirty, divergent,
|
||||||
|
fetch-failed, or mismatched checkouts stop without reset or install. Once Panama
|
||||||
|
exists, use `panama update` for normal updates.
|
||||||
|
|
||||||
|
`boot --server` is the one exception where a new Fedora VPS may begin as root.
|
||||||
|
Before it offers SSH hardening, it copies a safe root key when possible or
|
||||||
|
verifies the target key. The target user's `.ssh` must be owned by that user at
|
||||||
|
`0700`, and `authorized_keys` must be owned by that user at `0600`. Without a
|
||||||
|
verified target key, SSH hardening is unavailable and the bootstrap continues.
|
||||||
|
Every non-comment key line must parse with `ssh-keygen`. Root-key destination
|
||||||
|
creation and writing run as the target UID, followed by the same owner, mode,
|
||||||
|
and key checks. Do not replace that with root writes or assume the user's
|
||||||
|
primary group matches the username.
|
||||||
|
|
||||||
|
Accepted hardening uses atomic same-directory `00-panama.conf` with exactly
|
||||||
|
`PermitRootLogin no`, `PasswordAuthentication no`, and
|
||||||
|
`KbdInteractiveAuthentication no`. A pre-existing symlink or non-regular
|
||||||
|
object makes hardening unavailable, as does a missing SSH unit. Panama runs
|
||||||
|
`sshd -t`, then checks effective root and target-user policy with `sshd -T -C`
|
||||||
|
before reloading the detected unit. Validation or reload failure restores a
|
||||||
|
prior regular file with its metadata before it retries validation and reload.
|
||||||
|
Failed recovery stops the handoff with instructions that distinguish a prior
|
||||||
|
file from no prior file. The fixture contracts also cover declined hardening
|
||||||
|
and interrupted preparation. `panama test --safe` never reloads a live daemon,
|
||||||
|
so it is not live-host proof.
|
||||||
+45
-4
@@ -1,7 +1,48 @@
|
|||||||
|
# Ignore bash environment variables, and the lock its writer takes.
|
||||||
|
/config/bash/env
|
||||||
|
/config/bash/.env.lock
|
||||||
|
# Server secrets and state never enter the repo: live .env files belong in
|
||||||
|
# ~/Server/<Name>/ (only compose.yml is symlinked back here), and data/ is
|
||||||
|
# the bind-mount convention. Ignored anyway as a seatbelt -- the repo is
|
||||||
|
# public, and tests/server/compose-secrets-contract is the check that this
|
||||||
|
# line still exists.
|
||||||
|
/server/containers/**/.env
|
||||||
|
/server/containers/**/data/
|
||||||
|
# Personal espanso triggers (name, email), seeded per-machine by setup-identity.
|
||||||
|
/config/dot/espanso/match/identity.yml
|
||||||
|
# Ignore backups of old config files
|
||||||
|
/config/old
|
||||||
# Ignore Wireguard config of course!
|
# Ignore Wireguard config of course!
|
||||||
/config/wg/**
|
/config/wg/**
|
||||||
# Ignore bash environment variables.
|
# Ignore Neovim lazy-lock file
|
||||||
/config/bash/env
|
|
||||||
# Ignore backups of old config files
|
|
||||||
/config/old/**
|
|
||||||
/config/dot/nvim/lazy-lock.json
|
/config/dot/nvim/lazy-lock.json
|
||||||
|
# Local visual-companion sessions and generated design mocks.
|
||||||
|
/.superpowers/
|
||||||
|
# Local linked worktrees used for reviewed feature development.
|
||||||
|
/.worktrees/
|
||||||
|
# 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-3.0/bookmarks
|
||||||
|
/config/dot/gtk-4.0/settings.ini
|
||||||
|
/config/dot/tmux/current-theme.conf
|
||||||
|
/config/dot/hypr/hyprlock.conf
|
||||||
|
# The Firefox chrome override sheet, rendered from the active theme. It sits
|
||||||
|
# beside custom.css because chrome/ is symlinked into the profile as one
|
||||||
|
# directory; custom.css imports it and stays the file a person edits.
|
||||||
|
/config/firefox/chrome/panama-theme.css
|
||||||
|
/config/firefox/chrome/panama-theme.css.tmp
|
||||||
|
|
||||||
|
# Build products of the Vicinae extension. The source and reviewed lockfile are
|
||||||
|
# in the repository; the dependency tree and bundle are machine state.
|
||||||
|
/config/local/share/vicinae/extensions/*/node_modules/
|
||||||
|
/config/local/share/vicinae/extensions/*/dist/
|
||||||
|
/config/local/share/vicinae/extensions/*/build/
|
||||||
|
/config/local/share/vicinae/extensions/*/package-lock.json
|
||||||
|
!/config/local/share/vicinae/extensions/panama-search/package-lock.json
|
||||||
|
|||||||
@@ -1,2 +1,376 @@
|
|||||||
# Panama
|
# Panama
|
||||||
## Formerly Sunhat, this is a personal config for Fedora, with the intention of helping a user set up their Fedora system with one command.
|
|
||||||
|
Formerly Sunhat. A personal config for Fedora, with the intention of helping a
|
||||||
|
user set up their Fedora system with one checked command.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
(
|
||||||
|
set -euo pipefail
|
||||||
|
bootstrap_commit=0d1841cf86975d52485c94c0caa5d1b67e5c54a1
|
||||||
|
bootstrap_sha=fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
|
||||||
|
bootstrap_dir="$(umask 077; mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$bootstrap_dir"' EXIT
|
||||||
|
bootstrap="$bootstrap_dir/boot"
|
||||||
|
curl --fail --location --connect-timeout 10 --max-time 30 --max-filesize 262144 \
|
||||||
|
--output "$bootstrap" \
|
||||||
|
"https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot"
|
||||||
|
printf '%s %s\n' "$bootstrap_sha" "$bootstrap" >"$bootstrap_dir/boot.sha256"
|
||||||
|
sha256sum -c "$bootstrap_dir/boot.sha256"
|
||||||
|
PANAMA_BOOT_REVISION="$bootstrap_commit" PANAMA_BOOT_SHA256="$bootstrap_sha" \
|
||||||
|
bash "$bootstrap"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`boot` installs git if the machine lacks it, creates a verified checkout at
|
||||||
|
`~/.local/share/Panama` (or `$PANAMA_PATH`), and hands off to `install`. It is
|
||||||
|
deliberately small enough to read first. The command downloads one immutable
|
||||||
|
`boot` blob into a private temporary directory, checks its SHA-256, then asks
|
||||||
|
that verified script to fetch the same commit. A clean existing checkout may
|
||||||
|
fast-forward to that exact revision. A dirty or divergent checkout, a failed
|
||||||
|
fetch, or a revision mismatch stops before `install`; Panama never resets local
|
||||||
|
work or falls back to whatever checkout happens to be present.
|
||||||
|
|
||||||
|
Once a machine exists, the command that keeps it current is `panama update` —
|
||||||
|
one command, and it never asks you anything:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
panama update
|
||||||
|
```
|
||||||
|
|
||||||
|
It pulls, applies any repairs this machine has not had, and runs the stages
|
||||||
|
below that need no answers. `./install` remains what it is: how a machine is
|
||||||
|
built, and how you change an answer you gave.
|
||||||
|
|
||||||
|
The first question the interview asks is what the machine is: a **desktop**
|
||||||
|
gets everything below; a **server** — a VPS, a headless box — gets the same
|
||||||
|
shell environment minus everything that needs a screen, plus rootless podman
|
||||||
|
and the compose services in [`server/`](server/README.md). The answer is the
|
||||||
|
one thing Panama records durably (`~/.local/state/panama/role`), because
|
||||||
|
`panama update` asks nothing and still has to know. A fresh server skips the
|
||||||
|
question entirely. It uses the same immutable boot blob and pins as the desktop
|
||||||
|
command; only the final `--server` argument differs:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
(
|
||||||
|
set -euo pipefail
|
||||||
|
bootstrap_commit=0d1841cf86975d52485c94c0caa5d1b67e5c54a1
|
||||||
|
bootstrap_sha=fd8dcd70d1266da109972df346e3d85d1037f6f6e1942b0157064412ab892372
|
||||||
|
bootstrap_dir="$(umask 077; mktemp -d)"
|
||||||
|
trap 'rm -rf -- "$bootstrap_dir"' EXIT
|
||||||
|
bootstrap="$bootstrap_dir/boot"
|
||||||
|
curl --fail --location --connect-timeout 10 --max-time 30 --max-filesize 262144 \
|
||||||
|
--output "$bootstrap" \
|
||||||
|
"https://git.gbrown.org/gib/Panama/raw/commit/$bootstrap_commit/boot"
|
||||||
|
printf '%s %s\n' "$bootstrap_sha" "$bootstrap" >"$bootstrap_dir/boot.sha256"
|
||||||
|
sha256sum -c "$bootstrap_dir/boot.sha256"
|
||||||
|
PANAMA_BOOT_REVISION="$bootstrap_commit" PANAMA_BOOT_SHA256="$bootstrap_sha" \
|
||||||
|
bash "$bootstrap" --server
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
That command also works from a brand-new VPS's **root** login. It creates or
|
||||||
|
reuses your sudo-enabled user, then copies a safe root key when it can or
|
||||||
|
verifies the target key before offering SSH hardening. A verified target key
|
||||||
|
means the target user owns `.ssh` with mode `0700` and `authorized_keys` with
|
||||||
|
mode `0600`. Every non-comment line in `authorized_keys` must be valid OpenSSH
|
||||||
|
key material that `ssh-keygen` can parse. Root-key destination writes run as
|
||||||
|
the target user, and Panama rechecks the resulting owner, modes, and keys. SSH
|
||||||
|
hardening is unavailable without a verified target key or installed SSH unit,
|
||||||
|
and the install continues without it. Declining hardening also leaves SSH
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
When you accept hardening, Panama uses an atomic same-directory drop-in named
|
||||||
|
`00-panama.conf`, whose early filename gives it safer precedence. A pre-existing
|
||||||
|
symlink or non-regular object makes hardening unavailable. The effective policy
|
||||||
|
is exactly `PermitRootLogin no`, `PasswordAuthentication no`, and
|
||||||
|
`KbdInteractiveAuthentication no`. Panama validates syntax with `sshd -t` and
|
||||||
|
checks `sshd -T -C` for both root and target-user contexts before it reloads
|
||||||
|
the detected SSH unit. If validation or reload fails, it restores the previous
|
||||||
|
drop-in regular file with its metadata and validates and reloads that restored
|
||||||
|
configuration. Recovery that cannot complete stops the handoff and prints the
|
||||||
|
right manual command for either a prior file or no prior file. The fixture
|
||||||
|
contracts test these branches. No real daemon reload runs under
|
||||||
|
`panama test --safe`, so that suite is not live-host proof.
|
||||||
|
|
||||||
|
After that, it hands off to a normal install as the new user.
|
||||||
|
|
||||||
|
`install` asks its questions first and then runs the stages in `setup/scripts/`
|
||||||
|
in order, without stopping again:
|
||||||
|
|
||||||
|
| Script | Does |
|
||||||
|
|---|---|
|
||||||
|
| `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 |
|
||||||
|
| `link-skills` | Links the agent skills in `skills/` into `~/.claude/skills`, one per skill. Every machine gets these; personal ones link after and win a name clash |
|
||||||
|
| `link-user` | Links the personal content in `user/` — agent instructions, SSH host aliases — but only on a machine that answered yes. See [user/README.md](user/README.md) |
|
||||||
|
| `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 |
|
||||||
|
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81), the `nginx-bridge` network, and the nightly image-update timer |
|
||||||
|
| `link-server` | Server role only: creates `~/Server` and refreshes the links of whatever services this machine has enabled |
|
||||||
|
|
||||||
|
A server runs the shared stages plus its own two, and skips the rest —
|
||||||
|
`link-skills`, `change-settings`, `link-vicinae-scripts` and
|
||||||
|
`install-hardware` are desktop concerns. `link-dotfiles` links only the
|
||||||
|
universal dot dirs there (bash, nvim, tmux, btop and tmux theming, the hook
|
||||||
|
samples), and `install-packages` takes a short path: the core tools, nvm,
|
||||||
|
Bun, Claude Code and Codex — no third-party repos, no desktop, no flatpaks.
|
||||||
|
|
||||||
|
Then `panama migrate` applies any repairs this machine has not had yet. That is
|
||||||
|
the half of an upgrade installing cannot do: the stages above only ever add, so
|
||||||
|
nothing there can remove a file this repository stopped shipping or repair a
|
||||||
|
symlink that now points nowhere. A fresh machine is marked as already caught up
|
||||||
|
rather than having those repairs run against it, and a machine with work waiting
|
||||||
|
is told at the next login rather than left to find out.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## The desktop
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
GNOME is not gone from the machine: `gnome-control-center` is a declared
|
||||||
|
dependency, and two rows in Panama's own Settings still open it. Adding an
|
||||||
|
online account goes through the provider's dialog, because the OAuth sign-in
|
||||||
|
runs inside a library Fedora ships without a binding anything else can call.
|
||||||
|
And Digital wellbeing — screen time and break reminders — is the one panel of
|
||||||
|
GNOME's that still does something Panama does not. Everything else it used to
|
||||||
|
hand over is a page here now, and
|
||||||
|
[`tests/quickshell/gnome-handoff-contract`](tests/quickshell/gnome-handoff-contract)
|
||||||
|
fails the build if a door back opens onto a panel Panama owns, because those
|
||||||
|
rows kept working perfectly long after they stopped being true.
|
||||||
|
|
||||||
|
Displays is one of the panels it owns, and the only one where a wrong answer
|
||||||
|
can leave you unable to see well enough to undo it. So every change there is
|
||||||
|
applied as one complete layout, read back from the compositor, and reverted
|
||||||
|
after fifteen seconds unless you keep it — resolution, scale, rotation,
|
||||||
|
position and primary display, and now color profile, bit depth, SDR
|
||||||
|
brightness and saturation, and mirroring with them. Two of those opt out of
|
||||||
|
part of that, for reasons rather than convenience. A per-display variable
|
||||||
|
refresh rate override is applied but never verified, because `hyprctl` reports
|
||||||
|
whether adaptive sync is live this instant rather than what was asked for. And
|
||||||
|
a mirrored display's position is not asserted at all: the compositor stacks it
|
||||||
|
on the display it mirrors and ignores the coordinates the rule carried, so
|
||||||
|
holding it to them would make Keep permanently unavailable. Monitor brightness
|
||||||
|
sits outside the transaction entirely — it is the panel's own backlight over
|
||||||
|
DDC, and the buttons on the bezel change it behind our back.
|
||||||
|
|
||||||
|
| Piece | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README |
|
||||||
|
| `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI |
|
||||||
|
| `config/containers/` | Container definitions systemd runs as units — currently the speech-to-text server behind dictation |
|
||||||
|
| `config/dot/vicinae/` | Raycast-style launcher, themed. Its commands live in `config/local/share/vicinae/` — script commands (settings deep links, power actions, projects, reminders, window switcher, kill process, SSH hosts, recent files, color picker), and one compiled extension that adds web search with live suggestions. File search, calculator, clipboard, and emoji are Vicinae's own |
|
||||||
|
| `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) |
|
||||||
|
| `config/dot/wofi/` | Fallback launcher, in case the shell fails to start |
|
||||||
|
| `config/dot/xdg-desktop-portal/` | Portal backend routing |
|
||||||
|
|
||||||
|
New machines get a welcome card on first start, teaching the handful of keys
|
||||||
|
that matter and reachable afterwards from the launcher. `SUPER + /` shows every
|
||||||
|
shortcut the machine has, read from the live keymap so a rebind appears there
|
||||||
|
without anything being kept in sync. Rebinding is Settings' job, and so is
|
||||||
|
adding a shortcut of your own or assigning the four-finger touchpad swipes: an
|
||||||
|
entry names an application, a shell action or a window move rather than
|
||||||
|
carrying a command, so a hand-edited settings file can move a key but cannot
|
||||||
|
make one run something arbitrary. Settings carries a
|
||||||
|
[manual](config/dot/quickshell/manual/) written for the person using the
|
||||||
|
desktop rather than the person building it, opening with a chapter for people
|
||||||
|
arriving from GNOME, macOS or Windows.
|
||||||
|
|
||||||
|
Laptops work: battery, charge limits, idle timings that differ on battery, and
|
||||||
|
a closed lid that suspends when you are leaving and keeps working when you are
|
||||||
|
docked. A desktop sees none of it, because every one of those surfaces hides
|
||||||
|
when the hardware is absent.
|
||||||
|
|
||||||
|
**Start here: [`config/dot/hypr/README.md`](config/dot/hypr/README.md)** — it
|
||||||
|
covers the Lua migration, the uwsm environment gotcha, the HDR decision, the
|
||||||
|
full keymap, and troubleshooting.
|
||||||
|
|
||||||
|
Log in as **"Hyprland (uwsm-managed)"**, not plain "Hyprland".
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
bin/ Small user-facing commands on PATH; `panama` is the entry point
|
||||||
|
migrations/ One repair per file, for machines installed before a change
|
||||||
|
config/
|
||||||
|
bash/ .bashrc, aliases, env (env is gitignored)
|
||||||
|
copy/ Files copied verbatim over / (needs sudo)
|
||||||
|
dot/ Symlinked into ~/.config; quickshell/manual/ is the manual
|
||||||
|
Settings renders
|
||||||
|
firefox/ Vendored Firefox chrome, linked into the browser profile
|
||||||
|
containers/ Quadlets, linked into ~/.config/containers/systemd
|
||||||
|
local/ Icons, the cursor theme, and the launcher's commands and
|
||||||
|
extensions, linked into ~/.local/share
|
||||||
|
old/ Backups of whatever was replaced (gitignored)
|
||||||
|
wallpapers/ Copied into ~/Pictures/Wallpapers when absent
|
||||||
|
server/ The server role: compose services (one directory per
|
||||||
|
service), the nightly image updater, and its units. See
|
||||||
|
server/README.md
|
||||||
|
skills/ Agent skills for operating this desktop, linked into
|
||||||
|
~/.claude/skills
|
||||||
|
setup/
|
||||||
|
apps/ Applications built from source, one file each
|
||||||
|
lib/ Shared by more than one stage; the extras catalog reader
|
||||||
|
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
|
||||||
|
|
||||||
|
188 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||||
|
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||||||
|
specific external capability when automation needs it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
panama test --safe
|
||||||
|
panama test --allow live-host updates
|
||||||
|
panama test --allow live-compositor keybinds
|
||||||
|
PANAMA_TEST_TIMEOUT_SECONDS=300 panama test --safe
|
||||||
|
tests/setup/interview-contract # or one directly; they are plain executables
|
||||||
|
```
|
||||||
|
|
||||||
|
`--safe` means hermetic contracts only. A plain full run prompts in a terminal
|
||||||
|
before it starts any non-hermetic contract. Automation cannot answer that
|
||||||
|
prompt, so it must grant every required capability with a repeatable `--allow`.
|
||||||
|
Each contract has a 180-second outer timeout by default. Set
|
||||||
|
`PANAMA_TEST_TIMEOUT_SECONDS` to a positive integer to override it. When a
|
||||||
|
contract fails, the runner prints its captured stdout and stderr. Successful
|
||||||
|
stdout stays quiet. Successful stderr is surfaced as a warning.
|
||||||
|
|
||||||
|
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
|
||||||
|
tests/server/ The server role: the service catalog's shape, the secrets
|
||||||
|
rule that keeps a public repo safe, and panama-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
A project is the set of windows you open together — which applications, which
|
||||||
|
workspace each was on, and for a terminal, which directory it was sitting in.
|
||||||
|
Arrange the desktop, then run **Save Layout as Project** from the launcher and
|
||||||
|
name it; **Open Project** lays it out again.
|
||||||
|
|
||||||
|
Workspaces are recorded as positions rather than numbers, and opening a project
|
||||||
|
claims free ones, so it never lands on top of what you are already doing. An
|
||||||
|
application that refuses to open twice — Slack, Thunderbird, the browser — is
|
||||||
|
moved into place rather than launched again. Saved layouts are listed on
|
||||||
|
Shell › Workspaces in Settings, which is also where they are removed.
|
||||||
|
|
||||||
|
## The `panama` command
|
||||||
|
|
||||||
|
```sh
|
||||||
|
panama update # bring this machine up to date; asks nothing
|
||||||
|
panama sync # review, commit and push your changes to this repo
|
||||||
|
panama edit # open it in Neovim
|
||||||
|
panama doctor # what is actually running, not what was installed
|
||||||
|
panama diagnose # hand the health summary and recent errors to your agent
|
||||||
|
panama test # every contract, prompting before non-hermetic work
|
||||||
|
panama test --safe # hermetic contracts only
|
||||||
|
panama test --allow live-host updates # grant one capability to automation
|
||||||
|
panama contracts <file> # which contracts mention a file, and can they be run
|
||||||
|
panama migrate # apply repairs this machine has not had yet
|
||||||
|
panama upgrade # re-run ./install from anywhere, interview and all
|
||||||
|
panama apps # choose applications to install, by category
|
||||||
|
panama app # applications no repository carries; build one by name
|
||||||
|
panama server # the services a server runs: list, enable, disable, status
|
||||||
|
```
|
||||||
|
|
||||||
|
`panama update` and `panama sync` are separate verbs on purpose. One acts on
|
||||||
|
the machine, the other on the repository. A single command that chose between
|
||||||
|
them by checking whether the working tree happened to be dirty would do a
|
||||||
|
different job depending on state nobody can see — and, worse, would never
|
||||||
|
update a machine belonging to somebody who had left a file edited.
|
||||||
|
|
||||||
|
`panama update` stashes uncommitted work across the pull and restores it
|
||||||
|
afterwards. If restoring conflicts it resets the checkout and leaves the work
|
||||||
|
in the stash, saying so at the end of the run: every dotfile here is a symlink
|
||||||
|
into this repository, so a conflict marker is not a thing to fix at leisure. It
|
||||||
|
is live in `~/.config` the moment it is written.
|
||||||
|
|
||||||
|
Only two things still need `./install`: a machine that does not exist yet, and
|
||||||
|
an answer you want to change. Adding a package to a list you already have is
|
||||||
|
`panama update`; adding an optional category is `panama apps`.
|
||||||
|
|
||||||
|
`panama apps` is the optional-application catalog, opened after the fact. The
|
||||||
|
interview offers the same categories during `./install`, whole; this picks a
|
||||||
|
category and then the applications inside it, so a machine can acquire Slack in
|
||||||
|
March without having wanted Discord in January. Both read
|
||||||
|
`setup/lib/extras-catalog`, so the two cannot describe different catalogues.
|
||||||
|
|
||||||
|
A category is one file under `setup/packages/extras/`. A bare line is a dnf
|
||||||
|
package, a `flatpak:` line is a Flathub id, `| Name` gives the menu something
|
||||||
|
readable, and an indented line belongs to the entry above it — which is how OBS
|
||||||
|
carries its sixteen plugin extensions as one thing to tick.
|
||||||
|
|
||||||
|
Hooks are the extension point: drop a script at `~/.config/panama/hooks/theme-set`
|
||||||
|
and it runs whenever the color scheme changes, with the scheme and accent as
|
||||||
|
arguments. Same for `post-upgrade` and `post-migrate`, and a `<name>.d/`
|
||||||
|
directory beside each so several things can react without fighting over one
|
||||||
|
file. A broken hook is reported and stepped over, never fatal. Samples are
|
||||||
|
copied into place on install.
|
||||||
|
|
||||||
|
`panama migrate` applies repairs an installed machine has not had yet. Safe to
|
||||||
|
re-run: nothing is applied twice, and a machine with nothing waiting says so.
|
||||||
|
|
||||||
|
`panama-sudo` is pkexec with a stated reason: `panama-sudo --reason "why" --
|
||||||
|
command` shows the reason on Panama's password prompt, clearly labeled as an
|
||||||
|
unverified claim beside polkitd's own action text — meant for agents and
|
||||||
|
scripts, so the person typing the password learns why before they do. Without
|
||||||
|
a reason, a running shell, or `qs` it behaves exactly like pkexec.
|
||||||
|
|
||||||
|
`panama app` is deliberately not part of `./install`. Everything else Panama
|
||||||
|
installs comes from dnf or Flathub; these are built from source because no
|
||||||
|
packaged form exists, and a source build is slow, wants the network throughout,
|
||||||
|
and depends on an upstream that moves. That is the failure the interview exists
|
||||||
|
to prevent, so asking for one is something you do on purpose — and it is also
|
||||||
|
how you rebuild when a new version ships. Nothing is pinned: each build takes
|
||||||
|
the current upstream and reports a failure rather than working around it.
|
||||||
|
|
||||||
|
Adding one is adding a file to `setup/apps/`, and the file has to say why the
|
||||||
|
exception exists.
|
||||||
|
|
||||||
|
No script in this repository carries a `.sh` extension, with one deliberate
|
||||||
|
exception: each repair in `migrations/` is named `<timestamp>.sh`, because
|
||||||
|
`panama-migrate` finds them by globbing exactly that. Everywhere else 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.
|
||||||
|
|||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import pyperclip
|
||||||
|
import questionary
|
||||||
|
|
||||||
|
# List of directories to exclude
|
||||||
|
EXCLUDED_DIRS = {'node_modules', '.next', '.venv', '.git', '__pycache__', '.idea', '.vscode', 'ui'}
|
||||||
|
|
||||||
|
def collect_files(project_path):
|
||||||
|
"""
|
||||||
|
Collects files from the project directory, excluding specified directories and filtering by extensions.
|
||||||
|
Returns a list of file paths relative to the project directory.
|
||||||
|
"""
|
||||||
|
collected_files = []
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(project_path):
|
||||||
|
# Exclude specified directories
|
||||||
|
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
file_path = Path(root) / file
|
||||||
|
relative_path = file_path.relative_to(project_path)
|
||||||
|
collected_files.append(relative_path)
|
||||||
|
|
||||||
|
return collected_files
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Parse command-line arguments
|
||||||
|
parser = argparse.ArgumentParser(description='Generate Markdown from selected files.')
|
||||||
|
parser.add_argument('path', nargs='?', default='.', help='Path to the project directory')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
project_path = Path(args.path).resolve()
|
||||||
|
if not project_path.is_dir():
|
||||||
|
print(f"Error: '{project_path}' is not a directory.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Collect files from the project directory
|
||||||
|
file_list = collect_files(project_path)
|
||||||
|
|
||||||
|
if not file_list:
|
||||||
|
print("No files found in the project directory with the specified extensions.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Sort file_list for better organization
|
||||||
|
file_list.sort()
|
||||||
|
|
||||||
|
# Interactive file selection using questionary
|
||||||
|
print("\nSelect the files you want to include:")
|
||||||
|
selected_files = questionary.checkbox(
|
||||||
|
"Press space to select files, and Enter when you're done:",
|
||||||
|
choices=[str(f) for f in file_list]
|
||||||
|
).ask()
|
||||||
|
|
||||||
|
if not selected_files:
|
||||||
|
print("No files selected.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Generate markdown
|
||||||
|
markdown_lines = []
|
||||||
|
markdown_lines.append('')
|
||||||
|
|
||||||
|
for selected_file in selected_files:
|
||||||
|
file_path = project_path / selected_file
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
# Determine the language for code block from file extension
|
||||||
|
language = file_path.suffix.lstrip('.')
|
||||||
|
markdown_lines.append(f'{selected_file}')
|
||||||
|
markdown_lines.append(f'```{language}')
|
||||||
|
markdown_lines.append(content)
|
||||||
|
markdown_lines.append('```')
|
||||||
|
markdown_lines.append('')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading file {selected_file}: {e}")
|
||||||
|
|
||||||
|
markdown_text = '\n'.join(markdown_lines)
|
||||||
|
|
||||||
|
# Copy markdown content to clipboard
|
||||||
|
pyperclip.copy(markdown_text)
|
||||||
|
print("Markdown content has been copied to the clipboard.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Check if required libraries are installed
|
||||||
|
try:
|
||||||
|
import questionary
|
||||||
|
import pyperclip
|
||||||
|
except ImportError as e:
|
||||||
|
missing_module = e.name
|
||||||
|
print(f"Error: Missing required module '{missing_module}'.")
|
||||||
|
print(f"Please install it by running: pip install {missing_module}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -152,11 +152,14 @@ case "$cmd" in
|
|||||||
|
|
||||||
# 2.4) Run flatpak updates (user then system)
|
# 2.4) Run flatpak updates (user then system)
|
||||||
flatpak update -y
|
flatpak update -y
|
||||||
sudo flatpak update
|
sudo flatpak update -y
|
||||||
|
|
||||||
# 2.5) Optional firmware via fwupd
|
# 2.5) Optional firmware via fwupd
|
||||||
if $firmware; then
|
if $firmware; then
|
||||||
sudo fwupdmgr refresh
|
# fwupdmgr exits non-zero when metadata is already current -- that is
|
||||||
|
# not an error, but under 'set -e' it would abort the script before
|
||||||
|
# 'fwupdmgr update' ever runs.
|
||||||
|
sudo fwupdmgr refresh || true
|
||||||
sudo fwupdmgr update
|
sudo fwupdmgr update
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Executable
+1170
File diff suppressed because it is too large
Load Diff
Executable
+195
@@ -0,0 +1,195 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Hand a prompt to whichever coding agent this machine has chosen.
|
||||||
|
#
|
||||||
|
# Every rung of the escalation ladder ends here: a crash toast, a failed shell
|
||||||
|
# reload, a red health check, `panama diagnose`. They gather facts; this decides
|
||||||
|
# which binary runs them and puts it in a terminal you can watch and interrupt.
|
||||||
|
#
|
||||||
|
# Two settings decide everything, and both are read at press time rather than at
|
||||||
|
# start time, so choosing an agent in Settings takes effect on the next crash
|
||||||
|
# without restarting anything:
|
||||||
|
#
|
||||||
|
# preferredAgent none | claude | codex ("none" is the default: silence)
|
||||||
|
# agentAutoApprove true -> the agent starts in its own "don't stop to ask"
|
||||||
|
# mode; false -> its normal prompting mode, untouched.
|
||||||
|
#
|
||||||
|
# "none" exits 0 without a word. It is not an error to have no agent; it is the
|
||||||
|
# shipped state, and a rung that shouted about it would be a rung that gets
|
||||||
|
# turned off.
|
||||||
|
#
|
||||||
|
# panama-agent open the agent on the repo
|
||||||
|
# panama-agent --prompt "text" open it with something to work on
|
||||||
|
#
|
||||||
|
# Environment seams, for the contract and for a second checkout:
|
||||||
|
#
|
||||||
|
# PANAMA_PATH the repository; also the agent's working directory
|
||||||
|
# PANAMA_AGENT_SETTINGS the settings file to read (default: the real one)
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Adapted from Omarchy's bin/omarchy-agent (https://github.com/basecamp/omarchy)
|
||||||
|
#
|
||||||
|
# Copyright (c) David Heinemeier Hansson
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
||||||
|
SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"
|
||||||
|
|
||||||
|
# A fixed window class rather than the per-binary default, so one Hyprland rule
|
||||||
|
# can catch every agent window regardless of which agent is chosen.
|
||||||
|
readonly WINDOW_CLASS="panama-agent"
|
||||||
|
|
||||||
|
# THE INHERITED PATH IS NOT THE USER'S PATH. Every rung except `panama diagnose`
|
||||||
|
# reaches this script from the Quickshell shell, which systemd starts with
|
||||||
|
# neither PANAMA_PATH nor ~/.local/bin -- and ~/.local/bin is where both agents
|
||||||
|
# install themselves. Trusting PATH here meant a perfectly well installed agent
|
||||||
|
# reporting itself as missing, into the stderr of a detached process nobody will
|
||||||
|
# ever read: the whole ladder failing silently, which is the exact failure it
|
||||||
|
# exists to prevent.
|
||||||
|
#
|
||||||
|
# So the binary is resolved rather than named. PATH first, because a user who
|
||||||
|
# put an agent somewhere else meant it; then the XDG user bin directory, which
|
||||||
|
# is where the installers actually put them.
|
||||||
|
resolve_agent() {
|
||||||
|
local name="$1" found
|
||||||
|
found="$(command -v "$name" 2>/dev/null)" && { printf '%s' "$found"; return 0; }
|
||||||
|
[[ -x "$HOME/.local/bin/$name" ]] && { printf '%s' "$HOME/.local/bin/$name"; return 0; }
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The same repair, for the agent's own sake rather than this script's: an agent
|
||||||
|
# launched from a notification click would otherwise run every shell command it
|
||||||
|
# is asked to with a PATH unlike the one the user gets in a terminal. Applied
|
||||||
|
# just before the spawn rather than here, so resolve_agent above is answering
|
||||||
|
# the question the caller actually asked -- "can this be found from where I was
|
||||||
|
# started" -- instead of one this script has already fixed for itself.
|
||||||
|
repair_path() {
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$HOME/.local/bin:"*) ;;
|
||||||
|
*) PATH="$PATH:$HOME/.local/bin" ;;
|
||||||
|
esac
|
||||||
|
export PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: panama-agent [--prompt "text"]
|
||||||
|
|
||||||
|
Opens the agent named by preferredAgent in a terminal, in the Panama checkout.
|
||||||
|
With no agent chosen, exits silently: choose one on Settings > System > Agents.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt=""
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--prompt)
|
||||||
|
prompt="${2:?--prompt needs a value}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'panama-agent: unexpected argument: %s\n' "$1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Same shape as panama-idle's reader: a missing file, a missing key and an
|
||||||
|
# explicit null all mean "the default", because all three describe a machine
|
||||||
|
# that has never been asked the question.
|
||||||
|
read_setting() {
|
||||||
|
local key="$1" fallback="$2"
|
||||||
|
[[ -r "$SETTINGS" ]] || { printf '%s' "$fallback"; return; }
|
||||||
|
command -v jq >/dev/null 2>&1 || { printf '%s' "$fallback"; return; }
|
||||||
|
jq -r --arg k "$key" --arg d "$fallback" \
|
||||||
|
'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \
|
||||||
|
"$SETTINGS" 2>/dev/null || printf '%s' "$fallback"
|
||||||
|
}
|
||||||
|
|
||||||
|
agent="$(read_setting preferredAgent none)"
|
||||||
|
|
||||||
|
# The shipped state. Nothing to launch, nothing to say.
|
||||||
|
[[ -n "$agent" && "$agent" != "none" ]] || exit 0
|
||||||
|
|
||||||
|
auto_approve="$(read_setting agentAutoApprove true)"
|
||||||
|
|
||||||
|
case "$agent" in
|
||||||
|
claude | codex) ;;
|
||||||
|
*)
|
||||||
|
printf 'panama-agent: unsupported preferredAgent: %s\n' "$agent" >&2
|
||||||
|
printf 'Choose one on Settings > System > Agents.\n' >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Resolved to a path before argv is built, so kitty is never asked to repeat a
|
||||||
|
# PATH lookup this script has already done more carefully than kitty could.
|
||||||
|
if ! agent_bin="$(resolve_agent "$agent")"; then
|
||||||
|
printf 'panama-agent: %s is not installed.\n' "$agent" >&2
|
||||||
|
printf 'Looked on PATH and in %s.\n' "$HOME/.local/bin" >&2
|
||||||
|
printf 'Install it, or choose another agent on Settings > System > Agents.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The per-agent launch table. VERIFIED against the installed binaries' --help on
|
||||||
|
# 2026-08-25 (claude 2.1.245, codex-cli 0.149.1) -- these flags are not the same
|
||||||
|
# from release to release, so re-run --help before changing them.
|
||||||
|
#
|
||||||
|
# claude --permission-mode auto "auto" is one of acceptEdits/auto/
|
||||||
|
# bypassPermissions/manual/dontAsk/plan
|
||||||
|
# codex --approve-for-me routes approvals through automatic review
|
||||||
|
# inside the workspace-write sandbox
|
||||||
|
#
|
||||||
|
# With agentAutoApprove off, no mode flag is passed at all: the agent's own
|
||||||
|
# configured default is a choice the user already made, and overriding it with
|
||||||
|
# an explicit "prompt me" would be this script having an opinion it was told not
|
||||||
|
# to have.
|
||||||
|
declare -a argv=("$agent_bin")
|
||||||
|
case "$agent" in
|
||||||
|
claude) [[ "$auto_approve" == "true" ]] && argv+=(--permission-mode auto) ;;
|
||||||
|
codex) [[ "$auto_approve" == "true" ]] && argv+=(--approve-for-me) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# One argv element, after the option terminator. Both CLIs take the prompt as a
|
||||||
|
# trailing positional, and `--` is what stops a prompt beginning with a dash --
|
||||||
|
# or one that happens to read like a subcommand -- from being parsed as flags.
|
||||||
|
[[ -n "$prompt" ]] && argv+=(-- "$prompt")
|
||||||
|
|
||||||
|
# The checkout, not $HOME: the skills the prompts point at, the repository the
|
||||||
|
# agent is being asked about, and .claude/settings.json's pre-approved read-only
|
||||||
|
# diagnostics all live here. An agent started anywhere else finds none of them.
|
||||||
|
cd "$PANAMA_PATH"
|
||||||
|
|
||||||
|
repair_path
|
||||||
|
|
||||||
|
# setsid so the agent outlives whatever spawned it -- a notification handler, a
|
||||||
|
# crash watcher, a terminal that is about to close.
|
||||||
|
exec setsid kitty \
|
||||||
|
--directory "$PANAMA_PATH" \
|
||||||
|
--class "$WINDOW_CLASS" \
|
||||||
|
-e "${argv[@]}"
|
||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# "Something crashed" -> an agent already reading the core dump.
|
||||||
|
#
|
||||||
|
# Reached by clicking the crash notification panama-crash-watch sends, or run by
|
||||||
|
# hand against any PID in `coredumpctl list`. It gathers the four facts
|
||||||
|
# systemd-coredump recorded and points at the skill that says what to do with
|
||||||
|
# them; the method lives in the skill so it is edited in one place and works
|
||||||
|
# whichever agent is configured.
|
||||||
|
#
|
||||||
|
# panama-agent-crash <pid> [comm] [exe] [signal]
|
||||||
|
#
|
||||||
|
# The skill is named AND given as an absolute path. A harness with a skill
|
||||||
|
# mechanism follows the name; one without still has a file to read. That is the
|
||||||
|
# whole reason this ladder works for more than one agent.
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Adapted from Omarchy's bin/omarchy-agent-crash
|
||||||
|
# (https://github.com/basecamp/omarchy)
|
||||||
|
#
|
||||||
|
# Copyright (c) David Heinemeier Hansson
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
||||||
|
|
||||||
|
pid="${1:-}"
|
||||||
|
if [[ ! "$pid" =~ ^[0-9]+$ ]]; then
|
||||||
|
printf 'Not a PID: %s\n' "${pid:-<missing>}" >&2
|
||||||
|
printf 'Usage: panama-agent-crash <pid> [comm] [exe] [signal] (see: coredumpctl list)\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
comm="${2:-unknown}"
|
||||||
|
exe="${3:-unknown}"
|
||||||
|
signal="${4:-unknown}"
|
||||||
|
|
||||||
|
skill="$PANAMA_PATH/skills/diagnose-crash/SKILL.md"
|
||||||
|
|
||||||
|
# Looked up live so a PID typed by hand still gets a timestamp. A core that has
|
||||||
|
# already been rotated away costs only the timestamp, so this is allowed to
|
||||||
|
# fail: the other four facts are enough to start on.
|
||||||
|
when="$(coredumpctl list "$pid" --no-pager --no-legend 2>/dev/null | tail -1 | cut -d' ' -f1-4)" || true
|
||||||
|
when="${when:-unknown}"
|
||||||
|
[[ -n "${when// }" ]] || when="unknown"
|
||||||
|
|
||||||
|
prompt="$(
|
||||||
|
cat <<PROMPT
|
||||||
|
A process crashed on this Panama machine and I want to know why.
|
||||||
|
|
||||||
|
What systemd-coredump recorded:
|
||||||
|
process: $comm
|
||||||
|
PID: $pid
|
||||||
|
binary: $exe
|
||||||
|
signal: $signal
|
||||||
|
time: $when
|
||||||
|
|
||||||
|
Use the diagnose-crash skill. It covers how to investigate, what to rule out
|
||||||
|
first, and what to report. If your harness has no skill mechanism, read the
|
||||||
|
skill file directly and follow it instead:
|
||||||
|
|
||||||
|
$skill
|
||||||
|
PROMPT
|
||||||
|
)"
|
||||||
|
|
||||||
|
exec "$PANAMA_PATH/bin/panama-agent" --prompt "$prompt"
|
||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# "The shell would not reload" -> an agent already holding the error.
|
||||||
|
#
|
||||||
|
# Quickshell keeps the old shell running when a reload fails, which is what
|
||||||
|
# makes this rung possible at all: the desktop that just refused the new code is
|
||||||
|
# still there to notify you about it, and still there to click. shell.qml's
|
||||||
|
# onReloadFailed sends that notification; this builds the prompt behind it.
|
||||||
|
#
|
||||||
|
# panama-agent-reload "<what Quickshell said>"
|
||||||
|
#
|
||||||
|
# The failure string on its own is usually one line naming one file. The journal
|
||||||
|
# around it is where the rest is -- the QML warnings that preceded the fatal
|
||||||
|
# one, the property that was already undefined two saves ago -- so both go in.
|
||||||
|
#
|
||||||
|
# Environment seams, for the contract:
|
||||||
|
#
|
||||||
|
# PANAMA_PATH the repository
|
||||||
|
# PANAMA_RELOAD_UNIT the unit to read (default panama-quickshell.service)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
||||||
|
UNIT="${PANAMA_RELOAD_UNIT:-panama-quickshell.service}"
|
||||||
|
|
||||||
|
summary="${1:-}"
|
||||||
|
if [[ -z "${summary// }" ]]; then
|
||||||
|
printf 'Usage: panama-agent-reload "<the reload failure>"\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read a generous window and filter it down, rather than asking journalctl for
|
||||||
|
# forty lines and hoping they were the relevant ones. A failed reload usually
|
||||||
|
# arrives after a burst of unrelated shell chatter.
|
||||||
|
#
|
||||||
|
# Each line is truncated because the prompt leaves as one argv element and the
|
||||||
|
# kernel caps that at 128KB; a single Quickshell backtrace can be most of it.
|
||||||
|
context=""
|
||||||
|
if command -v journalctl >/dev/null 2>&1; then
|
||||||
|
context="$(journalctl --user -u "$UNIT" -n 400 --no-pager --output=cat 2>/dev/null \
|
||||||
|
| grep -iE 'quickshell|\.qml|qml:|panama' \
|
||||||
|
| tail -40 \
|
||||||
|
| cut -c 1-300)" || true
|
||||||
|
fi
|
||||||
|
[[ -n "${context// }" ]] || context="(nothing in the journal for $UNIT)"
|
||||||
|
|
||||||
|
prompt="$(
|
||||||
|
cat <<PROMPT
|
||||||
|
The Panama shell refused to reload on this machine. The old shell is still
|
||||||
|
running, so the desktop is up, but the change that was just saved is not live.
|
||||||
|
|
||||||
|
What Quickshell reported:
|
||||||
|
|
||||||
|
$summary
|
||||||
|
|
||||||
|
The last relevant lines from $UNIT:
|
||||||
|
|
||||||
|
$context
|
||||||
|
|
||||||
|
The shell lives in config/dot/quickshell in this repository, symlinked into
|
||||||
|
~/.config/quickshell -- so the file that failed to parse is a tracked file here,
|
||||||
|
not a copy. Find what broke the reload and say what it is. Read before you
|
||||||
|
write: a bad guess saved into this tree is live in the desktop immediately.
|
||||||
|
PROMPT
|
||||||
|
)"
|
||||||
|
|
||||||
|
exec "$PANAMA_PATH/bin/panama-agent" --prompt "$prompt"
|
||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Tell somebody when a program crashes.
|
||||||
|
#
|
||||||
|
# On GNOME, ABRT says so. Under a hand-assembled Hyprland desktop nothing does,
|
||||||
|
# and applications die silently -- which is most of how "Linux is flaky" gets
|
||||||
|
# earned. Fedora ships systemd-coredump by default, so the information is
|
||||||
|
# already there; nobody is reading it.
|
||||||
|
#
|
||||||
|
# Follows the journal for systemd-coredump's own message id and reports each
|
||||||
|
# program once per session.
|
||||||
|
#
|
||||||
|
# ONCE PER SESSION IS THE WHOLE DESIGN. This machine's portal backend crashes
|
||||||
|
# between eleven and sixty times a day -- see the portal-stability check in
|
||||||
|
# panama-doctor -- and a notification per crash would be a notification every
|
||||||
|
# few minutes for something the user can do nothing about. The first one is
|
||||||
|
# news; the fortieth is why people turn notifications off. The health page
|
||||||
|
# carries the running count for anyone who wants it.
|
||||||
|
#
|
||||||
|
# When an agent has been chosen, the notification stops being a dead end. It
|
||||||
|
# carries the diagnosis command as data in a `panama-exec` hint, which the shell
|
||||||
|
# runs on click. Command-as-data rather than a libnotify action, because an
|
||||||
|
# action would tie the click to this process still being alive to hear it, and
|
||||||
|
# this process is a `journalctl -f` that outlives nothing in particular. The
|
||||||
|
# hint survives a shell restart and never blocks the watcher.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
SETTINGS="${PANAMA_AGENT_SETTINGS:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json}"
|
||||||
|
|
||||||
|
# systemd-coredump's MESSAGE_ID. Matching on this rather than on text keeps
|
||||||
|
# working when the wording changes and never matches a program that merely
|
||||||
|
# mentions the word "crash" in its own logs.
|
||||||
|
readonly COREDUMP_MESSAGE_ID='fc2e22bc6ee647b6b90729ab34a250b1'
|
||||||
|
|
||||||
|
command -v journalctl >/dev/null 2>&1 || exit 0
|
||||||
|
command -v notify-send >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
# The shell owns org.freedesktop.Notifications, and the crash most worth
|
||||||
|
# reporting is the one that took the shell with it. Waiting means that report
|
||||||
|
# arrives rather than vanishing into a bus nobody is serving.
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
busctl --user status org.freedesktop.Notifications >/dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Read per crash rather than once at startup, so choosing an agent in Settings
|
||||||
|
# takes effect on the next crash instead of on the next login. This service runs
|
||||||
|
# for the life of the session; nothing restarts it when a preference changes.
|
||||||
|
read_setting() {
|
||||||
|
local key="$1" fallback="$2"
|
||||||
|
[[ -r "$SETTINGS" ]] || { printf '%s' "$fallback"; return; }
|
||||||
|
command -v jq >/dev/null 2>&1 || { printf '%s' "$fallback"; return; }
|
||||||
|
jq -r --arg k "$key" --arg d "$fallback" \
|
||||||
|
'if has($k) and (.[$k] != null) then (.[$k] | tostring) else $d end' \
|
||||||
|
"$SETTINGS" 2>/dev/null || printf '%s' "$fallback"
|
||||||
|
}
|
||||||
|
|
||||||
|
# What to call the agent in a sentence aimed at a person.
|
||||||
|
agent_label() {
|
||||||
|
case "$1" in
|
||||||
|
claude) printf 'Claude Code' ;;
|
||||||
|
codex) printf 'Codex' ;;
|
||||||
|
*) printf '%s' "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -A reported=()
|
||||||
|
|
||||||
|
# -f from now, not from the boot: a session that starts after a crash should
|
||||||
|
# not open with a notification about something the user has already lived
|
||||||
|
# through and cannot act on.
|
||||||
|
journalctl --user -f -n 0 --output=json MESSAGE_ID="$COREDUMP_MESSAGE_ID" 2>/dev/null \
|
||||||
|
| while IFS= read -r line; do
|
||||||
|
[[ -n "$line" ]] || continue
|
||||||
|
|
||||||
|
# One jq per entry rather than one per field: the fields are read
|
||||||
|
# together, and the click payload needs all of them.
|
||||||
|
IFS=$'\t' read -r uid exe comm pid signal < <(
|
||||||
|
jq -r '[(.COREDUMP_UID // ""),
|
||||||
|
(.COREDUMP_EXE // ""),
|
||||||
|
(.COREDUMP_COMM // ""),
|
||||||
|
(.COREDUMP_PID // ""),
|
||||||
|
(.COREDUMP_SIGNAL_NAME // "")] | @tsv' <<<"$line" 2>/dev/null
|
||||||
|
)
|
||||||
|
|
||||||
|
# Another user's crash is not this session's business, and reporting it
|
||||||
|
# would leak what they are running.
|
||||||
|
[[ "$uid" == "$(id -u)" ]] || continue
|
||||||
|
[[ -n "$exe" || -n "$comm" ]] || continue
|
||||||
|
|
||||||
|
# The executable name first: COREDUMP_COMM is the kernel's comm field
|
||||||
|
# and is truncated to fifteen characters, so it reports
|
||||||
|
# "panama-test-cra" for a program called panama-test-crasher.
|
||||||
|
if [[ -n "$exe" ]]; then
|
||||||
|
program="$(basename "$exe")"
|
||||||
|
else
|
||||||
|
program="$comm"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Never announce our own machinery. A crash watcher that notifies about
|
||||||
|
# the crash watcher, or about the agent it just launched to investigate
|
||||||
|
# the last crash, is a loop with a toast in it.
|
||||||
|
[[ "$program" == panama-crash-* || "$program" == panama-agent* ]] && continue
|
||||||
|
|
||||||
|
[[ -z "${reported[$program]:-}" ]] || continue
|
||||||
|
reported[$program]=1
|
||||||
|
|
||||||
|
# The toast can only offer a diagnosis if there is something to diagnose
|
||||||
|
# with. No agent, or the offer switched off, and it stays exactly the
|
||||||
|
# actionless notification it has always been.
|
||||||
|
agent="$(read_setting preferredAgent none)"
|
||||||
|
offer="$(read_setting crashDiagnoseOffer true)"
|
||||||
|
|
||||||
|
if [[ -n "$agent" && "$agent" != "none" && "$offer" != "false" && "$pid" =~ ^[0-9]+$ ]]; then
|
||||||
|
# By absolute path, not by name. The shell runs this hint, and the
|
||||||
|
# shell is started by systemd -- whose environment does not carry
|
||||||
|
# the repository's bin directory on PATH, so a bare name would
|
||||||
|
# click into "command not found".
|
||||||
|
exec_command="$(printf '%q %q %q %q %q' \
|
||||||
|
"$PANAMA_PATH/bin/panama-agent-crash" \
|
||||||
|
"$pid" "${comm:-$program}" "${exe:-unknown}" "${signal:-unknown}")"
|
||||||
|
|
||||||
|
# The hint is data, not privilege. Any process on this session bus
|
||||||
|
# could send one, and running it grants nothing a local process
|
||||||
|
# could not already do for itself.
|
||||||
|
# Same urgency as the plain report, deliberately. Making the
|
||||||
|
# clickable one critical would let a crash break through Do Not
|
||||||
|
# Disturb, which is a louder desktop than anybody asked for in
|
||||||
|
# exchange for an offer that keeps until it is read anyway.
|
||||||
|
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
||||||
|
--hint="string:panama-exec:$exec_command" \
|
||||||
|
"$program stopped unexpectedly" \
|
||||||
|
"Click to diagnose with $(agent_label "$agent")." \
|
||||||
|
2>/dev/null || true
|
||||||
|
else
|
||||||
|
notify-send --icon=dialog-error-symbolic --app-name=Panama \
|
||||||
|
"$program stopped unexpectedly" \
|
||||||
|
"It crashed and was not able to recover. System Health has the details." \
|
||||||
|
2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Stamps a new migration, so the name is never chosen by hand.
|
||||||
|
#
|
||||||
|
# panama-dev-migration "remove the stale settings-ssh-keys.sh launcher command"
|
||||||
|
#
|
||||||
|
# The filename is the commit timestamp of HEAD, which makes glob order
|
||||||
|
# chronological without a sequence number that two branches could pick at the
|
||||||
|
# same time. Two migrations authored against the same commit would collide, so
|
||||||
|
# a taken name gets the next free second rather than silently overwriting.
|
||||||
|
#
|
||||||
|
# Developer tool, not part of any install path. See bin/panama-migrate for what
|
||||||
|
# runs these and what rules they have to follow.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
||||||
|
MIGRATIONS_DIR="$PANAMA_PATH/migrations"
|
||||||
|
|
||||||
|
describe="${1:-}"
|
||||||
|
if [[ -z "$describe" ]]; then
|
||||||
|
echo 'usage: panama-dev-migration "what this repairs"' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
stamp="$(git -C "$PANAMA_PATH" log -1 --format=%cd --date=unix 2>/dev/null || date +%s)"
|
||||||
|
mkdir -p "$MIGRATIONS_DIR"
|
||||||
|
while [[ -e "$MIGRATIONS_DIR/$stamp.sh" ]]; do
|
||||||
|
stamp=$(( stamp + 1 ))
|
||||||
|
done
|
||||||
|
file="$MIGRATIONS_DIR/$stamp.sh"
|
||||||
|
|
||||||
|
cat >"$file" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# $describe
|
||||||
|
#
|
||||||
|
# Rules, because the runner cannot enforce them:
|
||||||
|
#
|
||||||
|
# * Safe to run twice. The marker records success, not intent.
|
||||||
|
# * Tolerant of the repair already being correct -- the user may have fixed
|
||||||
|
# it by hand, or a later ./install may have put it back.
|
||||||
|
# * Root work goes through \`panama-sudo --reason "..."\`, never bare sudo,
|
||||||
|
# so the password prompt names the repair.
|
||||||
|
# * Exit non-zero to be retried at the next login. Exit zero only when the
|
||||||
|
# machine is genuinely in the state this describes.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="\${PANAMA_PATH:-\$HOME/.local/share/Panama}"
|
||||||
|
|
||||||
|
# ... the repair goes here.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x "$file"
|
||||||
|
printf 'Created %s\n' "$file"
|
||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# The pressure valve.
|
||||||
|
#
|
||||||
|
# panama-hook theme-set dark orchid
|
||||||
|
#
|
||||||
|
# Runs ~/.config/panama/hooks/<name> and everything executable in
|
||||||
|
# ~/.config/panama/hooks/<name>.d/, in sorted order, with the hook's arguments.
|
||||||
|
#
|
||||||
|
# This exists so "can Panama also do X when the theme changes" is a five-line
|
||||||
|
# file somebody drops in a directory rather than a fork, a feature request, or
|
||||||
|
# a patch that has to be rebased forever. docs/UPSTREAM-INSPIRATION.md defers a
|
||||||
|
# plugin host as premature and still should: this is the thirty-line version
|
||||||
|
# that covers most of what people actually want from one, and it has no API to
|
||||||
|
# keep stable beyond "we will run your script and tell you what happened".
|
||||||
|
#
|
||||||
|
# A failing hook is reported and stepped over. Somebody's broken script must
|
||||||
|
# never break a theme change, an upgrade, or a login -- which is exactly what
|
||||||
|
# would happen if this used `set -e` and the caller did too.
|
||||||
|
#
|
||||||
|
# Hooks run synchronously, so a slow one delays whatever called it. That is
|
||||||
|
# deliberate: the alternative is a hook whose output arrives after the thing it
|
||||||
|
# was reacting to has already finished, which is harder to reason about than a
|
||||||
|
# pause.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HOOK_DIR="${PANAMA_HOOK_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/panama/hooks}"
|
||||||
|
|
||||||
|
name="${1:-}"
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
echo 'usage: panama-hook <name> [args...]' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
# A hook name reaches the filesystem, so it cannot be allowed to leave the
|
||||||
|
# directory. Callers are all in-repo today, which is exactly when this is
|
||||||
|
# cheap to add and easy to forget.
|
||||||
|
if [[ ! "$name" =~ ^[a-z][a-z0-9-]*$ ]]; then
|
||||||
|
echo "panama-hook: refusing hook name: $name" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_one() {
|
||||||
|
local script="$1"
|
||||||
|
# Shifted off before the arguments are forwarded, or every hook receives
|
||||||
|
# its own path as $1 and the real arguments arrive one place late.
|
||||||
|
shift
|
||||||
|
[[ -f "$script" && -x "$script" ]] || return 0
|
||||||
|
if ! "$script" "$@"; then
|
||||||
|
printf 'panama-hook: %s failed (%s); continuing\n' \
|
||||||
|
"$(basename "$script")" "$name" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The single file first, then the .d directory in sorted order. Both are
|
||||||
|
# optional and having neither is the normal case.
|
||||||
|
run_one "$HOOK_DIR/$name" "$@"
|
||||||
|
|
||||||
|
if [[ -d "$HOOK_DIR/$name.d" ]]; then
|
||||||
|
while IFS= read -r script; do
|
||||||
|
[[ -n "$script" ]] || continue
|
||||||
|
run_one "$script" "$@"
|
||||||
|
done < <(find "$HOOK_DIR/$name.d" -maxdepth 1 -type f | sort)
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# What this machine is, asked one yes-or-no question at a time.
|
||||||
|
#
|
||||||
|
# panama-hw laptop && echo "portable"
|
||||||
|
# panama-hw clamshell && panama-lid close
|
||||||
|
#
|
||||||
|
# Every subcommand exits 0 for yes and 1 for no, prints nothing, and answers
|
||||||
|
# correctly on a machine that has none of the hardware in question. That last
|
||||||
|
# part is the whole point: a desktop must be able to ask "am I in clamshell
|
||||||
|
# mode" and get a calm no rather than an error, because the scripts and
|
||||||
|
# services that ask are shared between machines.
|
||||||
|
#
|
||||||
|
# `--json` answers everything at once, for the health page and for contracts.
|
||||||
|
#
|
||||||
|
# Detection reads sysfs directly rather than shelling out to lspci or upower:
|
||||||
|
# lspci touches PCI config space and wakes a runtime-suspended GPU, which is a
|
||||||
|
# real cost to pay for a question asked at every login.
|
||||||
|
#
|
||||||
|
# Paths are overridable (PANAMA_HW_SYS, PANAMA_HW_ACPI) so the contract can
|
||||||
|
# drive fixture trees. Nothing else should set them.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SYS="${PANAMA_HW_SYS:-/sys}"
|
||||||
|
ACPI="${PANAMA_HW_ACPI:-/proc/acpi}"
|
||||||
|
|
||||||
|
# SMBIOS chassis types that mean "carried around": Portable, Laptop, Notebook,
|
||||||
|
# Hand Held, Sub Notebook, Tablet, Convertible, Detachable. A machine that
|
||||||
|
# reports something else, or reports nothing, is treated as stationary --
|
||||||
|
# guessing "laptop" on an unknown chassis would put battery chrome on a desktop.
|
||||||
|
readonly PORTABLE_CHASSIS=" 8 9 10 11 14 30 31 32 "
|
||||||
|
|
||||||
|
is_laptop() {
|
||||||
|
local type_file="$SYS/class/dmi/id/chassis_type" chassis
|
||||||
|
[[ -r "$type_file" ]] || return 1
|
||||||
|
chassis="$(cat "$type_file" 2>/dev/null)" || return 1
|
||||||
|
[[ "$PORTABLE_CHASSIS" == *" $chassis "* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The first SYSTEM battery, or nothing. Named rather than assumed to be BAT0:
|
||||||
|
# the second battery in a ThinkPad is BAT1, and a machine with only BAT1
|
||||||
|
# exists. The scope check is what keeps a desktop a desktop: a wireless mouse
|
||||||
|
# or a game controller publishes type=Battery with scope=Device, and counting
|
||||||
|
# one turned a tower into a "laptop" whose battery readout was the mouse's.
|
||||||
|
battery_path() {
|
||||||
|
local supply type scope
|
||||||
|
for supply in "$SYS"/class/power_supply/*; do
|
||||||
|
[[ -r "$supply/type" ]] || continue
|
||||||
|
type="$(cat "$supply/type" 2>/dev/null)"
|
||||||
|
[[ "$type" == "Battery" ]] || continue
|
||||||
|
scope="$(cat "$supply/scope" 2>/dev/null || echo System)"
|
||||||
|
[[ "$scope" == "Device" ]] && continue
|
||||||
|
printf '%s\n' "$supply"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
has_battery() { battery_path >/dev/null; }
|
||||||
|
|
||||||
|
# On wall power. A machine with no mains supply at all and no system battery
|
||||||
|
# is a desktop, and a desktop is always on wall power -- answering "no" there
|
||||||
|
# would make every battery-aware timing apply to a machine that cannot run
|
||||||
|
# out of power. But "no Mains" alone is not "desktop": hardware charged only
|
||||||
|
# over USB-PD exposes type=USB supplies and no Mains at all, and reading that
|
||||||
|
# as permanently-on-AC meant its battery timings never engaged while it ran
|
||||||
|
# down. When no Mains exists but a system battery does, the battery's own
|
||||||
|
# status is the answer: Discharging means battery, everything else means fed.
|
||||||
|
on_ac() {
|
||||||
|
local supply type online found=1 battery status
|
||||||
|
for supply in "$SYS"/class/power_supply/*; do
|
||||||
|
[[ -r "$supply/type" ]] || continue
|
||||||
|
type="$(cat "$supply/type" 2>/dev/null)"
|
||||||
|
[[ "$type" == "Mains" ]] || continue
|
||||||
|
found=0
|
||||||
|
online="$(cat "$supply/online" 2>/dev/null || echo 0)"
|
||||||
|
[[ "$online" == "1" ]] && return 0
|
||||||
|
done
|
||||||
|
# Mains exists and none of it is online: genuinely on battery.
|
||||||
|
(( found == 0 )) && return 1
|
||||||
|
if battery="$(battery_path)"; then
|
||||||
|
status="$(cat "$battery/status" 2>/dev/null || echo Unknown)"
|
||||||
|
[[ "$status" == "Discharging" ]] && return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ACPI first, logind second. Some platforms expose the lid only as an evdev
|
||||||
|
# switch with no /proc/acpi/button entry; logind watches the switch either
|
||||||
|
# way, so its LidClosed property is the fallback that keeps clamshell
|
||||||
|
# detection honest there. No logind (a container, a test tree) means the
|
||||||
|
# fallback quietly answers open, which is the safe direction.
|
||||||
|
lid_closed() {
|
||||||
|
local state
|
||||||
|
for state in "$ACPI"/button/lid/*/state; do
|
||||||
|
[[ -r "$state" ]] || continue
|
||||||
|
grep -qi closed "$state" && return 0
|
||||||
|
return 1
|
||||||
|
done
|
||||||
|
[[ -d "$ACPI/button/lid" ]] && return 1
|
||||||
|
busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
||||||
|
org.freedesktop.login1.Manager LidClosed 2>/dev/null | grep -q 'b true'
|
||||||
|
}
|
||||||
|
|
||||||
|
# A connected output that is not the built-in panel. eDP, LVDS and DSI are the
|
||||||
|
# internal ones; everything else arrived through a cable.
|
||||||
|
has_external_monitor() {
|
||||||
|
local status connector
|
||||||
|
for status in "$SYS"/class/drm/card*-*/status; do
|
||||||
|
[[ -r "$status" ]] || continue
|
||||||
|
[[ "$(cat "$status" 2>/dev/null)" == "connected" ]] || continue
|
||||||
|
connector="$(basename "$(dirname "$status")")"
|
||||||
|
case "$connector" in
|
||||||
|
*eDP*|*LVDS*|*DSI*) continue ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# The one definition the rest of the laptop work hangs on: the lid is shut and
|
||||||
|
# there is still a screen to use. Closing the lid on a dock must not suspend;
|
||||||
|
# closing it on a train must.
|
||||||
|
is_clamshell() { lid_closed && has_external_monitor; }
|
||||||
|
|
||||||
|
has_touchpad() {
|
||||||
|
local name
|
||||||
|
for name in "$SYS"/class/input/*/name; do
|
||||||
|
[[ -r "$name" ]] || continue
|
||||||
|
grep -qi touchpad "$name" && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vendor 0x10de on a display-class device. Read from sysfs rather than lspci
|
||||||
|
# so an idle discrete GPU is not woken to answer.
|
||||||
|
has_nvidia() {
|
||||||
|
local device vendor class
|
||||||
|
for device in "$SYS"/bus/pci/devices/*; do
|
||||||
|
[[ -r "$device/vendor" && -r "$device/class" ]] || continue
|
||||||
|
vendor="$(cat "$device/vendor" 2>/dev/null)"
|
||||||
|
[[ "$vendor" == "0x10de" ]] || continue
|
||||||
|
class="$(cat "$device/class" 2>/dev/null)"
|
||||||
|
[[ "$class" == 0x03* ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
answer() { "$1" && printf 'true' || printf 'false'; }
|
||||||
|
|
||||||
|
cmd_json() {
|
||||||
|
printf '{"laptop":%s,"battery":%s,"ac":%s,"lidClosed":%s,"externalMonitor":%s,"clamshell":%s,"touchpad":%s,"nvidia":%s}\n' \
|
||||||
|
"$(answer is_laptop)" "$(answer has_battery)" "$(answer on_ac)" \
|
||||||
|
"$(answer lid_closed)" "$(answer has_external_monitor)" \
|
||||||
|
"$(answer is_clamshell)" "$(answer has_touchpad)" "$(answer has_nvidia)"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
laptop) is_laptop ;;
|
||||||
|
battery) has_battery ;;
|
||||||
|
battery-path) battery_path ;;
|
||||||
|
ac) on_ac ;;
|
||||||
|
lid-closed) lid_closed ;;
|
||||||
|
external-monitor) has_external_monitor ;;
|
||||||
|
clamshell) is_clamshell ;;
|
||||||
|
touchpad) has_touchpad ;;
|
||||||
|
nvidia) has_nvidia ;;
|
||||||
|
--json) cmd_json ;;
|
||||||
|
-h|--help|"")
|
||||||
|
cat <<'USAGE'
|
||||||
|
usage: panama-hw <predicate>
|
||||||
|
|
||||||
|
Exits 0 for yes, 1 for no, and prints nothing.
|
||||||
|
|
||||||
|
laptop a portable chassis
|
||||||
|
battery a battery is present
|
||||||
|
battery-path print the first battery's sysfs path (0 if found)
|
||||||
|
ac on wall power (a machine with no mains is always yes)
|
||||||
|
lid-closed the lid is shut
|
||||||
|
external-monitor a connected output that is not the built-in panel
|
||||||
|
clamshell lid shut AND an external monitor: docked, keep working
|
||||||
|
touchpad a touchpad is present
|
||||||
|
nvidia an NVIDIA display device is present
|
||||||
|
|
||||||
|
--json every answer at once
|
||||||
|
USAGE
|
||||||
|
;;
|
||||||
|
*) printf 'panama-hw: unknown predicate: %s\n' "$1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Focus the window if it is already open; start it if it is not.
|
||||||
|
#
|
||||||
|
# panama-launch --class '^helium$' -- helium-browser-bin
|
||||||
|
# panama-launch --class '^kitty$' --title 'nvim' -- kitty nvim .
|
||||||
|
#
|
||||||
|
# This is what the application keys do on every other desktop. Pressing the
|
||||||
|
# browser key twice on macOS or Windows raises the browser; here it used to
|
||||||
|
# open a second one, which is the single most common "Linux feels wrong"
|
||||||
|
# moment and a twenty-line fix.
|
||||||
|
#
|
||||||
|
# Matching is a regular expression against the window class, optionally
|
||||||
|
# narrowed by title. Both halves matter: the terminal and the editor are both
|
||||||
|
# kitty on this desktop, and only the title tells them apart -- so a class-only
|
||||||
|
# match would make the editor key raise whatever terminal happened to be open.
|
||||||
|
#
|
||||||
|
# Anchor your patterns. `--class mail` would match `gmail-notifier`, and the
|
||||||
|
# key that should open Thunderbird would raise somebody's notifier instead.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
class_pattern=""
|
||||||
|
title_pattern=""
|
||||||
|
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
--class) class_pattern="${2:-}"; shift 2 ;;
|
||||||
|
--title) title_pattern="${2:-}"; shift 2 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$class_pattern" || $# -eq 0 ]]; then
|
||||||
|
echo 'usage: panama-launch --class <regex> [--title <regex>] -- command [args...]' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
launch() {
|
||||||
|
# setsid so the application outlives this script and is not a child of the
|
||||||
|
# compositor's exec, which would tie its lifetime to a shell that exits.
|
||||||
|
setsid "$@" >/dev/null 2>&1 &
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# No compositor, no window list: just start the thing.
|
||||||
|
command -v hyprctl >/dev/null 2>&1 || launch "$@"
|
||||||
|
|
||||||
|
address="$(hyprctl clients -j 2>/dev/null | jq -r --arg class "$class_pattern" --arg title "$title_pattern" '
|
||||||
|
[ .[]
|
||||||
|
| select(.mapped)
|
||||||
|
| select(.class | test($class))
|
||||||
|
| select($title == "" or (.title | test($title)))
|
||||||
|
]
|
||||||
|
# Most recently focused first: with several matches, raise the one the
|
||||||
|
# user was last in rather than whichever the compositor lists first.
|
||||||
|
| sort_by(-.focusHistoryID)
|
||||||
|
| .[0].address // empty
|
||||||
|
' 2>/dev/null)"
|
||||||
|
|
||||||
|
if [[ -n "$address" ]]; then
|
||||||
|
# Hyprland 0.56 dispatches through Lua: `hyprctl dispatch focuswindow
|
||||||
|
# address:0x...` is parsed as Lua source and fails. The selector string is
|
||||||
|
# what hl.focus accepts; a table of the same fields is refused.
|
||||||
|
exec hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })"
|
||||||
|
fi
|
||||||
|
|
||||||
|
launch "$@"
|
||||||
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Bringing an already-installed machine up to what this checkout expects.
|
||||||
|
#
|
||||||
|
# `./install` is additive: it copies files over `/`, links dotfiles, and
|
||||||
|
# installs packages. It has no way to say "remove that file", "disable that
|
||||||
|
# unit", "that symlink now points at the wrong place". So a machine installed
|
||||||
|
# in March keeps March's mistakes forever, and the only thing that ever fixes
|
||||||
|
# them is the person who happens to read a commit message.
|
||||||
|
#
|
||||||
|
# A migration is one shell script that performs one such repair, exactly once,
|
||||||
|
# on machines that need it.
|
||||||
|
#
|
||||||
|
# migrations/<unix-timestamp>.sh
|
||||||
|
#
|
||||||
|
# The name is the commit timestamp of HEAD when it was authored, so glob order
|
||||||
|
# over fixed-width epoch seconds IS chronological order -- no sequence numbers
|
||||||
|
# to collide on across branches. `panama-dev-migration` stamps them.
|
||||||
|
#
|
||||||
|
# State is one empty marker file per migration under
|
||||||
|
# $XDG_STATE_HOME/panama/migrations. Present means applied. There is no
|
||||||
|
# database and no version integer, because the failure mode of a version
|
||||||
|
# integer is that one bad migration strands every later one behind it.
|
||||||
|
#
|
||||||
|
# The rules a migration must follow are in the template that
|
||||||
|
# `panama-dev-migration` writes, and they are worth repeating here because
|
||||||
|
# this runner cannot enforce them:
|
||||||
|
#
|
||||||
|
# * Safe to run twice. The marker only records that it succeeded once.
|
||||||
|
# * Tolerant of the repair already being correct -- a user may have fixed it
|
||||||
|
# by hand, or a later `./install` may have overwritten it back.
|
||||||
|
# * Root work goes through `panama-sudo --reason "..."`, never bare sudo,
|
||||||
|
# so the prompt names the repair. See bin/panama-sudo.
|
||||||
|
#
|
||||||
|
# The marker is written ONLY on success, so a migration that fails stays
|
||||||
|
# pending and is retried at the next login. That is deliberate: a repair that
|
||||||
|
# could not complete has not happened, and recording it as done would hide it
|
||||||
|
# forever.
|
||||||
|
#
|
||||||
|
# This mirrors config/dot/quickshell/config/Migrations.qml, which does the same
|
||||||
|
# job for the settings JSON and documents the same reasoning. That one handles
|
||||||
|
# renamed preference keys; this one handles everything else.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
||||||
|
MIGRATIONS_DIR="$PANAMA_PATH/migrations"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama/migrations"
|
||||||
|
|
||||||
|
export PANAMA_PATH
|
||||||
|
|
||||||
|
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||||||
|
ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
|
||||||
|
err() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
# Every migration this checkout ships, oldest first. Empty is a valid state.
|
||||||
|
all_migrations() {
|
||||||
|
[[ -d "$MIGRATIONS_DIR" ]] || return 0
|
||||||
|
local file
|
||||||
|
for file in "$MIGRATIONS_DIR"/*.sh; do
|
||||||
|
[[ -e "$file" ]] || continue
|
||||||
|
basename "$file"
|
||||||
|
done | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_migrations() {
|
||||||
|
local name
|
||||||
|
while read -r name; do
|
||||||
|
[[ -n "$name" ]] || continue
|
||||||
|
[[ -e "$STATE_DIR/$name" ]] || printf '%s\n' "$name"
|
||||||
|
done < <(all_migrations)
|
||||||
|
}
|
||||||
|
|
||||||
|
run_one() {
|
||||||
|
local name="$1" file="$MIGRATIONS_DIR/$1"
|
||||||
|
info "$name"
|
||||||
|
# A subshell with its own strictness: a migration that forgets `set -e` is
|
||||||
|
# still stopped by its first failing command, and one that sets shell
|
||||||
|
# options cannot leak them into the next migration.
|
||||||
|
if bash -euo pipefail "$file"; then
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
: >"$STATE_DIR/$name"
|
||||||
|
ok "$name applied"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
err "$name failed and will be retried at the next login"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_run() {
|
||||||
|
local pending
|
||||||
|
pending="$(pending_migrations)"
|
||||||
|
if [[ -z "$pending" ]]; then
|
||||||
|
ok "Nothing to migrate; this machine matches the checkout."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local count failed=0 name
|
||||||
|
count="$(grep -c . <<<"$pending")"
|
||||||
|
info "$count migration(s) to apply"
|
||||||
|
while read -r name; do
|
||||||
|
[[ -n "$name" ]] || continue
|
||||||
|
# Stop at the first failure rather than continuing. Migrations are
|
||||||
|
# ordered, and a later one may assume an earlier one landed; running
|
||||||
|
# it anyway turns one stuck repair into an unpredictable machine.
|
||||||
|
if ! run_one "$name"; then
|
||||||
|
failed=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done <<<"$pending"
|
||||||
|
|
||||||
|
if (( failed )); then
|
||||||
|
warn "Re-running 'panama migrate' is safe and will retry from the failure."
|
||||||
|
warn "If it keeps failing, hand it to an agent: panama diagnose"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ok "This machine now matches the checkout."
|
||||||
|
# Only after repairs actually ran: a hook that fires on every login when
|
||||||
|
# there was nothing to do is a hook people disable.
|
||||||
|
"$PANAMA_PATH/bin/panama-hook" post-migrate || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# The check the login notifier runs. Exit 0 means work is waiting, so it reads
|
||||||
|
# as `if panama-migrate --pending; then notify; fi`.
|
||||||
|
cmd_pending() {
|
||||||
|
local pending
|
||||||
|
pending="$(pending_migrations)"
|
||||||
|
[[ -n "$pending" ]] || return 1
|
||||||
|
grep -c . <<<"$pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local name
|
||||||
|
while read -r name; do
|
||||||
|
[[ -n "$name" ]] || continue
|
||||||
|
if [[ -e "$STATE_DIR/$name" ]]; then
|
||||||
|
printf 'applied %s\n' "$name"
|
||||||
|
else
|
||||||
|
printf 'pending %s\n' "$name"
|
||||||
|
fi
|
||||||
|
done < <(all_migrations)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-run one that already succeeded. For developing a migration, and for the
|
||||||
|
# rare case where a repair was undone by something else.
|
||||||
|
cmd_force() {
|
||||||
|
local name="${1:-}"
|
||||||
|
[[ -n "$name" ]] || { err "force needs a migration name"; return 2; }
|
||||||
|
[[ -e "$MIGRATIONS_DIR/$name" ]] || { err "no such migration: $name"; return 2; }
|
||||||
|
rm -f "$STATE_DIR/$name"
|
||||||
|
run_one "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mark everything applied without running it. This is what a fresh install
|
||||||
|
# does: the machine was just built from this checkout, so every repair those
|
||||||
|
# migrations describe is already true of it, and running them would apply
|
||||||
|
# fixes for versions it never had.
|
||||||
|
cmd_baseline() {
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
local name count=0
|
||||||
|
while read -r name; do
|
||||||
|
[[ -n "$name" ]] || continue
|
||||||
|
[[ -e "$STATE_DIR/$name" ]] && continue
|
||||||
|
: >"$STATE_DIR/$name"
|
||||||
|
count=$(( count + 1 ))
|
||||||
|
done < <(all_migrations)
|
||||||
|
ok "Marked $count migration(s) as already applied."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-run}" in
|
||||||
|
run) shift || true; cmd_run "$@" ;;
|
||||||
|
--pending) shift || true; cmd_pending "$@" ;;
|
||||||
|
--list|list) shift || true; cmd_list "$@" ;;
|
||||||
|
--force) shift || true; cmd_force "$@" ;;
|
||||||
|
--baseline) shift || true; cmd_baseline "$@" ;;
|
||||||
|
-h|--help)
|
||||||
|
cat <<'USAGE'
|
||||||
|
usage: panama-migrate [run|--pending|--list|--force <name>|--baseline]
|
||||||
|
|
||||||
|
run Apply every pending migration, oldest first (default)
|
||||||
|
--pending Exit 0 and print the count when work is waiting, else exit 1
|
||||||
|
--list Show every migration and whether it has been applied
|
||||||
|
--force Re-run one migration that already succeeded
|
||||||
|
--baseline Mark everything applied without running it (fresh installs)
|
||||||
|
USAGE
|
||||||
|
;;
|
||||||
|
*) err "unknown argument: $1"; exit 2 ;;
|
||||||
|
esac
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# "Panama has repairs waiting for this machine."
|
||||||
|
#
|
||||||
|
# A migration that nobody knows about is a migration that never runs. This
|
||||||
|
# checks once per session and, when work is pending, sends one notification
|
||||||
|
# whose action opens a terminal running `panama migrate` -- so the repair is
|
||||||
|
# always something the user chose, never something that happened to them.
|
||||||
|
#
|
||||||
|
# Waiting for the notification server first is not politeness. Quickshell owns
|
||||||
|
# org.freedesktop.Notifications, and a shell that has not started yet would
|
||||||
|
# swallow this silently -- which is precisely the session where a pending
|
||||||
|
# migration is most likely to matter.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)}"
|
||||||
|
TERMINAL="${PANAMA_TERMINAL:-kitty}"
|
||||||
|
|
||||||
|
command -v notify-send >/dev/null 2>&1 || exit 1
|
||||||
|
|
||||||
|
count="$("$PANAMA_PATH/bin/panama-migrate" --pending)" || exit 1
|
||||||
|
[[ -n "$count" ]] || exit 1
|
||||||
|
|
||||||
|
# Up to ~15s for the shell to claim the bus name. Longer than a healthy start
|
||||||
|
# needs, short enough that a session without a shell gives up rather than
|
||||||
|
# lingering.
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
if busctl --user status org.freedesktop.Notifications >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( count == 1 )); then
|
||||||
|
body="One repair is waiting for this machine."
|
||||||
|
else
|
||||||
|
body="$count repairs are waiting for this machine."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Critical so it waits to be read: a notification that expires while the user
|
||||||
|
# is elsewhere has told nobody anything. The action is the whole point -- there
|
||||||
|
# is no instruction to remember and nothing to type.
|
||||||
|
action="$(notify-send --urgency=critical --icon=system-software-update \
|
||||||
|
--app-name=Panama \
|
||||||
|
--action=migrate="Apply now" --action=later="Later" \
|
||||||
|
"Panama updates" "$body" 2>/dev/null)" || exit 0
|
||||||
|
|
||||||
|
[[ "$action" == "migrate" ]] || exit 0
|
||||||
|
exec "$TERMINAL" --hold "$PANAMA_PATH/bin/panama-migrate" run
|
||||||
Executable
+323
@@ -0,0 +1,323 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# panama-server – the compose services a server machine runs.
|
||||||
|
#
|
||||||
|
# The repository carries the catalog: server/containers/<Name>/ holds a
|
||||||
|
# compose.yml, a podman-<name>.service user unit, and an .env.example naming
|
||||||
|
# what the service needs told. The machine carries the rest: ~/Server/<Name>/
|
||||||
|
# is a real directory owning the .env (secrets never sit inside the checkout,
|
||||||
|
# gitignored or not) and the bind-mounted data. Only the tracked files are
|
||||||
|
# symlinked, so a `git clean` in the repo can never reach a database.
|
||||||
|
#
|
||||||
|
# list every service in the catalog, with its state here
|
||||||
|
# enable <Name> link it into ~/Server, seed .env, enable the unit
|
||||||
|
# disable <Name> stop it and remove the unit; data and .env stay put
|
||||||
|
# status [<Name>] what is actually running
|
||||||
|
# relink refresh the symlinks for everything enabled here
|
||||||
|
#
|
||||||
|
# Verbs act on the unit and the links, never on data: there is deliberately no
|
||||||
|
# verb here that deletes ~/Server/<Name> or anything in it.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
PROGRAM="panama server"
|
||||||
|
|
||||||
|
SCRIPT_PATH=$(readlink -f "${BASH_SOURCE[0]}")
|
||||||
|
PANAMA_DIR=$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)
|
||||||
|
|
||||||
|
# Overridable so the contract can point this at a fixture catalog and a
|
||||||
|
# throwaway HOME rather than the machine's real services.
|
||||||
|
SERVER_DIR="${PANAMA_SERVER_DIR:-$PANAMA_DIR/server/containers}"
|
||||||
|
TARGET_DIR="${PANAMA_SERVER_TARGET:-$HOME/Server}"
|
||||||
|
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||||
|
STATE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/panama/server-definitions"
|
||||||
|
|
||||||
|
if [[ -t 1 ]] && command -v tput >/dev/null 2>&1 && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then
|
||||||
|
BOLD=$(tput bold); RESET=$(tput sgr0)
|
||||||
|
RED=$(tput setaf 1); GREEN=$(tput setaf 2); YELLOW=$(tput setaf 3); BLUE=$(tput setaf 4)
|
||||||
|
else
|
||||||
|
BOLD=""; RESET=""; RED=""; GREEN=""; YELLOW=""; BLUE=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
info() { printf '%s==>%s %s\n' "${BLUE}${BOLD}" "$RESET" "$*"; }
|
||||||
|
ok() { printf '%s✓%s %s\n' "${GREEN}${BOLD}" "$RESET" "$*"; }
|
||||||
|
warn() { printf '%s!%s %s\n' "${YELLOW}${BOLD}" "$RESET" "$*"; }
|
||||||
|
err() { printf '%s✗%s %s\n' "${RED}${BOLD}" "$RESET" "$*" >&2; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
${BOLD}$PROGRAM${RESET} – manage the compose services in server/containers/
|
||||||
|
|
||||||
|
${BOLD}Usage:${RESET}
|
||||||
|
$PROGRAM list Every service in the catalog, and its state here
|
||||||
|
$PROGRAM enable <Name> Link a service into ~/Server, seed its .env from
|
||||||
|
.env.example, and enable its unit. Stops short of
|
||||||
|
starting when the .env still holds CHANGE_ME
|
||||||
|
placeholders, and says which.
|
||||||
|
$PROGRAM disable <Name> Stop the unit and unlink it. ~/Server/<Name>,
|
||||||
|
its .env and its data are never touched.
|
||||||
|
$PROGRAM status [<Name>] What is actually running, per service
|
||||||
|
$PROGRAM relink Refresh every enabled service's symlinks after a
|
||||||
|
pull, and name the ones whose definitions changed
|
||||||
|
(nothing is restarted for you)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── The catalog ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
services() {
|
||||||
|
local dir
|
||||||
|
for dir in "$SERVER_DIR"/*/; do
|
||||||
|
[[ -d "$dir" ]] || continue
|
||||||
|
basename "$dir"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
require_service() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ ! -d "$SERVER_DIR/$name" ]]; then
|
||||||
|
err "No such service: '$name'"
|
||||||
|
printf 'The catalog has:\n' >&2
|
||||||
|
services | sed 's/^/ /' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The one unit file a service directory carries. Its filename is its identity
|
||||||
|
# -- podman-<name>.service, exactly what lands in the systemd user directory --
|
||||||
|
# so nothing here invents a name that could drift from the file's.
|
||||||
|
unit_path_for() {
|
||||||
|
local name="$1" unit
|
||||||
|
for unit in "$SERVER_DIR/$name"/*.service; do
|
||||||
|
[[ -e "$unit" ]] || continue
|
||||||
|
printf '%s' "$unit"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
unit_name_for() {
|
||||||
|
local unit
|
||||||
|
unit="$(unit_path_for "$1")" || return 1
|
||||||
|
basename "$unit"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Installed means our symlink is in the systemd user directory. A regular file
|
||||||
|
# of the same name is a pre-Panama install done by hand -- reported, never
|
||||||
|
# silently replaced.
|
||||||
|
unit_installed() {
|
||||||
|
local unit_name
|
||||||
|
unit_name="$(unit_name_for "$1")" || return 1
|
||||||
|
[[ -L "$UNIT_DIR/$unit_name" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
definition_hash() {
|
||||||
|
local name="$1" unit
|
||||||
|
unit="$(unit_path_for "$name")" || unit=/dev/null
|
||||||
|
cat "$SERVER_DIR/$name/compose.yml" "$unit" 2>/dev/null | sha256sum | cut -d' ' -f1
|
||||||
|
}
|
||||||
|
|
||||||
|
recorded_hash() {
|
||||||
|
[[ -r "$STATE_FILE" ]] || return 0
|
||||||
|
awk -v name="$1" '$1 == name {print $2}' "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
record_hash() {
|
||||||
|
local name="$1" hash="$2"
|
||||||
|
mkdir -p "$(dirname "$STATE_FILE")"
|
||||||
|
{ [[ -r "$STATE_FILE" ]] && awk -v name="$name" '$1 != name' "$STATE_FILE"; \
|
||||||
|
printf '%s %s\n' "$name" "$hash"; } >"$STATE_FILE.tmp"
|
||||||
|
mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Replace path with a symlink to src. An existing regular file is moved aside
|
||||||
|
# with its name intact plus a suffix, because the only regular file that can be
|
||||||
|
# here is a pre-Panama one somebody wrote by hand -- worth keeping to diff.
|
||||||
|
link_file() {
|
||||||
|
local src="$1" path="$2"
|
||||||
|
if [[ -L "$path" ]]; then
|
||||||
|
[[ "$(readlink -f "$path")" == "$(readlink -f "$src")" ]] && return 0
|
||||||
|
rm "$path"
|
||||||
|
elif [[ -e "$path" ]]; then
|
||||||
|
mv "$path" "$path.pre-panama"
|
||||||
|
warn "Kept the existing $(basename "$path") as $(basename "$path").pre-panama"
|
||||||
|
fi
|
||||||
|
ln -s "$src" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Verbs ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local name unit_name state enabled
|
||||||
|
for name in $(services); do
|
||||||
|
unit_name="$(unit_name_for "$name")" || { printf '%-24s %s\n' "$name" "no unit file"; continue; }
|
||||||
|
if unit_installed "$name"; then
|
||||||
|
state="$(systemctl --user is-active "$unit_name" 2>/dev/null || true)"
|
||||||
|
enabled="enabled"
|
||||||
|
[[ "$state" == active ]] && state="${GREEN}active${RESET}" || state="${YELLOW}${state:-unknown}${RESET}"
|
||||||
|
printf '%-24s %s, %b\n' "$name" "$enabled" "$state"
|
||||||
|
elif [[ -e "$UNIT_DIR/$unit_name" ]]; then
|
||||||
|
printf '%-24s %s\n' "$name" "installed by hand (not Panama's symlink)"
|
||||||
|
else
|
||||||
|
printf '%-24s %s\n' "$name" "-"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_enable() {
|
||||||
|
local name="${1:-}"
|
||||||
|
[[ -n "$name" ]] || { err "Which service? Usage: $PROGRAM enable <Name>"; exit 1; }
|
||||||
|
require_service "$name"
|
||||||
|
|
||||||
|
local repo_dir="$SERVER_DIR/$name" live_dir="$TARGET_DIR/$name"
|
||||||
|
[[ -f "$repo_dir/compose.yml" ]] || { err "$name has no compose.yml in the catalog"; exit 1; }
|
||||||
|
local unit_path unit_name
|
||||||
|
unit_path="$(unit_path_for "$name")" || { err "$name has no unit file in the catalog"; exit 1; }
|
||||||
|
unit_name="$(basename "$unit_path")"
|
||||||
|
|
||||||
|
mkdir -p "$live_dir"
|
||||||
|
link_file "$repo_dir/compose.yml" "$live_dir/compose.yml"
|
||||||
|
ok "Linked compose.yml → $live_dir/compose.yml"
|
||||||
|
|
||||||
|
# The .env lives with the machine, not the checkout. Seeded once from the
|
||||||
|
# example and never overwritten -- it is where the person's secrets go.
|
||||||
|
if [[ -f "$repo_dir/.env.example" && ! -e "$live_dir/.env" ]]; then
|
||||||
|
cp "$repo_dir/.env.example" "$live_dir/.env"
|
||||||
|
chmod 600 "$live_dir/.env"
|
||||||
|
ok "Seeded $live_dir/.env from .env.example"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Refusing to start on placeholders, and saying which: a service brought up
|
||||||
|
# with CHANGE_ME as its database password does not fail loudly, it runs --
|
||||||
|
# and what it runs is a service whose password is CHANGE_ME.
|
||||||
|
if [[ -e "$live_dir/.env" ]] && grep -q 'CHANGE_ME' "$live_dir/.env"; then
|
||||||
|
warn "$live_dir/.env still has placeholder values:"
|
||||||
|
grep -n 'CHANGE_ME' "$live_dir/.env" | sed 's/^/ /'
|
||||||
|
printf 'Fill them in, then run: %s enable %s\n' "$PROGRAM" "$name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$UNIT_DIR"
|
||||||
|
link_file "$unit_path" "$UNIT_DIR/$unit_name"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
if systemctl --user enable --now "$unit_name"; then
|
||||||
|
record_hash "$name" "$(definition_hash "$name")"
|
||||||
|
ok "$name enabled and started ($unit_name)"
|
||||||
|
else
|
||||||
|
err "$unit_name did not start; see: systemctl --user status $unit_name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_disable() {
|
||||||
|
local name="${1:-}"
|
||||||
|
[[ -n "$name" ]] || { err "Which service? Usage: $PROGRAM disable <Name>"; exit 1; }
|
||||||
|
require_service "$name"
|
||||||
|
local unit_name
|
||||||
|
unit_name="$(unit_name_for "$name")" || { err "$name has no unit file in the catalog"; exit 1; }
|
||||||
|
|
||||||
|
systemctl --user disable --now "$unit_name" 2>/dev/null || true
|
||||||
|
if [[ -L "$UNIT_DIR/$unit_name" ]]; then
|
||||||
|
rm "$UNIT_DIR/$unit_name"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
fi
|
||||||
|
ok "$name disabled. ~/Server/$name, its .env and its data were not touched."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
local name="${1:-}"
|
||||||
|
if [[ -n "$name" ]]; then
|
||||||
|
require_service "$name"
|
||||||
|
local unit_name
|
||||||
|
unit_name="$(unit_name_for "$name")" || { err "$name has no unit file"; exit 1; }
|
||||||
|
systemctl --user status --no-pager "$unit_name" || true
|
||||||
|
if [[ -d "$TARGET_DIR/$name" ]]; then
|
||||||
|
( cd "$TARGET_DIR/$name" && podman compose ps 2>/dev/null ) || true
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The role's health summary: what is enabled here, and is it actually up.
|
||||||
|
local any=0 unit_name state
|
||||||
|
for name in $(services); do
|
||||||
|
unit_installed "$name" || continue
|
||||||
|
any=1
|
||||||
|
unit_name="$(unit_name_for "$name")"
|
||||||
|
state="$(systemctl --user is-active "$unit_name" 2>/dev/null || true)"
|
||||||
|
if [[ "$state" == active ]]; then
|
||||||
|
ok "$name"
|
||||||
|
else
|
||||||
|
err "$name is ${state:-unknown}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
(( any )) || info "No services enabled on this machine yet. See: $PROGRAM list"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_relink() {
|
||||||
|
local name unit_path unit_name changed=() unmanaged=() refreshed=0
|
||||||
|
for name in $(services); do
|
||||||
|
unit_path="$(unit_path_for "$name")" || continue
|
||||||
|
unit_name="$(basename "$unit_path")"
|
||||||
|
|
||||||
|
if [[ -e "$UNIT_DIR/$unit_name" && ! -L "$UNIT_DIR/$unit_name" ]]; then
|
||||||
|
unmanaged+=("$name")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
unit_installed "$name" || continue
|
||||||
|
|
||||||
|
link_file "$unit_path" "$UNIT_DIR/$unit_name"
|
||||||
|
mkdir -p "$TARGET_DIR/$name"
|
||||||
|
link_file "$SERVER_DIR/$name/compose.yml" "$TARGET_DIR/$name/compose.yml"
|
||||||
|
refreshed=$((refreshed + 1))
|
||||||
|
|
||||||
|
# Changed since the last time this machine looked, which is what makes a
|
||||||
|
# pull actionable: the restart is deliberately yours to run, so the least
|
||||||
|
# this can do is say which services are running a definition that is no
|
||||||
|
# longer what the repository says.
|
||||||
|
local now
|
||||||
|
now="$(definition_hash "$name")"
|
||||||
|
if [[ "$(recorded_hash "$name")" != "$now" ]]; then
|
||||||
|
changed+=("$name")
|
||||||
|
record_hash "$name" "$now"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
systemctl --user daemon-reload 2>/dev/null || true
|
||||||
|
info "Relinked $refreshed enabled service(s)"
|
||||||
|
if (( ${#unmanaged[@]} > 0 )); then
|
||||||
|
warn "Installed by hand, left alone: ${unmanaged[*]}"
|
||||||
|
fi
|
||||||
|
if (( ${#changed[@]} > 0 )); then
|
||||||
|
warn "Definitions changed; restart each when ready:"
|
||||||
|
for name in "${changed[@]}"; do
|
||||||
|
printf ' %s: systemctl --user restart %s\n' "$name" "$(unit_name_for "$name")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dispatcher ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [[ ! -d "$SERVER_DIR" ]]; then
|
||||||
|
err "No service catalog at $SERVER_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
local cmd="${1:-}"
|
||||||
|
case "$cmd" in
|
||||||
|
list) shift; cmd_list "$@" ;;
|
||||||
|
enable) shift; cmd_enable "$@" ;;
|
||||||
|
disable) shift; cmd_disable "$@" ;;
|
||||||
|
status) shift; cmd_status "$@" ;;
|
||||||
|
relink) shift; cmd_relink "$@" ;;
|
||||||
|
help|-h|--help|"") usage ;;
|
||||||
|
*)
|
||||||
|
err "Unknown command: '$cmd'"
|
||||||
|
echo
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# pkexec, with a stated reason on the password prompt.
|
||||||
|
#
|
||||||
|
# panama-sudo --reason "Installing gamemode hooks" -- dnf install gamemode
|
||||||
|
#
|
||||||
|
# The reason travels to the shell over Quickshell IPC before pkexec runs, and
|
||||||
|
# Panama's prompt shows it clearly labeled beside polkitd's own action message
|
||||||
|
# -- beside, never instead of: anything can claim any reason, so the real
|
||||||
|
# action text stays the trust anchor. Meant for agents and scripts, so the
|
||||||
|
# person at the keyboard learns WHY before typing their password.
|
||||||
|
#
|
||||||
|
# Degrades to plain pkexec: no --reason, no running shell, or no qs on PATH
|
||||||
|
# all behave identically to calling pkexec yourself.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
reason=""
|
||||||
|
if [[ "${1:-}" == "--reason" ]]; then
|
||||||
|
reason="${2:?panama-sudo: --reason needs a value}"
|
||||||
|
shift 2
|
||||||
|
fi
|
||||||
|
[[ "${1:-}" == "--" ]] && shift
|
||||||
|
|
||||||
|
if (( $# == 0 )); then
|
||||||
|
echo 'usage: panama-sudo [--reason "why"] -- command [args...]' >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$reason" ]] && command -v qs >/dev/null 2>&1; then
|
||||||
|
qs ipc call polkit reason "$reason" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec pkexec "$@"
|
||||||
Executable
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Make a video smaller, or turn a picture into another format.
|
||||||
|
#
|
||||||
|
# panama-transcode video <file> 1080p|720p|480p
|
||||||
|
# panama-transcode image <file> jpg|png|webp
|
||||||
|
#
|
||||||
|
# The two things people actually right-click a media file to do. macOS has
|
||||||
|
# Quick Actions for both; Linux file managers have neither, and the usual
|
||||||
|
# answer is a web uploader or a forgotten ffmpeg incantation.
|
||||||
|
#
|
||||||
|
# Two rules, both about not losing work:
|
||||||
|
#
|
||||||
|
# * The input is never written to. Output goes beside it with a suffix.
|
||||||
|
# * An existing output is never overwritten. The suffix gains a number
|
||||||
|
# rather than replacing something somebody made earlier.
|
||||||
|
#
|
||||||
|
# ffmpeg does the work and is already a declared dependency; this only decides
|
||||||
|
# the arguments, which is the part worth writing down once.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
err() { printf 'panama-transcode: %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
command -v ffmpeg >/dev/null 2>&1 || { err 'ffmpeg is not installed'; exit 1; }
|
||||||
|
|
||||||
|
# A path that does not exist yet, beside the input.
|
||||||
|
free_path() {
|
||||||
|
local dir="$1" stem="$2" suffix="$3" ext="$4"
|
||||||
|
local candidate="$dir/$stem-$suffix.$ext"
|
||||||
|
local counter=2
|
||||||
|
while [[ -e "$candidate" ]]; do
|
||||||
|
candidate="$dir/$stem-$suffix-$counter.$ext"
|
||||||
|
counter=$(( counter + 1 ))
|
||||||
|
done
|
||||||
|
printf '%s' "$candidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
notify() {
|
||||||
|
command -v notify-send >/dev/null 2>&1 || return 0
|
||||||
|
notify-send --icon="${3:-video-x-generic}" "$1" "$2" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_video() {
|
||||||
|
local input="${1:-}" preset="${2:-1080p}"
|
||||||
|
[[ -f "$input" ]] || { err 'that file does not exist'; return 2; }
|
||||||
|
|
||||||
|
local height
|
||||||
|
case "$preset" in
|
||||||
|
1080p) height=1080 ;;
|
||||||
|
720p) height=720 ;;
|
||||||
|
480p) height=480 ;;
|
||||||
|
*) err "unknown size: $preset"; return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local dir stem output
|
||||||
|
dir="$(dirname "$input")"
|
||||||
|
stem="$(basename "${input%.*}")"
|
||||||
|
output="$(free_path "$dir" "$stem" "$preset" mp4)"
|
||||||
|
|
||||||
|
notify "Transcoding" "$(basename "$input") → $preset" video-x-generic
|
||||||
|
# -2 rather than -1 on width: H.264 needs even dimensions, and an odd one
|
||||||
|
# fails at the very end of a long encode.
|
||||||
|
if ffmpeg -nostdin -loglevel error -i "$input" \
|
||||||
|
-vf "scale=-2:'min($height,ih)'" \
|
||||||
|
-c:v libx264 -crf 23 -preset medium \
|
||||||
|
-c:a aac -b:a 128k \
|
||||||
|
"$output" </dev/null; then
|
||||||
|
notify "Transcoded" "$(basename "$output")" video-x-generic
|
||||||
|
printf '%s\n' "$output"
|
||||||
|
else
|
||||||
|
rm -f "$output"
|
||||||
|
notify "Transcode failed" "$(basename "$input")" dialog-error-symbolic
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_image() {
|
||||||
|
local input="${1:-}" format="${2:-jpg}"
|
||||||
|
[[ -f "$input" ]] || { err 'that file does not exist'; return 2; }
|
||||||
|
|
||||||
|
case "$format" in
|
||||||
|
jpg|png|webp) ;;
|
||||||
|
*) err "unknown format: $format"; return 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
local dir stem output
|
||||||
|
dir="$(dirname "$input")"
|
||||||
|
stem="$(basename "${input%.*}")"
|
||||||
|
output="$(free_path "$dir" "$stem" converted "$format")"
|
||||||
|
|
||||||
|
local -a quality=()
|
||||||
|
[[ "$format" == "jpg" ]] && quality=(-q:v 3)
|
||||||
|
[[ "$format" == "webp" ]] && quality=(-quality 82)
|
||||||
|
|
||||||
|
if ffmpeg -nostdin -loglevel error -i "$input" "${quality[@]}" "$output" </dev/null; then
|
||||||
|
notify "Converted" "$(basename "$output")" image-x-generic
|
||||||
|
printf '%s\n' "$output"
|
||||||
|
else
|
||||||
|
rm -f "$output"
|
||||||
|
notify "Conversion failed" "$(basename "$input")" dialog-error-symbolic
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
video) shift; cmd_video "$@" ;;
|
||||||
|
image) shift; cmd_image "$@" ;;
|
||||||
|
-h|--help|"")
|
||||||
|
cat <<'USAGE'
|
||||||
|
usage: panama-transcode video <file> [1080p|720p|480p]
|
||||||
|
panama-transcode image <file> [jpg|png|webp]
|
||||||
|
|
||||||
|
Writes beside the input, never over it, and never over an existing output.
|
||||||
|
USAGE
|
||||||
|
;;
|
||||||
|
*) err "unknown command: $1"; exit 2 ;;
|
||||||
|
esac
|
||||||
Executable
+194
@@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# A website, as an application.
|
||||||
|
#
|
||||||
|
# panama-webapp install https://app.example.com "Example"
|
||||||
|
# panama-webapp list
|
||||||
|
# panama-webapp remove "Example"
|
||||||
|
#
|
||||||
|
# Both macOS and Windows ship this now -- Safari's "Add to Dock", Edge's
|
||||||
|
# "Install this site as an app" -- and the dock and launcher here had nothing
|
||||||
|
# to feed them but installed packages. A web app gets its own icon, its own
|
||||||
|
# window with no browser chrome, and its own entry in the launcher, which is
|
||||||
|
# most of what "installed" means in practice.
|
||||||
|
#
|
||||||
|
# The desktop entry is ordinary and inspectable: it lives in
|
||||||
|
# ~/.local/share/applications with everything else, and `remove` finds its own
|
||||||
|
# entries by the launcher line rather than by keeping a list somewhere.
|
||||||
|
#
|
||||||
|
# Chromium-family browsers implement --app. Firefox does not, and there is no
|
||||||
|
# honest equivalent, so a machine whose default browser is Firefox is told
|
||||||
|
# rather than given something that opens a normal window and pretends.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
APPLICATIONS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||||||
|
ICONS="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/256x256/apps"
|
||||||
|
LAUNCH_MARKER="panama-webapp"
|
||||||
|
|
||||||
|
err() { printf 'panama-webapp: %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
# A filename that cannot escape the applications directory. Everything that is
|
||||||
|
# not a letter or a digit becomes a hyphen, which also makes the result
|
||||||
|
# predictable enough for `remove` to find.
|
||||||
|
slugify() {
|
||||||
|
printf '%s' "$1" \
|
||||||
|
| tr '[:upper:]' '[:lower:]' \
|
||||||
|
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# The browser to open the app window with. Follows the desktop's own default
|
||||||
|
# rather than naming one, and refuses rather than degrading when that browser
|
||||||
|
# cannot do app mode.
|
||||||
|
resolve_browser() {
|
||||||
|
local desktop exec_line binary
|
||||||
|
desktop="$(xdg-settings get default-web-browser 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ -n "$desktop" ]]; then
|
||||||
|
local file
|
||||||
|
for dir in "$APPLICATIONS" /usr/local/share/applications /usr/share/applications; do
|
||||||
|
file="$dir/$desktop"
|
||||||
|
[[ -r "$file" ]] || continue
|
||||||
|
exec_line="$(sed -n 's/^Exec=//p' "$file" | head -1)"
|
||||||
|
binary="$(awk '{ print $1 }' <<<"$exec_line")"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "${binary:-}" ]] || binary="$(command -v chromium || command -v google-chrome || true)"
|
||||||
|
[[ -n "$binary" ]] || return 1
|
||||||
|
|
||||||
|
# Chromium-family only. The name check is crude but the alternative is
|
||||||
|
# launching a browser to ask it, which is worse.
|
||||||
|
case "$(basename "$binary")" in
|
||||||
|
*firefox*|*librewolf*|*zen*) return 2 ;;
|
||||||
|
esac
|
||||||
|
printf '%s' "$binary"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Four attempts, in descending order of how much the site had to say about it.
|
||||||
|
# An icon is never worth failing an install over: a web app with the generic
|
||||||
|
# icon still works.
|
||||||
|
fetch_icon() {
|
||||||
|
local url="$1" slug="$2" origin html href target
|
||||||
|
origin="$(sed -E 's#^(https?://[^/]+).*#\1#' <<<"$url")"
|
||||||
|
target="$ICONS/$slug.png"
|
||||||
|
mkdir -p "$ICONS"
|
||||||
|
|
||||||
|
html="$(curl -fsSL --max-time 10 "$url" 2>/dev/null || true)"
|
||||||
|
href="$(grep -oiE '<link[^>]+rel="[^"]*apple-touch-icon[^"]*"[^>]*>' <<<"$html" \
|
||||||
|
| grep -oiE 'href="[^"]+"' | head -1 | sed 's/href="//I; s/"$//' || true)"
|
||||||
|
|
||||||
|
if [[ -n "$href" ]]; then
|
||||||
|
case "$href" in
|
||||||
|
http*) ;;
|
||||||
|
/*) href="$origin$href" ;;
|
||||||
|
*) href="$origin/$href" ;;
|
||||||
|
esac
|
||||||
|
curl -fsSL --max-time 10 -o "$target" "$href" 2>/dev/null && { printf '%s' "$slug"; return 0; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -fsSL --max-time 10 -o "$target" "$origin/apple-touch-icon.png" 2>/dev/null \
|
||||||
|
&& { printf '%s' "$slug"; return 0; }
|
||||||
|
|
||||||
|
local host
|
||||||
|
host="$(sed -E 's#^https?://([^/]+).*#\1#' <<<"$url")"
|
||||||
|
curl -fsSL --max-time 10 -o "$target" \
|
||||||
|
"https://www.google.com/s2/favicons?sz=256&domain=$host" 2>/dev/null \
|
||||||
|
&& { printf '%s' "$slug"; return 0; }
|
||||||
|
|
||||||
|
rm -f "$target"
|
||||||
|
printf 'applications-internet'
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_install() {
|
||||||
|
local url="${1:-}" name="${2:-}"
|
||||||
|
[[ -n "$url" ]] || { err 'install needs a URL'; return 2; }
|
||||||
|
[[ "$url" =~ ^https?:// ]] || { err 'the URL must begin with http:// or https://'; return 2; }
|
||||||
|
|
||||||
|
# Default the name from the host, so `install https://app.example.com` is
|
||||||
|
# enough for the common case.
|
||||||
|
[[ -n "$name" ]] || name="$(sed -E 's#^https?://(www\.)?([^/]+).*#\2#' <<<"$url")"
|
||||||
|
|
||||||
|
local slug; slug="$(slugify "$name")"
|
||||||
|
[[ -n "$slug" ]] || { err 'that name has no usable characters in it'; return 2; }
|
||||||
|
|
||||||
|
local browser status
|
||||||
|
browser="$(resolve_browser)"; status=$?
|
||||||
|
if (( status == 2 )); then
|
||||||
|
err 'the default browser cannot open a site as its own application.'
|
||||||
|
err 'Chromium-family browsers implement --app; Firefox does not.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[[ -n "$browser" ]] || { err 'no browser found'; return 1; }
|
||||||
|
|
||||||
|
local icon; icon="$(fetch_icon "$url" "$slug")"
|
||||||
|
|
||||||
|
mkdir -p "$APPLICATIONS"
|
||||||
|
local entry="$APPLICATIONS/$LAUNCH_MARKER-$slug.desktop"
|
||||||
|
cat >"$entry" <<ENTRY
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=$name
|
||||||
|
Comment=$url
|
||||||
|
Exec=$browser --app=$url
|
||||||
|
Icon=$icon
|
||||||
|
Terminal=false
|
||||||
|
Categories=Network;
|
||||||
|
StartupWMClass=$browser
|
||||||
|
X-Panama-WebApp=$url
|
||||||
|
ENTRY
|
||||||
|
|
||||||
|
command -v update-desktop-database >/dev/null 2>&1 \
|
||||||
|
&& update-desktop-database "$APPLICATIONS" >/dev/null 2>&1
|
||||||
|
command -v gtk-update-icon-cache >/dev/null 2>&1 \
|
||||||
|
&& gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null
|
||||||
|
|
||||||
|
printf 'Installed %s\n' "$name"
|
||||||
|
printf ' %s\n' "$entry"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_list() {
|
||||||
|
local entry name url
|
||||||
|
shopt -s nullglob
|
||||||
|
for entry in "$APPLICATIONS/$LAUNCH_MARKER-"*.desktop; do
|
||||||
|
name="$(sed -n 's/^Name=//p' "$entry" | head -1)"
|
||||||
|
url="$(sed -n 's/^X-Panama-WebApp=//p' "$entry" | head -1)"
|
||||||
|
printf '%s\t%s\n' "$name" "$url"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_remove() {
|
||||||
|
local name="${1:-}"
|
||||||
|
[[ -n "$name" ]] || { err 'remove needs a name'; return 2; }
|
||||||
|
local slug; slug="$(slugify "$name")"
|
||||||
|
local entry="$APPLICATIONS/$LAUNCH_MARKER-$slug.desktop"
|
||||||
|
|
||||||
|
# Only ever removes an entry this command installed. The prefix and the
|
||||||
|
# X-Panama-WebApp key both have to be there, so a name collision with a
|
||||||
|
# real application cannot delete it.
|
||||||
|
[[ -f "$entry" ]] || { err "no web app named '$name'"; return 1; }
|
||||||
|
grep -q '^X-Panama-WebApp=' "$entry" || { err "$entry is not a Panama web app"; return 1; }
|
||||||
|
|
||||||
|
rm -f "$entry" "$ICONS/$slug.png"
|
||||||
|
command -v update-desktop-database >/dev/null 2>&1 \
|
||||||
|
&& update-desktop-database "$APPLICATIONS" >/dev/null 2>&1
|
||||||
|
printf 'Removed %s\n' "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
install) shift; cmd_install "$@" ;;
|
||||||
|
list) shift; cmd_list "$@" ;;
|
||||||
|
remove) shift; cmd_remove "$@" ;;
|
||||||
|
-h|--help|"")
|
||||||
|
cat <<'USAGE'
|
||||||
|
usage: panama-webapp install <url> [name]
|
||||||
|
panama-webapp list
|
||||||
|
panama-webapp remove <name>
|
||||||
|
|
||||||
|
Turns a website into an application: its own icon, its own window with no
|
||||||
|
browser chrome, and its own entry in the launcher.
|
||||||
|
USAGE
|
||||||
|
;;
|
||||||
|
*) err "unknown command: $1"; exit 2 ;;
|
||||||
|
esac
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,589 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Panama's front door: the one command a fresh Fedora machine needs.
|
||||||
|
#
|
||||||
|
# Download this file from the documented commit URL, verify its documented
|
||||||
|
# SHA-256, then pass both immutable values as PANAMA_BOOT_REVISION and
|
||||||
|
# PANAMA_BOOT_SHA256 when invoking it.
|
||||||
|
#
|
||||||
|
# Deliberately dumb, because a copy of this script leaves the repository the
|
||||||
|
# moment somebody curls it -- nothing here can be fixed by re-running
|
||||||
|
# ./install, so there is as little here as possible: get git, get the clone,
|
||||||
|
# hand off. Everything with judgment in it lives in `install`, which is also
|
||||||
|
# where re-runs and upgrades already work.
|
||||||
|
#
|
||||||
|
# The one exception to dumb is the root path below, which cannot live in
|
||||||
|
# `install`: a fresh VPS hands you a root login and nothing else, and the user
|
||||||
|
# that `install` needs to exist is exactly what has not been created yet.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ ! "${PANAMA_BOOT_REVISION:-}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_REVISION must be a full lowercase commit" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! "${PANAMA_BOOT_SHA256:-}" =~ ^[0-9a-f]{64}$ ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_SHA256 must be a lowercase SHA-256" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
actual_boot_sha="$(sha256sum "${BASH_SOURCE[0]}" | cut -d' ' -f1)"
|
||||||
|
if [[ "$actual_boot_sha" != "$PANAMA_BOOT_SHA256" ]]; then
|
||||||
|
echo "boot: downloaded boot file does not match PANAMA_BOOT_SHA256" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_URL="https://git.gbrown.org/gib/Panama.git"
|
||||||
|
PANAMA_PATH="${PANAMA_PATH:-$HOME/.local/share/Panama}"
|
||||||
|
export PANAMA_PATH
|
||||||
|
BOOTSTRAP_USER=""
|
||||||
|
|
||||||
|
checkout_command() {
|
||||||
|
if [[ -n "$BOOTSTRAP_USER" ]]; then
|
||||||
|
runuser -u "$BOOTSTRAP_USER" -- "$@"
|
||||||
|
else
|
||||||
|
"$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Git's index hints are performance promises, not trust evidence. In
|
||||||
|
# particular, assume-unchanged and skip-worktree can make porcelain status
|
||||||
|
# report a clean checkout whose files no longer match HEAD. Compare every
|
||||||
|
# tracked blob and Git mode with the verified commit before handing control to
|
||||||
|
# any file in the worktree.
|
||||||
|
checkout_matches_verified_commit() (
|
||||||
|
local checkout="$1" listing="" entry metadata mode type expected path actual
|
||||||
|
local link_target_with_sentinel link_target
|
||||||
|
|
||||||
|
trap '[[ -z "$listing" ]] || rm -f -- "$listing"' EXIT
|
||||||
|
trap 'exit 130' INT
|
||||||
|
trap 'exit 143' TERM
|
||||||
|
listing="$(mktemp -u -t panama-boot-tree.XXXXXX)" || exit 1
|
||||||
|
umask 077
|
||||||
|
if ! (set -o noclobber; : >"$listing") 2>/dev/null; then
|
||||||
|
listing=""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
checkout_command git -C "$checkout" ls-tree -rz --full-tree \
|
||||||
|
"$PANAMA_BOOT_REVISION" >"$listing" || exit 1
|
||||||
|
while IFS= read -r -d '' entry; do
|
||||||
|
[[ "$entry" == *$'\t'* ]] || exit 1
|
||||||
|
metadata="${entry%%$'\t'*}"
|
||||||
|
path="${entry#*$'\t'}"
|
||||||
|
read -r mode type expected <<<"$metadata"
|
||||||
|
[[ "$type" == blob && -n "$path" && "$path" != /* ]] || exit 1
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
100644) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
|
||||||
|
&& ! -x "$checkout/$path" ]] || exit 1 ;;
|
||||||
|
100755) [[ -f "$checkout/$path" && ! -L "$checkout/$path" \
|
||||||
|
&& -x "$checkout/$path" ]] || exit 1 ;;
|
||||||
|
120000)
|
||||||
|
[[ -L "$checkout/$path" ]] || exit 1
|
||||||
|
# hash-object given a pathname follows a symlink. Git's 120000 blob is
|
||||||
|
# the link text itself, including any trailing newlines, so preserve
|
||||||
|
# those bytes with a sentinel and hash stdin instead.
|
||||||
|
link_target_with_sentinel="$(
|
||||||
|
readlink -n -- "$checkout/$path" && printf .
|
||||||
|
)" || exit 1
|
||||||
|
[[ "$link_target_with_sentinel" == *. ]] || exit 1
|
||||||
|
link_target="${link_target_with_sentinel%.}"
|
||||||
|
actual="$(
|
||||||
|
printf '%s' "$link_target" \
|
||||||
|
| checkout_command git -C "$checkout" hash-object --stdin
|
||||||
|
)" || exit 1
|
||||||
|
[[ "$actual" == "$expected" ]] || exit 1
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
actual="$(checkout_command git -C "$checkout" hash-object --no-filters -- "$path")" \
|
||||||
|
|| exit 1
|
||||||
|
[[ "$actual" == "$expected" ]] || exit 1
|
||||||
|
done <"$listing"
|
||||||
|
)
|
||||||
|
|
||||||
|
prepare_panama_checkout() {
|
||||||
|
local checkout="$1" actual_head checkout_status
|
||||||
|
|
||||||
|
if [[ -e "$checkout" && ! -d "$checkout/.git" ]]; then
|
||||||
|
printf 'boot: %s exists but is not a Panama Git checkout\n' "$checkout" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$checkout/.git" ]]; then
|
||||||
|
checkout_status="$(checkout_command git -C "$checkout" status --porcelain)" || {
|
||||||
|
printf 'boot: could not inspect the existing checkout at %s\n' "$checkout" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if [[ -n "$checkout_status" ]]; then
|
||||||
|
printf 'boot: existing checkout at %s has local changes\n' "$checkout" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Panama is already cloned at $checkout; fetching the verified revision"
|
||||||
|
checkout_command git -C "$checkout" fetch origin "$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
if ! checkout_command git -C "$checkout" merge-base --is-ancestor \
|
||||||
|
HEAD "$PANAMA_BOOT_REVISION"; then
|
||||||
|
echo "boot: existing checkout cannot fast-forward to the verified revision" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
checkout_command git -C "$checkout" merge --ff-only "$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
else
|
||||||
|
checkout_command mkdir -p "$(dirname "$checkout")" || return 1
|
||||||
|
checkout_command git init "$checkout" || return 1
|
||||||
|
checkout_command git -C "$checkout" remote add origin "$REPO_URL" || return 1
|
||||||
|
checkout_command git -C "$checkout" fetch --depth=1 origin \
|
||||||
|
"$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
checkout_command git -C "$checkout" checkout --detach \
|
||||||
|
"$PANAMA_BOOT_REVISION" || return 1
|
||||||
|
|
||||||
|
actual_head="$(checkout_command git -C "$checkout" rev-parse 'HEAD^{commit}')" || return 1
|
||||||
|
if [[ "$actual_head" != "$PANAMA_BOOT_REVISION" ]]; then
|
||||||
|
echo "boot: fetched checkout does not match PANAMA_BOOT_REVISION" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
checkout_command git -C "$checkout" checkout -b main || return 1
|
||||||
|
checkout_command git -C "$checkout" config branch.main.remote origin || return 1
|
||||||
|
checkout_command git -C "$checkout" config branch.main.merge refs/heads/main || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual_head="$(checkout_command git -C "$checkout" rev-parse 'HEAD^{commit}')" || return 1
|
||||||
|
if [[ "$actual_head" != "$PANAMA_BOOT_REVISION" ]]; then
|
||||||
|
echo "boot: checkout HEAD does not match PANAMA_BOOT_REVISION" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
SERVER=0
|
||||||
|
INSTALL_ARGS=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--server) SERVER=1; INSTALL_ARGS+=(--server) ;;
|
||||||
|
*)
|
||||||
|
printf 'boot: unknown argument: %s\n' "$arg" >&2
|
||||||
|
printf 'usage: boot [--server]\n' >&2
|
||||||
|
exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Keep the worktree comparison at the last possible boundary. Checkout
|
||||||
|
# preparation may invoke several commands and return to the caller; performing
|
||||||
|
# the byte/mode/link check here ensures a change in that interval is rejected
|
||||||
|
# before any tracked file is executed.
|
||||||
|
verified_install_handoff() {
|
||||||
|
local use_tty="$1"
|
||||||
|
if ! checkout_matches_verified_commit "$PANAMA_PATH"; then
|
||||||
|
echo "boot: checkout files do not match PANAMA_BOOT_REVISION" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$BOOTSTRAP_USER" ]]; then
|
||||||
|
if (( use_tty )); then
|
||||||
|
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||||
|
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||||
|
fi
|
||||||
|
exec runuser -u "$BOOTSTRAP_USER" -- env PANAMA_PATH="$PANAMA_PATH" \
|
||||||
|
"$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||||
|
fi
|
||||||
|
if (( use_tty )); then
|
||||||
|
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"} </dev/tty
|
||||||
|
fi
|
||||||
|
exec "$PANAMA_PATH/install" ${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# The public bootstrap contract runs this branch as an ordinary user with a
|
||||||
|
# stubbed root identity. Keep its filesystem adapter unavailable to a real root
|
||||||
|
# shell so it cannot redirect a real installation by accident.
|
||||||
|
BOOT_ROOT="${PANAMA_BOOT_FIXTURE_ROOT:-}"
|
||||||
|
if [[ -n "$BOOT_ROOT" && "$EUID" -eq 0 ]]; then
|
||||||
|
echo "boot: PANAMA_BOOT_FIXTURE_ROOT is test-only" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
system_path() {
|
||||||
|
local path="$1"
|
||||||
|
[[ "$path" == /* ]] || return 2
|
||||||
|
printf '%s%s\n' "$BOOT_ROOT" "$path"
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_authorized_keys() {
|
||||||
|
local keys="$1" line saw_key=0
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! ssh-keygen -l -f /dev/stdin >/dev/null 2>&1 <<<"$line"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
saw_key=1
|
||||||
|
done <"$keys"
|
||||||
|
(( saw_key ))
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_authorized_keys() {
|
||||||
|
local username="$1" user_home="$2" uid ssh_dir keys
|
||||||
|
uid="$(id -u "$username")" || return 1
|
||||||
|
[[ "$uid" =~ ^[0-9]+$ && "$uid" != 0 && "$user_home" == /* ]] || return 1
|
||||||
|
ssh_dir="$user_home/.ssh"
|
||||||
|
keys="$ssh_dir/authorized_keys"
|
||||||
|
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
|
||||||
|
valid_authorized_keys "$keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_root_authorized_keys() {
|
||||||
|
local keys
|
||||||
|
keys="$(system_path /root/.ssh/authorized_keys)" || return 1
|
||||||
|
[[ -f "$keys" && ! -L "$keys" ]] || return 1
|
||||||
|
[[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1
|
||||||
|
valid_authorized_keys "$keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_ssh_unit() {
|
||||||
|
local unit
|
||||||
|
for unit in sshd.service ssh.service; do
|
||||||
|
systemctl cat "$unit" >/dev/null 2>&1 && {
|
||||||
|
printf '%s\n' "$unit"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_ssh_dropin() {
|
||||||
|
local restore
|
||||||
|
if (( ssh_had_prior )); then
|
||||||
|
[[ -n "$ssh_backup" && -f "$ssh_backup" && ! -L "$ssh_backup" ]] || return 1
|
||||||
|
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
|
||||||
|
if ! cp -a -- "$ssh_backup" "$restore"; then
|
||||||
|
remove_ssh_artifact "$restore" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! mv -f -- "$restore" "$ssh_dropin"; then
|
||||||
|
remove_ssh_artifact "$restore" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
remove_ssh_artifact "$ssh_dropin"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_ssh_transaction_traps() {
|
||||||
|
trap - EXIT INT TERM
|
||||||
|
[[ -n "${ssh_saved_exit_trap:-}" ]] && eval "$ssh_saved_exit_trap"
|
||||||
|
[[ -n "${ssh_saved_int_trap:-}" ]] && eval "$ssh_saved_int_trap"
|
||||||
|
[[ -n "${ssh_saved_term_trap:-}" ]] && eval "$ssh_saved_term_trap"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_ssh_artifact() {
|
||||||
|
local artifact="$1"
|
||||||
|
[[ -n "$artifact" && ( -e "$artifact" || -L "$artifact" ) ]] || return 0
|
||||||
|
if rm -f -- "$artifact"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf 'SSH transaction cleanup failed. Retained artifact: %s\n' "$artifact" >&2
|
||||||
|
printf ' rm -f -- %q\n' "$artifact" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
print_ssh_recovery() {
|
||||||
|
if (( ssh_had_prior )); then
|
||||||
|
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
|
||||||
|
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
|
||||||
|
else
|
||||||
|
printf 'SSH rollback needs manual recovery. No prior drop-in existed.\n' >&2
|
||||||
|
printf ' rm -f -- %q\n' "$ssh_dropin" >&2
|
||||||
|
fi
|
||||||
|
printf ' sshd -t\n' >&2
|
||||||
|
printf ' systemctl reload %s\n' "$ssh_unit" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
policy_is_no() {
|
||||||
|
local policy="$1" setting="$2"
|
||||||
|
awk -v setting="$setting" '
|
||||||
|
$1 == setting { count += 1; if ($2 != "no") bad = 1 }
|
||||||
|
END { exit count != 1 || bad }
|
||||||
|
' <<<"$policy"
|
||||||
|
}
|
||||||
|
|
||||||
|
effective_ssh_policy_is_hardened() {
|
||||||
|
local username="$1" root_policy target_policy context
|
||||||
|
context='host=localhost,addr=127.0.0.1'
|
||||||
|
root_policy="$(sshd -T -C "user=root,$context")" || return 1
|
||||||
|
policy_is_no "$root_policy" permitrootlogin || return 1
|
||||||
|
policy_is_no "$root_policy" passwordauthentication || return 1
|
||||||
|
policy_is_no "$root_policy" kbdinteractiveauthentication || return 1
|
||||||
|
|
||||||
|
target_policy="$(sshd -T -C "user=$username,$context")" || return 1
|
||||||
|
policy_is_no "$target_policy" passwordauthentication || return 1
|
||||||
|
policy_is_no "$target_policy" kbdinteractiveauthentication
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback_ssh_transaction() {
|
||||||
|
local reload_restored="$1" rollback_failed=0
|
||||||
|
restore_ssh_dropin || rollback_failed=1
|
||||||
|
sshd -t || rollback_failed=1
|
||||||
|
if (( reload_restored )); then
|
||||||
|
systemctl reload "$ssh_unit" || rollback_failed=1
|
||||||
|
fi
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
if (( rollback_failed )); then
|
||||||
|
print_ssh_recovery
|
||||||
|
else
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_ssh_transaction_exit() {
|
||||||
|
if [[ "$ssh_transaction_state" == preparing \
|
||||||
|
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
elif [[ "$ssh_transaction_state" == activating || "$ssh_transaction_state" == activated ]]; then
|
||||||
|
restore_ssh_dropin || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_ssh_transaction_signal() {
|
||||||
|
local signal_status="$1"
|
||||||
|
trap - INT TERM
|
||||||
|
if [[ "$ssh_transaction_state" == preparing \
|
||||||
|
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
else
|
||||||
|
rollback_ssh_transaction 1 || true
|
||||||
|
fi
|
||||||
|
exit "$signal_status"
|
||||||
|
}
|
||||||
|
|
||||||
|
harden_server_ssh() {
|
||||||
|
local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit
|
||||||
|
local ssh_candidate="" ssh_backup="" ssh_had_prior=0
|
||||||
|
local ssh_transaction_state=""
|
||||||
|
local ssh_saved_exit_trap ssh_saved_int_trap ssh_saved_term_trap
|
||||||
|
sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1
|
||||||
|
ssh_dropin="$sshd_dir/00-panama.conf"
|
||||||
|
|
||||||
|
if [[ -L "$ssh_dropin" || ( -e "$ssh_dropin" && ! -f "$ssh_dropin" ) ]]; then
|
||||||
|
printf 'SSH hardening unavailable: %s is not a regular file\n' "$ssh_dropin" >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ssh_unit="$(detect_ssh_unit)"; then
|
||||||
|
echo "SSH hardening unavailable: neither sshd.service nor ssh.service is installed" >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Harden sshd (disable root, password, and keyboard-interactive authentication)? [Y/n]: '
|
||||||
|
read -r harden </dev/tty || harden=""
|
||||||
|
if [[ "$harden" =~ ^[Nn] ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_saved_exit_trap="$(trap -p EXIT)"
|
||||||
|
ssh_saved_int_trap="$(trap -p INT)"
|
||||||
|
ssh_saved_term_trap="$(trap -p TERM)"
|
||||||
|
ssh_transaction_state=preparing
|
||||||
|
trap 'handle_ssh_transaction_exit' EXIT
|
||||||
|
trap 'handle_ssh_transaction_signal 130' INT
|
||||||
|
trap 'handle_ssh_transaction_signal 143' TERM
|
||||||
|
|
||||||
|
if ! ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp)"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' >"$ssh_candidate"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e "$ssh_dropin" ]]; then
|
||||||
|
ssh_had_prior=1
|
||||||
|
if ! ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.backup)"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! cp -a -- "$ssh_dropin" "$ssh_backup"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_transaction_state=activating
|
||||||
|
if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_candidate" || true
|
||||||
|
remove_ssh_artifact "$ssh_backup" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
ssh_candidate=""
|
||||||
|
ssh_transaction_state=activated
|
||||||
|
|
||||||
|
if ! sshd -t || ! effective_ssh_policy_is_hardened "$username"; then
|
||||||
|
rollback_ssh_transaction 0 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! systemctl reload "$ssh_unit"; then
|
||||||
|
rollback_ssh_transaction 1 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_transaction_state=""
|
||||||
|
restore_ssh_transaction_traps
|
||||||
|
remove_ssh_artifact "$ssh_backup" || return 1
|
||||||
|
echo "Wrote $ssh_dropin; make sure your key works before logging out."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Panama assumes Fedora's repositories and package names.
|
||||||
|
if ! grep -qi '^ID=fedora' /etc/os-release 2>/dev/null; then
|
||||||
|
echo "This looks like something other than Fedora; Panama only supports Fedora." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Root ─────────────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# On a desktop, root is a mistake: the clone and every dotfile would land in
|
||||||
|
# root's home and configure the wrong user. On a fresh VPS it is the starting
|
||||||
|
# condition -- Hetzner hands over a root login and nothing else -- so with
|
||||||
|
# --server this walks the machine from that to a normal Panama install: a
|
||||||
|
# user with sudo, keys, an optionally hardened sshd, and `install --server`
|
||||||
|
# running as that user. Every step checks before acting, because the machine
|
||||||
|
# may be anywhere along this path already: a user half-created by hand, keys
|
||||||
|
# already copied, sshd already locked down.
|
||||||
|
if [[ "$(id -u)" -eq 0 ]]; then
|
||||||
|
if (( ! SERVER )); then
|
||||||
|
echo "Run this as your own user, not root: the install configures YOUR desktop." >&2
|
||||||
|
echo "Setting up a fresh server from its root login is: boot --server" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Everything here asks, and a root shell from `bash <(curl ...)` can have
|
||||||
|
# the pipe as stdin, so every prompt reads the terminal explicitly.
|
||||||
|
if ! (exec </dev/tty) 2>/dev/null; then
|
||||||
|
echo "No terminal to ask on; run this from an interactive root shell." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Username for this server [gib]: '
|
||||||
|
read -r username </dev/tty || username=""
|
||||||
|
username="${username:-gib}"
|
||||||
|
|
||||||
|
if id -u "$username" >/dev/null 2>&1; then
|
||||||
|
echo "User $username already exists"
|
||||||
|
# wheel is what makes sudo work on Fedora; a user created by hand may not
|
||||||
|
# have it, and everything after this depends on it.
|
||||||
|
id -nG "$username" | grep -qw wheel || usermod -aG wheel "$username"
|
||||||
|
else
|
||||||
|
echo "Creating $username with sudo (wheel)"
|
||||||
|
useradd -m -G wheel "$username"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# useradd leaves the account locked, and sudo asks for this password -- a
|
||||||
|
# user who cannot sudo is a user the install cannot run as.
|
||||||
|
if ! passwd -S "$username" 2>/dev/null | awk '{exit $2 != "PS" && $2 != "P"}'; then
|
||||||
|
echo "Set a password for $username (sudo will ask for it):"
|
||||||
|
passwd "$username" </dev/tty
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Do not close root/password access until the account's key is an exact,
|
||||||
|
# usable login path. The fixture adapter resolves these logical system paths
|
||||||
|
# beneath a temporary root; ordinary execution receives the original paths.
|
||||||
|
logical_user_home="$(getent passwd "$username" | cut -d: -f6)"
|
||||||
|
user_home=""
|
||||||
|
if [[ "$logical_user_home" == /* ]]; then
|
||||||
|
user_home="$(system_path "$logical_user_home")" || true
|
||||||
|
fi
|
||||||
|
bootstrap_home="$user_home"
|
||||||
|
if [[ -z "$bootstrap_home" ]]; then
|
||||||
|
bootstrap_home="$(system_path "/home/$username")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
user_ssh_dir="$user_home/.ssh"
|
||||||
|
user_keys="$user_ssh_dir/authorized_keys"
|
||||||
|
if [[ -n "$user_home" && ! -e "$user_keys" && ! -L "$user_keys" \
|
||||||
|
&& ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
|
||||||
|
copy_root_key=0
|
||||||
|
if [[ ! -e "$user_ssh_dir" ]]; then
|
||||||
|
copy_root_key=1
|
||||||
|
elif [[ ! -d "$user_ssh_dir" \
|
||||||
|
|| "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then
|
||||||
|
echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2
|
||||||
|
else
|
||||||
|
copy_root_key=1
|
||||||
|
fi
|
||||||
|
if (( copy_root_key )); then
|
||||||
|
echo "Copying root's authorized_keys to $username"
|
||||||
|
root_keys="$(system_path /root/.ssh/authorized_keys)"
|
||||||
|
if ! runuser -u "$username" -- install -d -m 0700 -- "$user_ssh_dir" \
|
||||||
|
|| ! runuser -u "$username" -- install -m 0600 -- /dev/stdin "$user_keys" \
|
||||||
|
<"$root_keys"; then
|
||||||
|
echo "SSH hardening unavailable: could not install root's key for $username" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if safe_authorized_keys "$username" "$user_home"; then
|
||||||
|
harden_status=0
|
||||||
|
harden_server_ssh "$username" "$user_home" || harden_status=$?
|
||||||
|
if (( harden_status != 0 && harden_status != 2 )); then
|
||||||
|
echo "SSH hardening failed; stopping before install handoff." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "SSH hardening unavailable: $username has no safe authorized_keys" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "Installing git, which the clone needs"
|
||||||
|
dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create or advance the checkout as the target user. A root-owned .git in a
|
||||||
|
# user's home would break every later update.
|
||||||
|
PANAMA_PATH="$bootstrap_home/.local/share/Panama"
|
||||||
|
BOOTSTRAP_USER="$username"
|
||||||
|
prepare_panama_checkout "$PANAMA_PATH"
|
||||||
|
|
||||||
|
echo "Handing off to install as $username"
|
||||||
|
verified_install_handoff 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# git is the one dependency the clone itself needs. Everything else -- gum
|
||||||
|
# included -- is bootstrapped by `install`.
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "Installing git, which the clone needs"
|
||||||
|
sudo dnf install -y --repo=fedora --repo=updates --from-repo=fedora,updates git
|
||||||
|
fi
|
||||||
|
|
||||||
|
prepare_panama_checkout "$PANAMA_PATH"
|
||||||
|
|
||||||
|
# A shell invoked from automation can have a pipe as stdin, while the first
|
||||||
|
# thing install runs is the interview, which has to be able to ask.
|
||||||
|
# Reattach the terminal when there is one; without one the interview will say
|
||||||
|
# so itself.
|
||||||
|
# The probe actually opens /dev/tty rather than testing -r: a process with no
|
||||||
|
# controlling terminal passes -r and then fails the redirect.
|
||||||
|
handoff_tty=0
|
||||||
|
if [[ ! -t 0 ]] && (exec </dev/tty) 2>/dev/null; then
|
||||||
|
handoff_tty=1
|
||||||
|
fi
|
||||||
|
verified_install_handoff "$handoff_tty"
|
||||||
+18
-3
@@ -3,13 +3,26 @@
|
|||||||
# Aliases I like
|
# Aliases I like
|
||||||
alias :q="exit"
|
alias :q="exit"
|
||||||
alias :wq="exit"
|
alias :wq="exit"
|
||||||
alias startsunshine="systemctl --user restart sunshine.service"
|
|
||||||
alias sourcerc="source ~/.bashrc"
|
alias sourcerc="source ~/.bashrc"
|
||||||
alias c="clear"
|
alias c="clear"
|
||||||
alias shutdown="systemctl poweroff"
|
alias shutdown="systemctl poweroff"
|
||||||
alias update-grub="sudo grub-mkconfig -o /etc/grub2-efi.cfg"
|
# The config target differs by firmware: EFI machines regenerate the EFI
|
||||||
|
# config, BIOS machines /boot/grub2/grub.cfg -- writing the EFI path on a BIOS
|
||||||
|
# machine updates a file nothing boots from.
|
||||||
|
update-grub() {
|
||||||
|
if [ -d /sys/firmware/efi ]; then
|
||||||
|
sudo grub2-mkconfig -o /etc/grub2-efi.cfg
|
||||||
|
else
|
||||||
|
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
|
||||||
|
fi
|
||||||
|
}
|
||||||
alias nvidia-smi-docker='sudo docker run --rm --gpus all --privileged nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi'
|
alias nvidia-smi-docker='sudo docker run --rm --gpus all --privileged nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi'
|
||||||
alias ncconnect='sudo docker exec -u www-data -it nextcloud-aio-nextcloud bash'
|
alias ncconnect='sudo docker exec -u www-data -it nextcloud-aio-nextcloud bash'
|
||||||
|
alias avante='nvim -c "lua vim.defer_fn(function()require(\"avante.api\").zen_mode()end, 100)"'
|
||||||
|
alias nlc='nvim leetcode.nvim'
|
||||||
|
alias clauded='claude --allow-dangerously-skip-permissions'
|
||||||
|
alias claudedr='claude --allow-dangerously-skip-permissions --resume'
|
||||||
|
alias commit-repo='git add -A && git commit -m "Update stuff" && git push'
|
||||||
|
|
||||||
# Docker Shortcuts
|
# Docker Shortcuts
|
||||||
alias docker-up='sudo docker compose up -d'
|
alias docker-up='sudo docker compose up -d'
|
||||||
@@ -27,7 +40,8 @@ alias ls='eza -lh --group-directories-first --icons'
|
|||||||
alias lsa='ls -a'
|
alias lsa='ls -a'
|
||||||
alias lt='eza --tree --level=2 --long --icons --git'
|
alias lt='eza --tree --level=2 --long --icons --git'
|
||||||
alias lta='lt -a'
|
alias lta='lt -a'
|
||||||
alias ff="fzf --preview 'batcat --style=numbers --color=always {}'"
|
# Fedora's bat installs /usr/bin/bat; batcat is the Debian name.
|
||||||
|
alias ff="fzf --preview 'bat --style=numbers --color=always {}'"
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
alias ..='cd ..'
|
alias ..='cd ..'
|
||||||
@@ -35,6 +49,7 @@ alias ...='cd ../..'
|
|||||||
alias ....='cd ../../..'
|
alias ....='cd ../../..'
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
|
alias vim='vimx'
|
||||||
alias n='nvim'
|
alias n='nvim'
|
||||||
alias n.="nvim ."
|
alias n.="nvim ."
|
||||||
alias ns="nvim src"
|
alias ns="nvim src"
|
||||||
|
|||||||
+55
-7
@@ -1,5 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Personal environment -- API keys, tokens -- lives OUTSIDE the checkout,
|
||||||
|
# where agents, backup tools and `panama update` walk, and is kept owner-only
|
||||||
|
# every time it is read: a secrets file that drifts to 644 is quietly
|
||||||
|
# re-tightened rather than trusted. Sourced first, because settings below
|
||||||
|
# (PANAMA_SSH_TMUX) read it. (config/bash/env, its old home inside the repo,
|
||||||
|
# is still sourced by .bashrc's glob if a machine has not been migrated yet.)
|
||||||
|
PANAMA_ENV="${XDG_CONFIG_HOME:-$HOME/.config}/panama/env"
|
||||||
|
if [ -f "$PANAMA_ENV" ]; then
|
||||||
|
[ "$(stat -c %a "$PANAMA_ENV" 2>/dev/null)" = "600" ] || chmod 600 "$PANAMA_ENV"
|
||||||
|
. "$PANAMA_ENV"
|
||||||
|
fi
|
||||||
|
unset PANAMA_ENV
|
||||||
|
|
||||||
# Editor used by CLI
|
# Editor used by CLI
|
||||||
export EDITOR="nvim"
|
export EDITOR="nvim"
|
||||||
export SUDO_EDITOR="$EDITOR"
|
export SUDO_EDITOR="$EDITOR"
|
||||||
@@ -16,16 +29,51 @@ export ANDROID_SDK_HOME="$HOME/.local/share/Android"
|
|||||||
export GOPATH="$HOME/.local/share/go"
|
export GOPATH="$HOME/.local/share/go"
|
||||||
export DOTNETPATH="$HOME/.dotnet/tools"
|
export DOTNETPATH="$HOME/.dotnet/tools"
|
||||||
|
|
||||||
# Set complete path
|
# Podman
|
||||||
export PATH="$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PANAMA_PATH/bin:$BUN_INSTALL/bin:$CARGO_PATH/bin:$PNPM_HOME:$PYENV_ROOT/bin:$HOME/.rbenv/bin:/usr/lib/ccache/bin/:$GOPATH/bin:$DOTNETPATH"
|
# export DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
|
||||||
|
|
||||||
# Nvm
|
# Set complete path
|
||||||
set -h
|
export PATH="$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PANAMA_PATH/bin:$BUN_INSTALL/bin:$CARGO_PATH/bin:$PNPM_HOME/bin:$PYENV_ROOT/bin:$HOME/.rbenv/bin:/usr/lib/ccache/bin/:$GOPATH/bin:$DOTNETPATH"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
|
||||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
# 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"
|
||||||
|
|
||||||
|
# Nvm. Guarded because the file belongs to the nvm package: before that is
|
||||||
|
# installed it does not exist, and an unconditional source means every shell on
|
||||||
|
# a fresh machine opens with an error.
|
||||||
|
[ -f /etc/profile.d/nvm.sh ] && source /etc/profile.d/nvm.sh
|
||||||
|
# Auto-switch Node version when entering a directory with .nvmrc
|
||||||
|
_nvm_auto_use() {
|
||||||
|
# Guarded on nvm actually being loaded: without this, a machine where the
|
||||||
|
# nvm profile script is absent printed "command not found" on every single
|
||||||
|
# prompt in any directory carrying a .nvmrc.
|
||||||
|
if [[ -f .nvmrc ]] && type -t nvm >/dev/null 2>&1; then
|
||||||
|
nvm use --silent
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_nvm_auto_use"
|
||||||
|
|
||||||
|
# Auto-start or attach tmux for SSH interactive shells. A deliberate Panama
|
||||||
|
# behavior (tmux is in initial-packages, and a dropped SSH session keeping its
|
||||||
|
# work is the point), but guarded: it must not replace the shell of someone
|
||||||
|
# whose machine lacks tmux, and PANAMA_SSH_TMUX=off turns it off for people
|
||||||
|
# who want a plain shell -- set it in ~/.config/panama/env.
|
||||||
|
if [[ -n "$SSH_CONNECTION" && -z "$TMUX" && $- == *i* &&
|
||||||
|
"${PANAMA_SSH_TMUX:-on}" != "off" ]] && command -v tmux >/dev/null 2>&1; then
|
||||||
|
exec tmux new-session -A -s main
|
||||||
|
fi
|
||||||
|
|
||||||
# Zoxide
|
# Zoxide
|
||||||
eval "$(zoxide init bash)"
|
eval "$(zoxide init bash)"
|
||||||
|
|
||||||
# Oh My Posh
|
# Oh My Posh
|
||||||
eval "$(oh-my-posh init bash --config $PANAMA_PATH/config/dot/ohmyposh/gib.omp.json)"
|
# Guarded for the same reason the nvm source above is: oh-my-posh is a package,
|
||||||
|
# and a shell opened before it is installed -- a stage re-run by hand, an
|
||||||
|
# install that failed partway -- would otherwise print command-not-found on
|
||||||
|
# every prompt. An unthemed prompt is a worse shell; an erroring one is a
|
||||||
|
# broken-looking machine.
|
||||||
|
if command -v oh-my-posh >/dev/null 2>&1; then
|
||||||
|
eval "$(oh-my-posh init bash --config "$PANAMA_PATH/config/dot/ohmyposh/gib.omp.json")"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Dictation's speech-to-text server, as a Quadlet.
|
||||||
|
#
|
||||||
|
# Quadlet rather than a hand-written unit wrapping `podman run`: systemd
|
||||||
|
# generates the unit from this at boot, so there is one description of the
|
||||||
|
# container rather than a unit and a command line drifting apart.
|
||||||
|
#
|
||||||
|
# Upstream's own Vulkan image, not one built here. whisper.cpp publishes it, it
|
||||||
|
# already contains whisper-server, and it is maintained by the people who write
|
||||||
|
# the thing -- a Containerfile in this repository would be a compile step on
|
||||||
|
# every machine and a build to keep working, in exchange for nothing.
|
||||||
|
#
|
||||||
|
# Vulkan rather than ROCm, which is the reason this tag and not another. ROCm's
|
||||||
|
# runtime is seven gigabytes and serves AMD alone; Vulkan compute runs on AMD,
|
||||||
|
# Intel and NVIDIA through whatever Mesa driver a machine already has. These
|
||||||
|
# machines are a mix of all three, and this image works on every one of them.
|
||||||
|
#
|
||||||
|
# Deliberately no [Install] section. whisper-server loads the model when it
|
||||||
|
# starts and holds it, so a container started at login costs half a gigabyte of
|
||||||
|
# memory in every session where nobody dictates. panama-dictate starts it on the
|
||||||
|
# first press of the key, and it stays up for the rest of the session.
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Panama dictation speech-to-text server
|
||||||
|
Documentation=https://github.com/ggml-org/whisper.cpp
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Image=ghcr.io/ggml-org/whisper.cpp:main-vulkan
|
||||||
|
|
||||||
|
# The image's own entrypoint is a shell; the server is what is wanted.
|
||||||
|
Entrypoint=/app/build/bin/whisper-server
|
||||||
|
Exec=--host 0.0.0.0 --port 8791 --model /models/ggml-small.bin --inference-path /inference
|
||||||
|
|
||||||
|
# The whole directory rather than a renderD node: the number differs between
|
||||||
|
# machines, and this file is meant to be identical on all of them.
|
||||||
|
AddDevice=/dev/dri
|
||||||
|
|
||||||
|
# The model is host state -- fetched once, kept across image updates, and shared
|
||||||
|
# with nothing else. Read-only because the server never writes to it, and :z
|
||||||
|
# relabels for SELinux, which is enforcing on Fedora.
|
||||||
|
Volume=%h/.local/share/panama/whisper:/models:ro,z
|
||||||
|
|
||||||
|
# Loopback only. This transcribes whatever it is sent, with no authentication,
|
||||||
|
# and has no business being reachable from the network.
|
||||||
|
PublishPort=127.0.0.1:8791:8791
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Loading the model takes a few seconds on a cold start; systemd should wait for
|
||||||
|
# it rather than give up, and should bring the server back if it dies mid-session.
|
||||||
|
TimeoutStartSec=180
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# A stray press of the power button should ask, not act. Fedora's logind
|
||||||
|
# default is HandlePowerKey=poweroff -- an instant, unconfirmed shutdown.
|
||||||
|
# Panama binds the key in the compositor to open its power menu instead
|
||||||
|
# (keybinds.lua), the way GNOME turns the key into a dialog, so logind is
|
||||||
|
# told to stand down. Holding the button still hard-cuts through firmware,
|
||||||
|
# which remains the escape hatch this file cannot and should not remove.
|
||||||
|
[Login]
|
||||||
|
HandlePowerKey=ignore
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Clear the mountpoint before starting, so a start that follows a crash lands
|
||||||
|
# on clean ground.
|
||||||
|
#
|
||||||
|
# This service provides the fuse mount at /run/user/$UID/doc, and bwrap binds
|
||||||
|
# doc/by-app/<id> into every flatpak sandbox it builds. No mount, no sandbox,
|
||||||
|
# no application -- all of them, not one, because they all share this.
|
||||||
|
#
|
||||||
|
# The shipped unit is Type=dbus with Restart=no, so nothing restarts it on
|
||||||
|
# failure: it comes back only when something next calls its bus name. On this
|
||||||
|
# machine that was a flatpak launching two seconds after the daemon exited
|
||||||
|
# 21, and the activation raced the dying instance's fuse state and came up
|
||||||
|
# with no mount at all. systemctl then reports active (running) -- the
|
||||||
|
# fusermount3 helper is even still there as a child -- while every new sandbox
|
||||||
|
# fails. Nothing running at the time notices, because a sandbox needs the
|
||||||
|
# mount only while it is being built, so the breakage surfaces whenever you
|
||||||
|
# next open something you had not opened yet. Here that was three days later,
|
||||||
|
# and it was the second time: the first was written off as a one-off.
|
||||||
|
#
|
||||||
|
# `-` because a clean start has nothing to unmount and fusermount3 exits 1
|
||||||
|
# saying so, which is not a failure to start on.
|
||||||
|
#
|
||||||
|
# The unmount alone did not close the hole: the third recurrence (2026-08-26)
|
||||||
|
# started on ground the ExecStartPre had cleaned and still came up with no
|
||||||
|
# mount, sitting active-and-broken for fourteen hours until the next fresh
|
||||||
|
# flatpak launch found it. Hence ExecStartPost: the unit is not "started"
|
||||||
|
# until the mount it exists to provide actually exists. Type=dbus declares
|
||||||
|
# readiness at bus-name acquisition, which can land before the fuse thread
|
||||||
|
# has the mount up, so this polls rather than checks once. If five seconds
|
||||||
|
# pass without a mount, the start fails -- loudly, in the journal, as failed
|
||||||
|
# -- and the next bus activation retries from the ExecStartPre cleanup
|
||||||
|
# instead of every sandbox build failing against a unit that claims to be
|
||||||
|
# fine.
|
||||||
|
[Service]
|
||||||
|
ExecStartPre=-/usr/bin/fusermount3 -u /run/user/%U/doc
|
||||||
|
ExecStartPost=/usr/bin/timeout 5 /bin/sh -c 'until /usr/bin/mountpoint -q /run/user/%U/doc; do /usr/bin/sleep 0.2; done'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# External monitor brightness (DDC/CI) on GPUs that are not "VGA".
|
||||||
|
#
|
||||||
|
# ddcutil ships 60-ddcutil-i2c.rules, which grants the seated user access to
|
||||||
|
# the GPU's i2c buses -- but only when the GPU's PCI class is 0x030000 (VGA
|
||||||
|
# compatible controller). An AMD iGPU that is not the primary boot display
|
||||||
|
# enumerates as 0x038000 (Display controller) instead, so every DDC bus it
|
||||||
|
# exposes stays root-only and the Brightness service reads EACCES. Same grant,
|
||||||
|
# broadened to the class that hardware actually reports.
|
||||||
|
SUBSYSTEM=="i2c-dev", KERNEL=="i2c-[0-9]*", ATTRS{class}=="0x038000", TAG+="uaccess"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Stable, colon-free symlinks for the DRM devices.
|
||||||
|
#
|
||||||
|
# Why this exists: Hyprland's AQ_DRM_DEVICES selects which GPU renders, but
|
||||||
|
# - /dev/dri/cardN numbering is assigned at boot and is not stable, and
|
||||||
|
# - /dev/dri/by-path/ names contain colons, which AQ_DRM_DEVICES itself uses
|
||||||
|
# as its separator, so they cannot be used.
|
||||||
|
#
|
||||||
|
# These rules are keyed to specific PCI addresses and simply do not match on
|
||||||
|
# other machines; hypr/env.lua checks that the symlink exists before setting
|
||||||
|
# AQ_DRM_DEVICES, so a non-match is harmless.
|
||||||
|
#
|
||||||
|
# Apply without rebooting: sudo udevadm control --reload && sudo udevadm trigger
|
||||||
|
|
||||||
|
# desktop-gib: Radeon RX 7800 XT (discrete, drives DP-2)
|
||||||
|
KERNEL=="card*", KERNELS=="0000:03:00.0", SUBSYSTEM=="drm", SUBSYSTEMS=="pci", SYMLINK+="dri/amd-dgpu"
|
||||||
|
|
||||||
|
# desktop-gib: Granite Ridge integrated graphics
|
||||||
|
KERNEL=="card*", KERNELS=="0000:12:00.0", SUBSYSTEM=="drm", SUBSYSTEMS=="pci", SYMLINK+="dri/amd-igpu"
|
||||||
@@ -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,17 +1,9 @@
|
|||||||
# espanso match file
|
# espanso match file
|
||||||
# https://espanso.org/docs/
|
# https://espanso.org/docs/
|
||||||
|
# Name and email triggers (:fn, :em, ...) are NOT here: they are personal, and
|
||||||
|
# this file is shared. setup-identity seeds them into match/identity.yml --
|
||||||
|
# per-machine, gitignored, yours to edit -- from the install interview.
|
||||||
matches:
|
matches:
|
||||||
# Name
|
|
||||||
- trigger: ":fn"
|
|
||||||
replace: "Gabriel Brown"
|
|
||||||
- trigger: ":fin"
|
|
||||||
replace: "Gabriel A Brown"
|
|
||||||
# Email
|
|
||||||
- trigger: ":em"
|
|
||||||
replace: "[email protected]"
|
|
||||||
- trigger: ":empro"
|
|
||||||
replace: "[email protected]"
|
|
||||||
|
|
||||||
# Date
|
# Date
|
||||||
- trigger: ":date"
|
- trigger: ":date"
|
||||||
replace: "{{mydate}}"
|
replace: "{{mydate}}"
|
||||||
@@ -147,18 +139,6 @@ matches:
|
|||||||
- trigger: "dokcer"
|
- trigger: "dokcer"
|
||||||
replace: "docker"
|
replace: "docker"
|
||||||
|
|
||||||
- trigger: "gborwn"
|
|
||||||
replace: "gbrown"
|
|
||||||
|
|
||||||
- trigger: "gbrwon"
|
|
||||||
replace: "gbrown"
|
|
||||||
|
|
||||||
- trigger: "gbronw"
|
|
||||||
replace: "gbrown"
|
|
||||||
|
|
||||||
- trigger: "grbown"
|
|
||||||
replace: "gbrown"
|
|
||||||
|
|
||||||
- trigger: "restrat"
|
- trigger: "restrat"
|
||||||
replace: "restart"
|
replace: "restart"
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Available matches
|
# Available matches
|
||||||
| Trigger | Replace |
|
| Trigger | Replace |
|
||||||
|--------------|--------------------------------------------------------|
|
|--------------|-----------------------------------------------------------------|
|
||||||
| :block: | \```🖰\n``` |
|
| :block: | \```🖰<br>``` |
|
||||||
| :code: | \`🖰` |
|
| :code: | \`🖰` |
|
||||||
| :h1: | # |
|
| :h1: | # |
|
||||||
| :h2: | ## |
|
| :h2: | ## |
|
||||||
@@ -14,9 +14,10 @@
|
|||||||
| :strike: | \~\~🖰~~ |
|
| :strike: | \~\~🖰~~ |
|
||||||
| :url: | \[🖰]() |
|
| :url: | \[🖰]() |
|
||||||
| :image: | !\[](🖰) |
|
| :image: | !\[](🖰) |
|
||||||
| :horizontal: | ___\n |
|
| :horizontal: | ___<br> |
|
||||||
| :task: | - [ ] |
|
| :task: | - [ ] |
|
||||||
| :donetask: | - [x] |
|
| :donetask: | - [x] |
|
||||||
| :collapse: | \<details>\<summary>🖰\</summary>\n\<p>\n\n\</p>\n\</details> |
|
| :collapse: | \<details><br>\<summary>\</summary><br>🖱<br>\</details> |
|
||||||
|
| :comment: | \<\!--- 🖱 --> |
|
||||||
|
|
||||||
**Note: The 🖰 symbol is where your mouse cursor will be after the trigger and \n represents a new line.**
|
**Note**: The symbol "🖰" denotes where your mouse cursor will be after the trigger.
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ author: "Jos\xE9 Ferreira"
|
|||||||
description: A simple package to make writing Markdown easier
|
description: A simple package to make writing Markdown easier
|
||||||
name: markdown-shortcuts
|
name: markdown-shortcuts
|
||||||
title: Markdown shortcuts
|
title: Markdown shortcuts
|
||||||
version: 0.1.0
|
version: 0.2.0
|
||||||
homepage: "https://github.com/jpmvferreira/espanso-mega-pack"
|
homepage: "https://github.com/jpmvferreira/espanso-mega-pack"
|
||||||
tags: ["markdown", "development", "writing"]
|
tags: ["markdown", "development", "writing"]
|
||||||
@@ -1,65 +1,83 @@
|
|||||||
name: markdown-shortcuts
|
|
||||||
parent: default
|
|
||||||
|
|
||||||
matches:
|
matches:
|
||||||
- triggers: [":block:", ":mb:"]
|
- trigger: ":block:"
|
||||||
|
label: "Markdown - Code block"
|
||||||
replace: |-
|
replace: |-
|
||||||
```$|$
|
```$|$
|
||||||
```
|
```
|
||||||
force_clipboard: true
|
force_clipboard: true
|
||||||
|
|
||||||
- triggers: [":code:", ":mc:"]
|
- trigger: ":code:"
|
||||||
|
label: "Markdown - Inline code"
|
||||||
replace: |-
|
replace: |-
|
||||||
`$|$`
|
`$|$`
|
||||||
force_clipboard: true
|
force_clipboard: true
|
||||||
|
|
||||||
- trigger: ":h1:"
|
- trigger: ":h1:"
|
||||||
|
label: "Markdown - Header1"
|
||||||
replace: "#"
|
replace: "#"
|
||||||
|
|
||||||
- trigger: ":h2:"
|
- trigger: ":h2:"
|
||||||
|
label: "Markdown - Header2"
|
||||||
replace: "##"
|
replace: "##"
|
||||||
|
|
||||||
- trigger: ":h3:"
|
- trigger: ":h3:"
|
||||||
|
label: "Markdown - Header3"
|
||||||
replace: "###"
|
replace: "###"
|
||||||
|
|
||||||
- trigger: ":h4:"
|
- trigger: ":h4:"
|
||||||
|
label: "Markdown - Header4"
|
||||||
replace: "####"
|
replace: "####"
|
||||||
|
|
||||||
- trigger: ":h5:"
|
- trigger: ":h5:"
|
||||||
|
label: "Markdown - Header5"
|
||||||
replace: "#####"
|
replace: "#####"
|
||||||
|
|
||||||
- trigger: ":h6:"
|
- trigger: ":h6:"
|
||||||
|
label: "Markdown - Header6"
|
||||||
replace: "######"
|
replace: "######"
|
||||||
|
|
||||||
- trigger: ":bold:"
|
- trigger: ":bold:"
|
||||||
|
label: "Markdown - Bold"
|
||||||
replace: "**$|$**"
|
replace: "**$|$**"
|
||||||
|
|
||||||
- trigger: ":italic:"
|
- trigger: ":italic:"
|
||||||
|
label: "Markdown - Italic"
|
||||||
replace: "*$|$*"
|
replace: "*$|$*"
|
||||||
|
|
||||||
- trigger: ":strike:"
|
- trigger: ":strike:"
|
||||||
|
label: "Markdown - Strikethrough"
|
||||||
replace: "~~$|$~~"
|
replace: "~~$|$~~"
|
||||||
|
|
||||||
- trigger: ":url:"
|
- trigger: ":url:"
|
||||||
|
label: "Markdown - URL"
|
||||||
replace: "[$|$]()"
|
replace: "[$|$]()"
|
||||||
|
|
||||||
- triggers: [":image:", ":img:"]
|
- trigger: ":img:"
|
||||||
|
label: "Markdown - Image"
|
||||||
replace: ""
|
replace: ""
|
||||||
|
|
||||||
- triggers: [":horizontal:", ":mh:"]
|
- trigger: ":horizontal:"
|
||||||
|
label: "Markdown - Horizontal line"
|
||||||
replace: "___"
|
replace: "___"
|
||||||
|
|
||||||
- triggers: [":task:", ":mt:"]
|
- trigger: ":task:"
|
||||||
|
label: "Markdown - Task"
|
||||||
replace: "- [ ] "
|
replace: "- [ ] "
|
||||||
|
|
||||||
- triggers: [":taskdone:", ":mtd:"]
|
- trigger: ":taskdone:"
|
||||||
|
label: "Markdown - Task completed"
|
||||||
replace: "- [x] "
|
replace: "- [x] "
|
||||||
|
|
||||||
- triggers: [":collapse:", ":mcol:"]
|
- trigger: ":collapse:"
|
||||||
|
label: "Markdown - Collapse block"
|
||||||
replace: |-
|
replace: |-
|
||||||
<details>
|
<details>
|
||||||
<summary></summary>
|
<summary></summary>
|
||||||
|
|
||||||
$|$
|
$|$
|
||||||
</details>
|
</details>
|
||||||
force_clipboard: true
|
force_clipboard: true
|
||||||
|
|
||||||
|
- trigger: ":comment:"
|
||||||
|
label: "Markdown - Comment"
|
||||||
|
replace: "<!--- $|$ -->"
|
||||||
|
force_clipboard: true
|
||||||
|
|||||||
@@ -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,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": "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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,132 +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: 4px;
|
|
||||||
border-color: rgb(130,170,255);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-split-border {
|
|
||||||
border-width: 4px;
|
|
||||||
border-color: rgb(130,170,255);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-split-horizontal {
|
|
||||||
border-left-width: 0;
|
|
||||||
border-top-width: 0;
|
|
||||||
border-bottom-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-split-vertical {
|
|
||||||
border-left-width: 0;
|
|
||||||
border-top-width: 0;
|
|
||||||
border-right-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-stacked-border {
|
|
||||||
border-width: 4px;
|
|
||||||
border-color: rgb(130,170,255);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-tabbed-border {
|
|
||||||
border-width: 4px;
|
|
||||||
border-color: rgb(130,170,255);
|
|
||||||
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(130,170,255,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(130,170,255);
|
|
||||||
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: 4px;
|
|
||||||
border-color: rgb(130,170,255);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-tilepreview-tiled {
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: rgba(130,170,255,0.3);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
background-color: rgba(130,170,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-tilepreview-stacked {
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: rgba(130,170,255,0.3);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
background-color: rgba(130,170,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-tilepreview-tabbed {
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: rgba(130,170,255,0.3);
|
|
||||||
border-style: solid;
|
|
||||||
border-radius: 14px;
|
|
||||||
background-color: rgba(130,170,255,0.2);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
file://$HOME/Documents Documents
|
||||||
|
file://$HOME/Downloads Downloads
|
||||||
|
file://$HOME/Music Music
|
||||||
|
file://$HOME/Pictures Pictures
|
||||||
|
file://$HOME/Videos Videos
|
||||||
|
file://$HOME/.local/share/Panama Panama
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/* Panama regenerates the block below from the active theme's palette, and only
|
||||||
|
* the block: every rule outside the markers belongs to the vendored theme and
|
||||||
|
* is never touched. Edit the theme, not this file -- the next theme change
|
||||||
|
* overwrites whatever is between the markers. The values shipped here are Tokyo
|
||||||
|
* Moon, the default dark theme, so the file is valid before it has ever been
|
||||||
|
* rewritten. See quickshell/scripts/panama-theme-apps. */
|
||||||
|
/* PANAMA THEME BEGIN */
|
||||||
|
/* Generated from the "moon" theme by panama-theme-apps. Edit the
|
||||||
|
* theme, not this block -- the next theme change overwrites it. */
|
||||||
|
@define-color accent_bg_color #82aaff;
|
||||||
|
@define-color accent_color #65bcff;
|
||||||
|
@define-color accent_fg_color #1e2030;
|
||||||
|
@define-color destructive_bg_color #c53b53;
|
||||||
|
@define-color destructive_color #ff757f;
|
||||||
|
@define-color destructive_fg_color #c8d3f5;
|
||||||
|
@define-color success_bg_color #c3e88d;
|
||||||
|
@define-color success_color #c3e88d;
|
||||||
|
@define-color success_fg_color #1e2030;
|
||||||
|
@define-color warning_bg_color #ffc777;
|
||||||
|
@define-color warning_color #ffc777;
|
||||||
|
@define-color warning_fg_color #1e2030;
|
||||||
|
@define-color error_bg_color #c53b53;
|
||||||
|
@define-color error_color #ff757f;
|
||||||
|
@define-color error_fg_color #c8d3f5;
|
||||||
|
@define-color window_bg_color #2e2f3d;
|
||||||
|
@define-color window_fg_color #c8d3f5;
|
||||||
|
@define-color view_bg_color #222436;
|
||||||
|
@define-color view_fg_color #c8d3f5;
|
||||||
|
@define-color headerbar_bg_color #2e2f3d;
|
||||||
|
@define-color headerbar_fg_color #c8d3f5;
|
||||||
|
@define-color headerbar_border_color #3b4261;
|
||||||
|
@define-color headerbar_backdrop_color #1e2030;
|
||||||
|
@define-color headerbar_shade_color rgba(30, 32, 48, 0.08);
|
||||||
|
@define-color headerbar_darker_shade_color rgba(30, 32, 48, 0.9);
|
||||||
|
@define-color sidebar_bg_color #1e2030;
|
||||||
|
@define-color sidebar_fg_color #c8d3f5;
|
||||||
|
@define-color sidebar_backdrop_color #1e2030;
|
||||||
|
@define-color sidebar_shade_color rgba(59, 66, 97, 0.08);
|
||||||
|
@define-color sidebar_border_color rgba(59, 66, 97, 0.36);
|
||||||
|
@define-color secondary_sidebar_bg_color #1e2030;
|
||||||
|
@define-color secondary_sidebar_fg_color #c8d3f5;
|
||||||
|
@define-color secondary_sidebar_backdrop_color #1e2030;
|
||||||
|
@define-color secondary_sidebar_shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color secondary_sidebar_border_color rgba(59, 66, 97, 0.36);
|
||||||
|
@define-color card_bg_color #2f334d;
|
||||||
|
@define-color card_fg_color #c8d3f5;
|
||||||
|
@define-color card_shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color dialog_bg_color #21212f;
|
||||||
|
@define-color dialog_fg_color #c8d3f5;
|
||||||
|
@define-color popover_bg_color #21212f;
|
||||||
|
@define-color popover_fg_color #c8d3f5;
|
||||||
|
@define-color popover_shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color thumbnail_bg_color #2f334d;
|
||||||
|
@define-color thumbnail_fg_color #c8d3f5;
|
||||||
|
@define-color shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color scrollbar_outline_color rgba(30, 32, 48, 0.5);
|
||||||
|
/* PANAMA THEME END */
|
||||||
|
|
||||||
|
/* The GNOME named palette below is reference, not theme: applications ask for
|
||||||
|
* @red_3 by name and expect GNOME's red. It stays outside the block. */
|
||||||
|
@define-color blue_1 #99c1f1;
|
||||||
|
@define-color blue_2 #62a0ea;
|
||||||
|
@define-color blue_3 #3584e4;
|
||||||
|
@define-color blue_4 #1c71d8;
|
||||||
|
@define-color blue_5 #1a5fb4;
|
||||||
|
@define-color green_1 #8ff0a4;
|
||||||
|
@define-color green_2 #57e389;
|
||||||
|
@define-color green_3 #33d17a;
|
||||||
|
@define-color green_4 #2ec27e;
|
||||||
|
@define-color green_5 #26a269;
|
||||||
|
@define-color yellow_1 #f9f06b;
|
||||||
|
@define-color yellow_2 #f8e45c;
|
||||||
|
@define-color yellow_3 #f6d32d;
|
||||||
|
@define-color yellow_4 #f5c211;
|
||||||
|
@define-color yellow_5 #e5a50a;
|
||||||
|
@define-color orange_1 #ffbe6f;
|
||||||
|
@define-color orange_2 #ffa348;
|
||||||
|
@define-color orange_3 #ff7800;
|
||||||
|
@define-color orange_4 #e66100;
|
||||||
|
@define-color orange_5 #c64600;
|
||||||
|
@define-color red_1 #f66151;
|
||||||
|
@define-color red_2 #ed333b;
|
||||||
|
@define-color red_3 #e01b24;
|
||||||
|
@define-color red_4 #c01c28;
|
||||||
|
@define-color red_5 #a51d2d;
|
||||||
|
@define-color purple_1 #dc8add;
|
||||||
|
@define-color purple_2 #c061cb;
|
||||||
|
@define-color purple_3 #9141ac;
|
||||||
|
@define-color purple_4 #813d9c;
|
||||||
|
@define-color purple_5 #613583;
|
||||||
|
@define-color brown_1 #cdab8f;
|
||||||
|
@define-color brown_2 #b5835a;
|
||||||
|
@define-color brown_3 #986a44;
|
||||||
|
@define-color brown_4 #865e3c;
|
||||||
|
@define-color brown_5 #63452c;
|
||||||
|
@define-color light_1 #ffffff;
|
||||||
|
@define-color light_2 #f6f5f4;
|
||||||
|
@define-color light_3 #deddda;
|
||||||
|
@define-color light_4 #c0bfbc;
|
||||||
|
@define-color light_5 #9a9996;
|
||||||
|
@define-color dark_1 #77767b;
|
||||||
|
@define-color dark_2 #5e5c64;
|
||||||
|
@define-color dark_3 #3d3846;
|
||||||
|
@define-color dark_4 #241f31;
|
||||||
|
@define-color dark_5 #000000;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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 ...`.
|
||||||
|
#
|
||||||
|
# Under GNOME, gnome-settings-daemon publishes settings over XSETTINGS and this
|
||||||
|
# file is effectively ignored. Under Hyprland there is no settings daemon, so
|
||||||
|
# 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=@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=@PREFER_DARK@
|
||||||
|
|
||||||
|
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
||||||
|
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
|
||||||
|
gtk-button-images=0
|
||||||
|
gtk-menu-images=0
|
||||||
|
gtk-enable-event-sounds=1
|
||||||
|
gtk-enable-input-feedback-sounds=0
|
||||||
|
gtk-xft-antialias=1
|
||||||
|
gtk-xft-hinting=1
|
||||||
|
gtk-xft-hintstyle=hintslight
|
||||||
|
gtk-xft-rgba=rgb
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
/* GTK NAMED COLORS ---------------- use responsibly! */
|
||||||
|
/* Panama regenerates the block below from the active theme's palette, and only
|
||||||
|
* the block: every rule outside the markers belongs to the vendored theme and
|
||||||
|
* is never touched. Edit the theme, not this file -- the next theme change
|
||||||
|
* overwrites whatever is between the markers. The values shipped here are Tokyo
|
||||||
|
* Moon, the default dark theme, so the file is valid before it has ever been
|
||||||
|
* rewritten. See quickshell/scripts/panama-theme-apps. */
|
||||||
|
/* PANAMA THEME BEGIN */
|
||||||
|
/* Generated from the "moon" theme by panama-theme-apps. Edit the
|
||||||
|
* theme, not this block -- the next theme change overwrites it. */
|
||||||
|
@define-color accent_bg_color #82aaff;
|
||||||
|
@define-color accent_color #65bcff;
|
||||||
|
@define-color accent_fg_color #1e2030;
|
||||||
|
@define-color destructive_bg_color #c53b53;
|
||||||
|
@define-color destructive_color #ff757f;
|
||||||
|
@define-color destructive_fg_color #c8d3f5;
|
||||||
|
@define-color success_bg_color #c3e88d;
|
||||||
|
@define-color success_color #c3e88d;
|
||||||
|
@define-color success_fg_color #1e2030;
|
||||||
|
@define-color warning_bg_color #ffc777;
|
||||||
|
@define-color warning_color #ffc777;
|
||||||
|
@define-color warning_fg_color #1e2030;
|
||||||
|
@define-color error_bg_color #c53b53;
|
||||||
|
@define-color error_color #ff757f;
|
||||||
|
@define-color error_fg_color #c8d3f5;
|
||||||
|
@define-color window_bg_color #2e2f3d;
|
||||||
|
@define-color window_fg_color #c8d3f5;
|
||||||
|
@define-color view_bg_color #222436;
|
||||||
|
@define-color view_fg_color #c8d3f5;
|
||||||
|
@define-color headerbar_bg_color #2e2f3d;
|
||||||
|
@define-color headerbar_fg_color #c8d3f5;
|
||||||
|
@define-color headerbar_border_color #3b4261;
|
||||||
|
@define-color headerbar_backdrop_color #1e2030;
|
||||||
|
@define-color headerbar_shade_color rgba(30, 32, 48, 0.08);
|
||||||
|
@define-color headerbar_darker_shade_color rgba(30, 32, 48, 0.9);
|
||||||
|
@define-color sidebar_bg_color #1e2030;
|
||||||
|
@define-color sidebar_fg_color #c8d3f5;
|
||||||
|
@define-color sidebar_backdrop_color #1e2030;
|
||||||
|
@define-color sidebar_shade_color rgba(59, 66, 97, 0.08);
|
||||||
|
@define-color sidebar_border_color rgba(59, 66, 97, 0.36);
|
||||||
|
@define-color secondary_sidebar_bg_color #1e2030;
|
||||||
|
@define-color secondary_sidebar_fg_color #c8d3f5;
|
||||||
|
@define-color secondary_sidebar_backdrop_color #1e2030;
|
||||||
|
@define-color secondary_sidebar_shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color secondary_sidebar_border_color rgba(59, 66, 97, 0.36);
|
||||||
|
@define-color card_bg_color #2f334d;
|
||||||
|
@define-color card_fg_color #c8d3f5;
|
||||||
|
@define-color card_shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color dialog_bg_color #21212f;
|
||||||
|
@define-color dialog_fg_color #c8d3f5;
|
||||||
|
@define-color popover_bg_color #21212f;
|
||||||
|
@define-color popover_fg_color #c8d3f5;
|
||||||
|
@define-color popover_shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color thumbnail_bg_color #2f334d;
|
||||||
|
@define-color thumbnail_fg_color #c8d3f5;
|
||||||
|
@define-color shade_color rgba(30, 32, 48, 0.25);
|
||||||
|
@define-color scrollbar_outline_color rgba(30, 32, 48, 0.5);
|
||||||
|
/* PANAMA THEME END */
|
||||||
|
|
||||||
|
/* The GNOME named palette below is reference, not theme: applications ask for
|
||||||
|
* @red_3 by name and expect GNOME's red. It stays outside the block. */
|
||||||
|
@define-color blue_1 #99c1f1;
|
||||||
|
@define-color blue_2 #62a0ea;
|
||||||
|
@define-color blue_3 #3584e4;
|
||||||
|
@define-color blue_4 #1c71d8;
|
||||||
|
@define-color blue_5 #1a5fb4;
|
||||||
|
@define-color green_1 #8ff0a4;
|
||||||
|
@define-color green_2 #57e389;
|
||||||
|
@define-color green_3 #33d17a;
|
||||||
|
@define-color green_4 #2ec27e;
|
||||||
|
@define-color green_5 #26a269;
|
||||||
|
@define-color yellow_1 #f9f06b;
|
||||||
|
@define-color yellow_2 #f8e45c;
|
||||||
|
@define-color yellow_3 #f6d32d;
|
||||||
|
@define-color yellow_4 #f5c211;
|
||||||
|
@define-color yellow_5 #e5a50a;
|
||||||
|
@define-color orange_1 #ffbe6f;
|
||||||
|
@define-color orange_2 #ffa348;
|
||||||
|
@define-color orange_3 #ff7800;
|
||||||
|
@define-color orange_4 #e66100;
|
||||||
|
@define-color orange_5 #c64600;
|
||||||
|
@define-color red_1 #f66151;
|
||||||
|
@define-color red_2 #ed333b;
|
||||||
|
@define-color red_3 #e01b24;
|
||||||
|
@define-color red_4 #c01c28;
|
||||||
|
@define-color red_5 #a51d2d;
|
||||||
|
@define-color purple_1 #dc8add;
|
||||||
|
@define-color purple_2 #c061cb;
|
||||||
|
@define-color purple_3 #9141ac;
|
||||||
|
@define-color purple_4 #813d9c;
|
||||||
|
@define-color purple_5 #613583;
|
||||||
|
@define-color brown_1 #cdab8f;
|
||||||
|
@define-color brown_2 #b5835a;
|
||||||
|
@define-color brown_3 #986a44;
|
||||||
|
@define-color brown_4 #865e3c;
|
||||||
|
@define-color brown_5 #63452c;
|
||||||
|
@define-color light_1 #ffffff;
|
||||||
|
@define-color light_2 #f6f5f4;
|
||||||
|
@define-color light_3 #deddda;
|
||||||
|
@define-color light_4 #c0bfbc;
|
||||||
|
@define-color light_5 #9a9996;
|
||||||
|
@define-color dark_1 #77767b;
|
||||||
|
@define-color dark_2 #5e5c64;
|
||||||
|
@define-color dark_3 #3d3846;
|
||||||
|
@define-color dark_4 #241f31;
|
||||||
|
@define-color dark_5 #000000;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--standalone-color-oklab: max(l, 0.85) a b;
|
||||||
|
--accent-color: oklab(
|
||||||
|
from var(--accent-bg-color) var(--standalone-color-oklab)
|
||||||
|
);
|
||||||
|
--destructive-color: oklab(
|
||||||
|
from var(--destructive-bg-color) var(--standalone-color-oklab)
|
||||||
|
);
|
||||||
|
--success-color: oklab(
|
||||||
|
from var(--success-bg-color) var(--standalone-color-oklab)
|
||||||
|
);
|
||||||
|
--warning-color: oklab(
|
||||||
|
from var(--warning-bg-color) var(--standalone-color-oklab)
|
||||||
|
);
|
||||||
|
--error-color: oklab(
|
||||||
|
from var(--error-bg-color) var(--standalone-color-oklab)
|
||||||
|
);
|
||||||
|
--active-toggle-bg-color: rgb(255 255 255 / 20%);
|
||||||
|
--active-toggle-fg-color: #ffffff;
|
||||||
|
--overview-bg-color: #2e2f3d;
|
||||||
|
--overview-fg-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@import '../gtk-3.0/libadwaita.css';
|
||||||
|
@import '../gtk-3.0/libadwaita-tweaks.css';
|
||||||
|
|
||||||
|
* {
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window {
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maximize window {
|
||||||
|
border-radius: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.maximize {
|
||||||
|
border-radius: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
/* More customizations */
|
||||||
|
/* GTK3 */
|
||||||
|
button.titlebutton {
|
||||||
|
color: transparent;
|
||||||
|
min-width: 12px;
|
||||||
|
min-height: 12px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 2px;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.titlebutton:backdrop {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************
|
||||||
|
* Title buttons *
|
||||||
|
*****************/
|
||||||
|
.raven .expander-button,
|
||||||
|
.raven .raven-header:not(.top) button.image-button,
|
||||||
|
widget > box.terminal-titlebar > button.image-button,
|
||||||
|
widget > box.terminal-titlebar > button.image-button.toggle,
|
||||||
|
.terminix-session-sidebar button.tilix-sidebar-close-button,
|
||||||
|
.tilix-session-sidebar button.tilix-sidebar-close-button,
|
||||||
|
.nemo-window toolbar.primary-toolbar > toolitem > box.raised > button,
|
||||||
|
.nemo-window toolbar.primary-toolbar > toolitem > box.linked.raised > button,
|
||||||
|
.nautilus-window headerbar > revealer > button,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton,
|
||||||
|
headerbar button.titlebutton,
|
||||||
|
.csd headerbar button.titlebutton,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton,
|
||||||
|
.titlebar button.titlebutton,
|
||||||
|
.csd .titlebar button.titlebutton,
|
||||||
|
.titlebar.default-decoration button.titlebutton,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton,
|
||||||
|
row button.image-button,
|
||||||
|
check,
|
||||||
|
radio,
|
||||||
|
headerbar.default-decoration button.titlebutton,
|
||||||
|
headerbar button.suggested-action,
|
||||||
|
headerbar button.destructive-action,
|
||||||
|
headerbar button:not(.suggested-action):not(.destructive-action),
|
||||||
|
button.close,
|
||||||
|
button.circular {
|
||||||
|
border-radius: 9999px;
|
||||||
|
-gtk-outline-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
headerbar,
|
||||||
|
.titlebar {
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.close,
|
||||||
|
headerbar button.titlebutton.close,
|
||||||
|
.titlebar button.titlebutton.close {
|
||||||
|
min-width: 12px;
|
||||||
|
min-height: 12px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 2px;
|
||||||
|
color: transparent;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
background-color: #f25056;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.maximize,
|
||||||
|
headerbar button.titlebutton.maximize,
|
||||||
|
.titlebar button.titlebutton.maximize {
|
||||||
|
min-width: 12px;
|
||||||
|
min-height: 12px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 2px;
|
||||||
|
color: transparent;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
background-color: #39ea49;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.minimize,
|
||||||
|
headerbar button.titlebutton.minimize,
|
||||||
|
.titlebar button.titlebutton.minimize {
|
||||||
|
min-width: 12px;
|
||||||
|
min-height: 12px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 2px;
|
||||||
|
color: transparent;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
background-color: #fac536;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#MozillaGtkWidget.background
|
||||||
|
headerbar.titlebar.default-decoration
|
||||||
|
button.titlebutton.close:hover,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.close:hover,
|
||||||
|
headerbar button.titlebutton.close:hover,
|
||||||
|
.csd headerbar button.titlebutton.close:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:hover,
|
||||||
|
.titlebar button.titlebutton.close:hover,
|
||||||
|
.csd .titlebar button.titlebutton.close:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:hover {
|
||||||
|
padding: 2px;
|
||||||
|
margin: -2px 0;
|
||||||
|
background-color: #f25056;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#MozillaGtkWidget.background
|
||||||
|
headerbar.titlebar.default-decoration
|
||||||
|
button.titlebutton.maximize:hover,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.maximize:hover,
|
||||||
|
headerbar button.titlebutton.maximize:hover,
|
||||||
|
.csd headerbar button.titlebutton.maximize:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
.titlebar button.titlebutton.maximize:hover,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:hover {
|
||||||
|
padding: 2px;
|
||||||
|
margin: -2px 0;
|
||||||
|
background-color: #39ea49;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#MozillaGtkWidget.background
|
||||||
|
headerbar.titlebar.default-decoration
|
||||||
|
button.titlebutton.minimize:hover,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.minimize:hover,
|
||||||
|
headerbar button.titlebutton.minimize:hover,
|
||||||
|
.csd headerbar button.titlebutton.minimize:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
.titlebar button.titlebutton.minimize:hover,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:hover {
|
||||||
|
padding: 2px;
|
||||||
|
margin: -2px 0;
|
||||||
|
background-color: #fac536;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.close:hover,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.maximize:hover,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.minimize:hover,
|
||||||
|
headerbar button.titlebutton.close:hover,
|
||||||
|
headerbar button.titlebutton.close:backdrop:hover,
|
||||||
|
headerbar button.titlebutton.maximize:hover,
|
||||||
|
headerbar button.titlebutton.maximize:backdrop:hover,
|
||||||
|
headerbar button.titlebutton.minimize:hover,
|
||||||
|
headerbar button.titlebutton.minimize:backdrop:hover,
|
||||||
|
.csd headerbar button.titlebutton.close:hover,
|
||||||
|
.csd headerbar button.titlebutton.close:backdrop:hover,
|
||||||
|
.csd headerbar button.titlebutton.maximize:hover,
|
||||||
|
.csd headerbar button.titlebutton.maximize:backdrop:hover,
|
||||||
|
.csd headerbar button.titlebutton.minimize:hover,
|
||||||
|
.csd headerbar button.titlebutton.minimize:backdrop:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:backdrop:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:backdrop:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:backdrop:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:backdrop:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:backdrop:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:backdrop:hover,
|
||||||
|
.titlebar button.titlebutton.close:hover,
|
||||||
|
.titlebar button.titlebutton.close:backdrop:hover,
|
||||||
|
.titlebar button.titlebutton.maximize:hover,
|
||||||
|
.titlebar button.titlebutton.maximize:backdrop:hover,
|
||||||
|
.titlebar button.titlebutton.minimize:hover,
|
||||||
|
.titlebar button.titlebutton.minimize:backdrop:hover,
|
||||||
|
.csd .titlebar button.titlebutton.close:hover,
|
||||||
|
.csd .titlebar button.titlebutton.close:backdrop:hover,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:hover,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:backdrop:hover,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:hover,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:backdrop:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:backdrop:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:backdrop:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:backdrop:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:backdrop:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:backdrop:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:hover,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:backdrop:hover {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.close:active,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.maximize:active,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.minimize:active,
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.close:checked,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.maximize:checked,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.minimize:checked,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.close:backdrop:active,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.maximize:backdrop:active,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.minimize:backdrop:active,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.close:backdrop:checked,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.minimize:backdrop:checked,
|
||||||
|
headerbar button.titlebutton.close:active,
|
||||||
|
headerbar button.titlebutton.close:checked,
|
||||||
|
headerbar button.titlebutton.close:backdrop:active,
|
||||||
|
headerbar button.titlebutton.close:backdrop:checked,
|
||||||
|
headerbar button.titlebutton.maximize:active,
|
||||||
|
headerbar button.titlebutton.maximize:checked,
|
||||||
|
headerbar button.titlebutton.maximize:backdrop:active,
|
||||||
|
headerbar button.titlebutton.maximize:backdrop:checked,
|
||||||
|
headerbar button.titlebutton.minimize:active,
|
||||||
|
headerbar button.titlebutton.minimize:checked,
|
||||||
|
headerbar button.titlebutton.minimize:backdrop:active,
|
||||||
|
headerbar button.titlebutton.minimize:backdrop:checked,
|
||||||
|
.csd headerbar button.titlebutton.close:active,
|
||||||
|
.csd headerbar button.titlebutton.close:checked,
|
||||||
|
.csd headerbar button.titlebutton.close:backdrop:active,
|
||||||
|
.csd headerbar button.titlebutton.close:backdrop:checked,
|
||||||
|
.csd headerbar button.titlebutton.maximize:active,
|
||||||
|
.csd headerbar button.titlebutton.maximize:checked,
|
||||||
|
.csd headerbar button.titlebutton.maximize:backdrop:active,
|
||||||
|
.csd headerbar button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.csd headerbar button.titlebutton.minimize:active,
|
||||||
|
.csd headerbar button.titlebutton.minimize:checked,
|
||||||
|
.csd headerbar button.titlebutton.minimize:backdrop:active,
|
||||||
|
.csd headerbar button.titlebutton.minimize:backdrop:checked,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:active,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:checked,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:backdrop:active,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:backdrop:checked,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:active,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:checked,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:backdrop:active,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:backdrop:checked,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:active,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:checked,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:backdrop:active,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:backdrop:checked,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:active,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:checked,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:backdrop:active,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:backdrop:checked,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:active,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:checked,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:backdrop:active,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:active,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:checked,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:backdrop:active,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:backdrop:checked,
|
||||||
|
.titlebar button.titlebutton.close:active,
|
||||||
|
.titlebar button.titlebutton.close:checked,
|
||||||
|
.titlebar button.titlebutton.close:backdrop:active,
|
||||||
|
.titlebar button.titlebutton.close:backdrop:checked,
|
||||||
|
.titlebar button.titlebutton.maximize:active,
|
||||||
|
.titlebar button.titlebutton.maximize:checked,
|
||||||
|
.titlebar button.titlebutton.maximize:backdrop:active,
|
||||||
|
.titlebar button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.titlebar button.titlebutton.minimize:active,
|
||||||
|
.titlebar button.titlebutton.minimize:checked,
|
||||||
|
.titlebar button.titlebutton.minimize:backdrop:active,
|
||||||
|
.titlebar button.titlebutton.minimize:backdrop:checked,
|
||||||
|
.csd .titlebar button.titlebutton.close:active,
|
||||||
|
.csd .titlebar button.titlebutton.close:checked,
|
||||||
|
.csd .titlebar button.titlebutton.close:backdrop:active,
|
||||||
|
.csd .titlebar button.titlebutton.close:backdrop:checked,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:active,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:checked,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:backdrop:active,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:active,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:checked,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:backdrop:active,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:backdrop:checked,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:active,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:checked,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:backdrop:active,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:backdrop:checked,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:active,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:checked,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:backdrop:active,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:active,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:checked,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:backdrop:active,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:backdrop:checked,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:active,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:checked,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:backdrop:active,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:backdrop:checked,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:active,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:checked,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:backdrop:active,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:backdrop:checked,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:active,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:checked,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:backdrop:active,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:backdrop:checked {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solid-csd headerbar:not(.default-decoration) button.titlebutton.close:backdrop,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.maximize:backdrop,
|
||||||
|
.solid-csd
|
||||||
|
headerbar:not(.default-decoration)
|
||||||
|
button.titlebutton.minimize:backdrop,
|
||||||
|
headerbar button.titlebutton.close:backdrop,
|
||||||
|
headerbar button.titlebutton.maximize:backdrop,
|
||||||
|
headerbar button.titlebutton.minimize:backdrop,
|
||||||
|
.csd headerbar button.titlebutton.close:backdrop,
|
||||||
|
.csd headerbar button.titlebutton.maximize:backdrop,
|
||||||
|
.csd headerbar button.titlebutton.minimize:backdrop,
|
||||||
|
headerbar.default-decoration button.titlebutton.close:backdrop,
|
||||||
|
headerbar.default-decoration button.titlebutton.maximize:backdrop,
|
||||||
|
headerbar.default-decoration button.titlebutton.minimize:backdrop,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.close:backdrop,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.maximize:backdrop,
|
||||||
|
.csd headerbar.default-decoration button.titlebutton.minimize:backdrop,
|
||||||
|
.titlebar button.titlebutton.close:backdrop,
|
||||||
|
.titlebar button.titlebutton.maximize:backdrop,
|
||||||
|
.titlebar button.titlebutton.minimize:backdrop,
|
||||||
|
.csd .titlebar button.titlebutton.close:backdrop,
|
||||||
|
.csd .titlebar button.titlebutton.maximize:backdrop,
|
||||||
|
.csd .titlebar button.titlebutton.minimize:backdrop,
|
||||||
|
.titlebar.default-decoration button.titlebutton.close:backdrop,
|
||||||
|
.titlebar.default-decoration button.titlebutton.maximize:backdrop,
|
||||||
|
.titlebar.default-decoration button.titlebutton.minimize:backdrop,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.close:backdrop,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.maximize:backdrop,
|
||||||
|
.csd .titlebar.default-decoration button.titlebutton.minimize:backdrop {
|
||||||
|
color: transparent;
|
||||||
|
background-color: rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.titlebutton image {
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* gtk4 */
|
||||||
|
|
||||||
|
/*********************
|
||||||
|
* GtkWindowControls *
|
||||||
|
*********************/
|
||||||
|
windowcontrols > button {
|
||||||
|
min-height: 12px;
|
||||||
|
min-width: 12px;
|
||||||
|
padding: 0px;
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.minimize,
|
||||||
|
windowcontrols > button.maximize,
|
||||||
|
windowcontrols > button.close {
|
||||||
|
color: transparent;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.minimize > image {
|
||||||
|
background-color: #fdbe04;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.minimize:hover > image {
|
||||||
|
padding: 3px;
|
||||||
|
margin-right: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.maximize > image {
|
||||||
|
background-color: #38c76a;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.maximize:hover > image {
|
||||||
|
padding: 3px;
|
||||||
|
margin-right: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.close > image {
|
||||||
|
background-color: #fd5f51;
|
||||||
|
box-shadow: inset 0 -1px 0 0 alpha(white, 0.2),
|
||||||
|
inset 0 1px 0 0 alpha(white, 0.3), inset 1px 0 0 0 alpha(white, 0.07),
|
||||||
|
inset -1px 0 0 0 alpha(white, 0.07), 0 1px 3px rgba(0, 0, 0, 0.12),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
windowcontrols > button.close:hover > image {
|
||||||
|
padding: 3px;
|
||||||
|
margin-right: -1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
|
windowcontrols > button > image {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xbel version="1.0"
|
||||||
|
xmlns:bookmark="http://www.freedesktop.org/standards/desktop-bookmarks"
|
||||||
|
xmlns:mime="http://www.freedesktop.org/standards/shared-mime-info"
|
||||||
|
>
|
||||||
|
<bookmark href="smb://server.gib/" added="2025-08-14T15:11:25.449506Z" modified="2025-08-14T15:11:25.449511Z" visited="2025-08-14T15:11:25.449508Z">
|
||||||
|
<title>Windows shares on server.gib</title>
|
||||||
|
<info>
|
||||||
|
<metadata owner="http://freedesktop.org">
|
||||||
|
<bookmark:applications>
|
||||||
|
<bookmark:application name="org.gnome.Nautilus" exec="'org.gnome.Nautilus %u'" modified="2025-08-14T15:11:25.449509Z" count="1"/>
|
||||||
|
</bookmark:applications>
|
||||||
|
</metadata>
|
||||||
|
</info>
|
||||||
|
</bookmark>
|
||||||
|
</xbel>
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"THEME_NAME":"Tahoe-Dark"}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# Panama desktop parity
|
||||||
|
|
||||||
|
This is the living inventory for the GNOME-to-Hyprland transition. It records
|
||||||
|
real behavior, not a one-for-one count of extensions: an enabled extension with
|
||||||
|
no configured action is not treated as missing functionality.
|
||||||
|
|
||||||
|
Last live audit: 2026-08-17, Fedora 44, Hyprland 0.56.2, Quickshell 0.3.0.
|
||||||
|
|
||||||
|
## Daily desktop surfaces
|
||||||
|
|
||||||
|
| Capability | Panama implementation | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| App launcher and run dialog | Vicinae; Wofi emergency fallback | Live |
|
||||||
|
| App grid | Vicinae applications provider | Live |
|
||||||
|
| 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 |
|
||||||
|
| 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 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 where the hardware exposes it |
|
||||||
|
| AppIndicator / tray | Native StatusNotifierItem host and themed menus | Live |
|
||||||
|
| Screenshot and recording | Quickshell picker, grim/slurp/satty/wf-recorder | Live |
|
||||||
|
| Screen text and code recognition | Local Tesseract/ZBar Read mode, selectable Prism result sheet | Live after package install |
|
||||||
|
| Lock, idle, and power | hyprlock, hypridle, login1, Quickshell power menu | Live |
|
||||||
|
| Wallpaper | hyprpaper | Live |
|
||||||
|
| Keyring and authorization | gnome-keyring and hyprpolkitagent | Live |
|
||||||
|
| Portals and screen sharing | xdg-desktop-portal-hyprland plus GTK portal | Live |
|
||||||
|
| 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 | 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 |
|
||||||
|
|
||||||
|
## Portable machines
|
||||||
|
|
||||||
|
Panama was a desktop configuration until 2026-08-22, in the code as well as on
|
||||||
|
the machine: there was no reference to a battery, a lid, or a power source
|
||||||
|
anywhere in it, and `hypridle.conf` said "This is a desktop" in its own header.
|
||||||
|
|
||||||
|
| Capability | Panama implementation | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| Battery level | `services/Battery.qml` reads sysfs the way Vitals reads procfs; bar indicator hides entirely without a battery | Live |
|
||||||
|
| Power source | `panama-hw ac`; a machine with no mains supply reports as on wall power, so a desktop is never treated as running out | Live |
|
||||||
|
| Charge threshold | Power page control, only where the firmware exposes a ceiling; written through `panama-sudo` | Live |
|
||||||
|
| Idle timings per power source | hypridle holds one set at a time, so `panama-idle` rebuilds its config when the charger comes or goes | Live |
|
||||||
|
| Lock on lid close | hypridle's `before_sleep_cmd` already locks, so a lid-close suspend is a locked suspend | Live |
|
||||||
|
| Clamshell / docked | A logind `handle-lid-switch` inhibitor held while an external display is connected. No lid watcher, and a failure returns logind's default | Live, untested on a lid |
|
||||||
|
| Display arrangement on reconnect | `Displays.plannedRestore()` reapplies a confirmed layout, refusing when the stored mode is not offered | Live |
|
||||||
|
| Charger and low-battery notices | `StatusEvents`; the charger is ambient so DND quiets it, critical battery is not | Live |
|
||||||
|
| Hardware predicates | `panama-hw`: laptop, battery, ac, lid-closed, external-monitor, clamshell, touchpad, nvidia | Live |
|
||||||
|
|
||||||
|
The clamshell path is the one row that has not run on hardware with a lid.
|
||||||
|
Everything testable about it is tested; the end-to-end behaviour of closing a
|
||||||
|
real lid on a docked machine is not, and the helper's header says so rather
|
||||||
|
than letting silence imply otherwise.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
| GNOME extension | Result |
|
||||||
|
|---|---|
|
||||||
|
| Forge | Replaced by Hyprland tiling and the original keymap |
|
||||||
|
| Dash to Dock | Replaced by the Quickshell dock |
|
||||||
|
| AppIndicator | Replaced by the Quickshell system tray |
|
||||||
|
| Clipboard Indicator | Replaced by the bar clipboard panel and Vicinae history |
|
||||||
|
| Emoji Copy | Replaced by Vicinae emoji search on `SUPER+.` |
|
||||||
|
| Vitals | Replaced by the CPU, memory, and GPU bar readouts |
|
||||||
|
| Weather O'Clock | Replaced by bar weather and the date-menu weather card |
|
||||||
|
| 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 | 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 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 |
|
||||||
|
|
||||||
|
## System UI still worth adding
|
||||||
|
|
||||||
|
These are not blockers for normal use, but they are the remaining places where
|
||||||
|
a polished general-purpose desktop can go beyond the current shell.
|
||||||
|
|
||||||
|
1. A real external RustDesk test while hyprlock is active. Local process state
|
||||||
|
cannot prove what a remote client sees or whether input is accepted.
|
||||||
|
2. Closing a real lid on a docked laptop. The inhibitor, the predicates and the
|
||||||
|
decision are all tested; the physical gesture is not.
|
||||||
|
3. A keyboard-layout change notice. Hyprland reports the active keymap but not
|
||||||
|
a change event Quickshell already consumes, so this needs either polling or
|
||||||
|
new event plumbing, and a single-layout machine cannot test it.
|
||||||
|
4. Sticky keys, slow keys and bounce keys. Wayland has no protocol for these,
|
||||||
|
so each compositor implements them for itself — mutter does, which is how
|
||||||
|
GNOME has them on Wayland, and Hyprland does not. There is no XKB option to
|
||||||
|
lean on either: the accessx option group is X11-only and does not appear in
|
||||||
|
evdev.lst at all (checked, not assumed — `grep -c accessx
|
||||||
|
/usr/share/X11/xkb/rules/evdev.lst` is 0), and Hyprland will happily store
|
||||||
|
`accessx:enable` as a keyboard option that nothing ever acts on. This is a
|
||||||
|
Hyprland gap rather than a Wayland impossibility, and the Accessibility page
|
||||||
|
says so in those terms; an earlier version of this line blamed X11 and sent
|
||||||
|
anyone who needs sticky keys to the wrong conclusion about the platform.
|
||||||
|
5. An on-screen keyboard, for a touch or convertible machine.
|
||||||
|
|
||||||
|
KDE Connect, the printer UI, Tesseract, and ZBar are installed and remain
|
||||||
|
tracked in `setup/packages/hyprland-packages`.
|
||||||
|
|
||||||
|
## Global menu decision
|
||||||
|
|
||||||
|
A macOS-style global application menu is not being shipped. Quickshell can
|
||||||
|
render a `DBusMenu` that an application already exports, but it does not provide
|
||||||
|
an application-menu registrar or a reliable focused-window-to-menu association.
|
||||||
|
Hyprland has no cross-toolkit global-menu protocol, and the daily applications
|
||||||
|
audited here do not consistently export one: GTK 4/Nautilus and Electron apps
|
||||||
|
are the important failures.
|
||||||
|
|
||||||
|
That would produce a premium-looking empty strip for most windows and a working
|
||||||
|
menu for a minority, which is worse than omitting it. Relevant references:
|
||||||
|
|
||||||
|
- <https://quickshell.org/docs/v0.3.0/types/Quickshell/DBusMenu/>
|
||||||
|
- <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 labeled as an application menu.
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
# Panama :: Hyprland
|
||||||
|
|
||||||
|
A Hyprland desktop built to reproduce the GNOME + Forge setup it replaces, so
|
||||||
|
that muscle memory transfers unchanged.
|
||||||
|
|
||||||
|
## The one thing to know first
|
||||||
|
|
||||||
|
**Hyprland 0.55 replaced hyprlang with Lua.** `hyprland.conf` still loads on
|
||||||
|
0.56 as a legacy fallback and is removed in 0.57. Everything here is Lua.
|
||||||
|
|
||||||
|
The authoritative option reference for the installed build is on disk:
|
||||||
|
|
||||||
|
| File | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `/usr/share/hypr/stubs/hl.meta.lua` | Generated type stub — the exact option list for this build |
|
||||||
|
| `/usr/share/hypr/hyprland.lua` | Upstream example config |
|
||||||
|
|
||||||
|
Wire the stub into your editor with `.luarc.json` in this directory for
|
||||||
|
autocomplete.
|
||||||
|
|
||||||
|
**The Hypr ecosystem tools did NOT move to Lua.** `hyprlock.conf`,
|
||||||
|
`hypridle.conf`, `hyprpaper.conf` and `hyprtoolkit.conf` are still hyprlang.
|
||||||
|
Don't "fix" them.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `hyprland.lua` | Entry point. Each `require()` is its own error scope |
|
||||||
|
| `prefs.lua` | Reads the settings file the Settings app writes. See below |
|
||||||
|
| `env.lua` | Environment. Note the uwsm caveat below |
|
||||||
|
| `monitors.lua` | Monitor geometry and scaling (the Kuycon by description), and the HDR decision |
|
||||||
|
| `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, including the custom shortcuts the Settings app stores |
|
||||||
|
| `actions.lua` | Named-action resolver: the whitelist tables that turn stored `{kind, target}` data into binds, gestures — never into free-form commands |
|
||||||
|
| `autostart.lua` | Session startup |
|
||||||
|
| `overrides.lua` | Per-machine escape hatch, loaded last |
|
||||||
|
| `shaders/` | Whole-screen color-filter shaders (grayscale and the three color-blindness corrections) `looks.lua` maps the `colorFilter` setting onto |
|
||||||
|
| `hyprlock.conf` / `hypridle.conf` / `hyprpaper.conf` / `hyprtoolkit.conf` | Ecosystem tools (hyprlang) |
|
||||||
|
|
||||||
|
Validate any change without leaving your session:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Hyprland --verify-config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generated configuration
|
||||||
|
|
||||||
|
Two ecosystem tools cannot read the shared settings file, and their configs live
|
||||||
|
in this directory — which is a symlink into the Panama repository, so writing to
|
||||||
|
them at runtime would put machine state into a tracked file. Both are therefore
|
||||||
|
generated elsewhere:
|
||||||
|
|
||||||
|
| Tool | Generated to | Pointed there by |
|
||||||
|
|---|---|---|
|
||||||
|
| `hypridle` | `$XDG_STATE_HOME/panama/hypridle.conf` | a systemd user drop-in installed by `panama-idle install` |
|
||||||
|
| `hyprpaper` | not generated — the wallpaper is applied over IPC and re-applied at shell start | — |
|
||||||
|
|
||||||
|
`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;
|
||||||
|
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:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
~/.config/quickshell/scripts/panama-idle remove
|
||||||
|
```
|
||||||
|
|
||||||
|
## Settings: one file, both sides
|
||||||
|
|
||||||
|
`~/.config/panama/settings.json` is shared with the Quickshell side. The
|
||||||
|
relationship is:
|
||||||
|
|
||||||
|
- **This config is the default.** Every adjustable value is written
|
||||||
|
`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.
|
||||||
|
- **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
|
||||||
|
`quickshell/config/PreferenceSchema.qml` with a `hypr` block naming the
|
||||||
|
`hl.config` path, then read it here with `prefs.get`. Nothing else is needed —
|
||||||
|
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 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
|
||||||
|
the Settings app without editing this file.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||||
|
```
|
||||||
|
|
||||||
|
Only the **chord** is ever taken from settings — the action is always the Lua
|
||||||
|
value written here. A stored override can therefore move a shortcut but can
|
||||||
|
never make one do something else, which is what makes reading overrides from a
|
||||||
|
file the user can edit safe.
|
||||||
|
|
||||||
|
Overrides are keyed by the **shipped chord**, not the description. Descriptions
|
||||||
|
are not unique: "Calculator" is both `SUPER + C` and the `XF86Calculator`
|
||||||
|
hardware key, and keying by description moved both onto the same new chord,
|
||||||
|
silently costing the hardware key.
|
||||||
|
|
||||||
|
An override whose value is not a plausible chord is ignored in favour of the
|
||||||
|
shipped one, so a hand-edited `settings.json` cannot cost you a keymap.
|
||||||
|
|
||||||
|
### Keybind descriptions are required
|
||||||
|
|
||||||
|
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 the Settings app drops it from the
|
||||||
|
Keyboard page under Input rather than showing a mystery row.
|
||||||
|
`tests/quickshell/keybinds-contract` fails if any bind lacks a description, so
|
||||||
|
this cannot regress silently.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
hl.bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||||
|
```
|
||||||
|
|
||||||
|
The page groups shortcuts by what the description says they do, so a new bind
|
||||||
|
lands in the right section with no change to the UI.
|
||||||
|
|
||||||
|
### Never use `hyprctl keyword`
|
||||||
|
|
||||||
|
On a Lua-configured Hyprland it refuses the write, prints
|
||||||
|
`keyword can't work with non-legacy parsers` to **stdout**, and still **exits 0**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ hyprctl getoption decoration:rounding -j # → "int": 18
|
||||||
|
$ hyprctl keyword decoration:rounding 4 # → the refusal above
|
||||||
|
$ echo $? # → 0
|
||||||
|
$ hyprctl getoption decoration:rounding -j # → "int": 18, unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `hyprctl eval 'hl.config({ ... })'` instead. Note that `eval` *also* exits 0
|
||||||
|
on syntax and runtime errors, reporting them as an `error:` line on stdout — so
|
||||||
|
for either command, the only trustworthy signal that a write landed is reading
|
||||||
|
the value back with `hyprctl getoption`.
|
||||||
|
|
||||||
|
## The look
|
||||||
|
|
||||||
|
Tokyo Night Moon, with two accents that come from the tmux theme:
|
||||||
|
|
||||||
|
| Token | Value | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `accent` | `#82aaff` | Carries every state meaning — focused, active, on |
|
||||||
|
| `accentSecondary` | `#b172b0` | Never used alone; only the far end of a gradient |
|
||||||
|
|
||||||
|
The signature is **the prism**: the two accents meeting. It appears in exactly
|
||||||
|
four places, and nowhere else —
|
||||||
|
|
||||||
|
1. A one-pixel hairline along the top edge of every glass surface (the bar,
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
The restraint is the point. The pink stops being special the moment it's used
|
||||||
|
as a flat fill, so it never is.
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
**Adwaita Sans for everything the user reads as text — no monospace in the UI.**
|
||||||
|
A monospaced clock or percentage reads as terminal output pasted into a panel,
|
||||||
|
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 recolor. Where a
|
||||||
|
themed freedesktop icon is wanted instead, `quickshell/widgets/ThemedIcon.qml`
|
||||||
|
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:
|
||||||
|
Theme.tabularFigures`. Tabular figures share one advance width, so the text
|
||||||
|
stops twitching as numbers tick without reaching for a monospaced face.
|
||||||
|
|
||||||
|
Window rounding is 18, matching the shell's popover radius, so a window and a
|
||||||
|
panel next to each other read as the same family of object.
|
||||||
|
|
||||||
|
**On animated gradient borders:** Hyprland can rotate the border gradient
|
||||||
|
continuously with `borderangle` + `style = "loop"`. Don't. The wiki is explicit
|
||||||
|
that it forces a full-refresh-rate repaint forever, even when no border is
|
||||||
|
visible — at 4500×3000 that is pure idle GPU burn. This config uses
|
||||||
|
`style = "once"`, so the gradient sweeps into place when a window takes focus
|
||||||
|
and then costs nothing.
|
||||||
|
|
||||||
|
## Session: use the uwsm one
|
||||||
|
|
||||||
|
Log in as **"Hyprland (uwsm-managed)"**, not plain "Hyprland".
|
||||||
|
|
||||||
|
`xdg-desktop-portal-hyprland`'s systemd unit requires `graphical-session.target`,
|
||||||
|
and Fedora ships no `hyprland-session.target`. Without uwsm that target never
|
||||||
|
activates and **screen sharing in OBS, Sunshine and Zoom silently fails.**
|
||||||
|
|
||||||
|
### The uwsm environment gotcha
|
||||||
|
|
||||||
|
`hl.env()` in `env.lua` exports into the *compositor's* environment, **not** the
|
||||||
|
systemd user manager. Anything started as a user unit — `vicinae.service`,
|
||||||
|
`hyprpaper.service`, `hyprpolkitagent.service`, `hypridle.service` — and every
|
||||||
|
app those launch will not see it.
|
||||||
|
|
||||||
|
That is why `~/.config/uwsm/env` and `~/.config/uwsm/env-hyprland` exist and
|
||||||
|
duplicate the important variables. Keep them in sync with `env.lua`; `env.lua`
|
||||||
|
remains the fallback for the plain session.
|
||||||
|
|
||||||
|
## HDR — read this before turning it on
|
||||||
|
|
||||||
|
GNOME ran this panel in bt2100/HDR. This config deliberately does not.
|
||||||
|
|
||||||
|
On Hyprland 0.56.x, `cm = "hdr"` currently breaks **screencopy**: `grim`
|
||||||
|
screenshots come back empty, and OBS/Sunshine capture and hyprlock's blurred
|
||||||
|
background go with them. Root cause is still open upstream.
|
||||||
|
|
||||||
|
So the desktop runs SDR at 10-bit (`cm = "auto"`) and HDR is handed to
|
||||||
|
fullscreen games only, via `render.cm_auto_hdr = 1` in `looks.lua`. Games get
|
||||||
|
HDR; screenshots keep working.
|
||||||
|
|
||||||
|
To try full-time HDR anyway, `overrides.lua` has the block and the ordered list
|
||||||
|
of workarounds to reach for when things break.
|
||||||
|
|
||||||
|
## Keymap
|
||||||
|
|
||||||
|
The mental model is unchanged from Forge:
|
||||||
|
|
||||||
|
- **SUPER** acts on windows
|
||||||
|
- **ALT** acts on workspaces
|
||||||
|
- **SUPER + CTRL** changes layout structure
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `SUPER + H/J/K/L` (or arrows) | Focus |
|
||||||
|
| `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` · `,/N` | Taller · shorter |
|
||||||
|
| `SUPER + [` / `]` / `=` | Shrink / expand / reset split |
|
||||||
|
| `SUPER + Q` | Close |
|
||||||
|
| `SUPER + U` | Fullscreen |
|
||||||
|
| `SUPER + CTRL + C` | Toggle float |
|
||||||
|
| `SUPER + CTRL + SHIFT + C` | Pin (nearest thing to Forge's "always float") |
|
||||||
|
| `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 / minimize to scratchpad |
|
||||||
|
|
||||||
|
### Workspaces (dynamic, like GNOME)
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `ALT + H` / `ALT + L` | Previous / next workspace |
|
||||||
|
| `ALT + SHIFT + H/L` | Move window to previous / next |
|
||||||
|
| `ALT + 1..9`, `ALT + 0` | Jump to workspace |
|
||||||
|
| `ALT + SHIFT + 1..9` | Send window to workspace |
|
||||||
|
| `SUPER + scroll` | Change workspace |
|
||||||
|
|
||||||
|
### Launcher and shell
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `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` | Settings |
|
||||||
|
| `SUPER + SHIFT + F` | Start or reveal focus session |
|
||||||
|
| `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` | Color picker |
|
||||||
|
| `CTRL + ALT + L` · `SUPER + Backspace` | Lock (SUPER+L is "focus right") |
|
||||||
|
| `SUPER + /` | Every shortcut, on screen. Reads the live keymap, so a rebind shows here |
|
||||||
|
| `SUPER + ALT + T/N/W/F/C/E` | Go to that application if it is open, rather than starting another |
|
||||||
|
| `CTRL + ALT + Delete` | Power menu |
|
||||||
|
|
||||||
|
### Apps
|
||||||
|
`SUPER + T` terminal · `N` neovim · `W` browser · `F` files · `C` calculator ·
|
||||||
|
`E` mail · `I` Settings · `CTRL + SHIFT + Esc` system monitor. GNOME
|
||||||
|
Settings remains searchable in Vicinae for hardware and account panels.
|
||||||
|
|
||||||
|
## Calendar and notifications
|
||||||
|
|
||||||
|
Click the clock to open the two-column Daybook on **Agenda**. The month grid,
|
||||||
|
today's schedule, Up Next, weather, and conditional media controls stay visible
|
||||||
|
together. Its right pane has three first-class pages:
|
||||||
|
|
||||||
|
- **Agenda** — the selected day's calendar.
|
||||||
|
- **Ongoing** — Focus, Caffeine, recording, screen sharing, camera, and
|
||||||
|
microphone activity, visible until each state ends.
|
||||||
|
- **Notifications** — grouped history, Do Not Disturb, and clear actions.
|
||||||
|
|
||||||
|
`SUPER + B` opens Notifications directly. Switching pages preserves the
|
||||||
|
selected date and visible month. Brief completed events such as screenshots and
|
||||||
|
device changes remain in Signal Glass instead of accumulating in Ongoing.
|
||||||
|
|
||||||
|
Calendar data comes from Evolution Data Server, so the Google, iCloud,
|
||||||
|
Nextcloud, and local calendars already configured for GNOME remain the source
|
||||||
|
of truth. Panama never stores account credentials. Use GNOME Calendar or Online
|
||||||
|
Accounts to add and manage accounts, and click an event in Daybook to hand off
|
||||||
|
editing and full detail to GNOME Calendar.
|
||||||
|
|
||||||
|
The right side of the bar shows a small calendar capsule only from 15 minutes
|
||||||
|
before a timed event until 5 minutes after it starts. It displays relative time
|
||||||
|
(`12m` or `Now`); the event title is available on hover but otherwise stays out
|
||||||
|
of the bar. All-day events never trigger the capsule.
|
||||||
|
|
||||||
|
## Control Center
|
||||||
|
|
||||||
|
Click the right-side status cluster or press `SUPER + S` for Panama's Control
|
||||||
|
Center. It is attached directly beneath the bar and keeps Wi-Fi, Bluetooth,
|
||||||
|
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 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
|
||||||
|
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. 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.
|
||||||
|
|
||||||
|
Phone uses KDE Connect for the capabilities the paired iPhone actually
|
||||||
|
advertises: Send File, Send Clipboard, and Ring. The device remains visible
|
||||||
|
while iOS suspends KDE Connect, but those actions stay disabled until it
|
||||||
|
reconnects. Messages opens BlueBubbles independently of KDE Connect. No battery
|
||||||
|
percentage is invented when iOS reports none. Active file sends appear in
|
||||||
|
Daybook's Ongoing page; successful sends become a quiet recent exchange.
|
||||||
|
|
||||||
|
## Screen Intelligence
|
||||||
|
|
||||||
|
`SUPER + SHIFT + S` opens the capture picker directly in Selection + Read
|
||||||
|
mode. Read is also available beside Screenshot and Record under `Print`, and
|
||||||
|
works on a whole display, a window, or a region. Tesseract recognizes English
|
||||||
|
text locally; ZBar recognizes QR codes and barcodes. The result sheet can copy
|
||||||
|
text, search it, translate it, or open a detected web address. No pixels leave
|
||||||
|
the workstation unless one of those explicit network actions is selected.
|
||||||
|
|
||||||
|
## Deliberate deviations from GNOME
|
||||||
|
|
||||||
|
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
|
||||||
|
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` or `SUPER + Backspace`.** `SUPER + L` is "focus right" in this keymap.
|
||||||
|
- **`SUPER + grave` was Forge's "cycle windows of same app"**, which Hyprland
|
||||||
|
has no equivalent for. "Last window" moved to `SUPER + SHIFT + grave`.
|
||||||
|
- **`gnome-control-center` is launched with `XDG_CURRENT_DESKTOP=GNOME`**,
|
||||||
|
because it hard-refuses to start otherwise. Panels backed by system services
|
||||||
|
work; panels backed by GNOME Shell (Displays, Keyboard Shortcuts,
|
||||||
|
Multitasking, Appearance) are inert — Hyprland owns those now.
|
||||||
|
|
||||||
|
## Gaming
|
||||||
|
|
||||||
|
`content = "game"` on the window rules in `rules.lua` is the keystone — it is
|
||||||
|
what `misc.vrr = 3`, `render.direct_scanout = 2` and `cursor.no_break_fs_vrr = 2`
|
||||||
|
key off. Games additionally get blur, animation, shadow and dim disabled, plus
|
||||||
|
`immediate` for tearing.
|
||||||
|
|
||||||
|
Tearing only takes effect when the game is fullscreen and the only thing on
|
||||||
|
screen — no bar, no notifications.
|
||||||
|
|
||||||
|
## Do not install a notification daemon
|
||||||
|
|
||||||
|
`mako`, `dunst` and `SwayNotificationCenter` all register
|
||||||
|
`Name=org.freedesktop.Notifications` for D-Bus activation. Installing any of
|
||||||
|
them creates a startup race against Quickshell's notification server, and
|
||||||
|
whoever wins keeps the name. If one gets installed as a dependency,
|
||||||
|
`systemctl --user mask <name>.service`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Nothing autostarts** — check `systemctl --user status hyprland-session.target`
|
||||||
|
and `systemctl --user show-environment | grep WAYLAND_DISPLAY`. An empty
|
||||||
|
environment means uwsm didn't export it and units with
|
||||||
|
`ConditionEnvironment=WAYLAND_DISPLAY` silently skip.
|
||||||
|
|
||||||
|
**Screen sharing missing** — `busctl --user list | grep impl.portal` should show
|
||||||
|
`xdg-desktop-portal-hyprland`.
|
||||||
|
|
||||||
|
**Qt apps look wrong** — `QT_QPA_PLATFORMTHEME` must be `gtk3`. It is a single
|
||||||
|
value, not a list: Qt splits on `:` and uses only the first token.
|
||||||
|
|
||||||
|
**Wrong GPU / won't start** — `AQ_DRM_DEVICES` points at
|
||||||
|
`/dev/dri/amd-dgpu`, created by `config/copy/etc/udev/rules.d/99-panama-gpu.rules`.
|
||||||
|
Both `env.lua` and `uwsm/env-hyprland` guard on the symlink existing, so a
|
||||||
|
missing rule degrades to "let aquamarine choose" rather than failing to start.
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Named actions
|
||||||
|
--
|
||||||
|
-- The one thing that makes it safe for settings.json to describe a shortcut.
|
||||||
|
--
|
||||||
|
-- Panama Settings lets a person invent a keyboard shortcut and assign a
|
||||||
|
-- four-finger gesture. Both are stored in the same user-editable JSON file the
|
||||||
|
-- rest of the desktop reads, and both have to end up as something the
|
||||||
|
-- compositor executes -- which is exactly the shape of every configuration
|
||||||
|
-- format that turned out to be a shell injection.
|
||||||
|
--
|
||||||
|
-- It is not one here, and this file is why. A stored action is DATA:
|
||||||
|
--
|
||||||
|
-- { kind = "app" | "shell" | "window", target = "<id>", label = "<text>" }
|
||||||
|
--
|
||||||
|
-- `kind` is an enum with three members. `target` is either a key of one of the
|
||||||
|
-- whitelist tables below -- whose values are literals written here, in Lua, by
|
||||||
|
-- a human -- or, for `app`, an identifier that has to match a character class
|
||||||
|
-- containing no shell metacharacter at all, and which is then quoted as a
|
||||||
|
-- single argv element for panama-launch rather than pasted into a command.
|
||||||
|
--
|
||||||
|
-- So the worst a hand-edited (or maliciously written) settings file can do is
|
||||||
|
-- pick a different entry from a list that is fixed at ship time, or launch an
|
||||||
|
-- application by id. It cannot introduce a command. There is no path from a
|
||||||
|
-- stored string to a new exec string; the table lookups are the only source of
|
||||||
|
-- one.
|
||||||
|
--
|
||||||
|
-- Everything invalid returns nil and the caller skips the bind or gesture --
|
||||||
|
-- the prefs.lua philosophy: never raise, never guess. A malformed entry costs
|
||||||
|
-- one shortcut, never the keymap and never the compositor.
|
||||||
|
--
|
||||||
|
-- Required by keybinds.lua (custom shortcuts) and input.lua (four-finger
|
||||||
|
-- gestures). services/Keybinds.qml's describeAction() is the QML mirror of the
|
||||||
|
-- vocabulary below; the two lists have to be edited together.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local actions = {}
|
||||||
|
|
||||||
|
-- ── Shell verbs ─────────────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- Every entry is a command string written HERE. Nothing stored anywhere else
|
||||||
|
-- contributes a character to one; `target` only chooses which of these to use.
|
||||||
|
--
|
||||||
|
-- The `qs ipc call` targets and functions are the ones quickshell/shell.qml
|
||||||
|
-- actually registers -- an IpcHandler silently declines to register a function
|
||||||
|
-- it cannot type-check, so a verb invented here would be a shortcut that does
|
||||||
|
-- nothing. Checked against shell.qml, not remembered.
|
||||||
|
--
|
||||||
|
-- The three that are not IPC (`launcher`, `color-picker`, `lock`) are the same
|
||||||
|
-- literal commands the shipped binds in keybinds.lua use, for the same reason
|
||||||
|
-- they use them: they are the tools, not the shell.
|
||||||
|
local SHELL = {
|
||||||
|
["dnd-toggle"] = { label = "Do Not Disturb", command = "qs ipc call notifications dnd" },
|
||||||
|
["notifications"] = { label = "Notifications", command = "qs ipc call notifications toggle" },
|
||||||
|
["overview"] = { label = "Overview", command = "qs ipc call overview toggle" },
|
||||||
|
["launcher"] = { label = "Launcher", command = "vicinae toggle" },
|
||||||
|
["clipboard"] = { label = "Clipboard history", command = "qs ipc call clipboard toggle" },
|
||||||
|
["screenshot"] = { label = "Screenshot / record", command = "qs ipc call capture open" },
|
||||||
|
["screenshot-screen"] = { label = "Screenshot: whole screen", command = "qs ipc call capture screenNow" },
|
||||||
|
["screenshot-window"] = { label = "Screenshot: window", command = "qs ipc call capture windowNow" },
|
||||||
|
["screen-intelligence"]= { label = "Screen Intelligence", command = "qs ipc call screen-intelligence open" },
|
||||||
|
["color-picker"] = { label = "Color picker", command = "hyprpicker -a -f hex" },
|
||||||
|
["quick-settings"] = { label = "Quick settings", command = "qs ipc call quicksettings toggle" },
|
||||||
|
["settings"] = { label = "Settings", command = "qs ipc call settings toggle" },
|
||||||
|
["cheatsheet"] = { label = "Keyboard shortcuts", command = "qs ipc call cheatsheet toggle" },
|
||||||
|
["focus-session"] = { label = "Focus session", command = "qs ipc call focus reveal" },
|
||||||
|
["caffeine"] = { label = "Keep awake", command = "qs ipc call caffeine toggle" },
|
||||||
|
["night-light"] = { label = "Night Light", command = "qs ipc call night-light toggle" },
|
||||||
|
["activity"] = { label = "Activity", command = "qs ipc call activity toggle" },
|
||||||
|
["power-menu"] = { label = "Power menu", command = "qs ipc call powermenu toggle" },
|
||||||
|
["lock"] = { label = "Lock", command = "loginctl lock-session" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ── Window verbs ────────────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- Builders rather than dispatchers, so nothing is constructed for a verb that
|
||||||
|
-- is never chosen, and so a gesture builds its dispatcher when the fingers
|
||||||
|
-- move rather than holding one from config time.
|
||||||
|
--
|
||||||
|
-- `workspace:N` is not in the table: it is ten entries that differ by a number,
|
||||||
|
-- and the number is validated as 1..10 in `window_action` below.
|
||||||
|
local WINDOW = {
|
||||||
|
["float-toggle"] = { label = "Toggle float", build = function() return hl.dsp.window.float({ action = "toggle" }) end },
|
||||||
|
["fullscreen"] = { label = "Fullscreen", build = function() return hl.dsp.window.fullscreen({ mode = "fullscreen" }) end },
|
||||||
|
["pin"] = { label = "Pin window", build = function() return hl.dsp.window.pin({ action = "toggle" }) end },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Published so a contract can read the vocabulary without parsing this file,
|
||||||
|
-- and so the ten workspace verbs have one definition rather than two.
|
||||||
|
actions.shell_verbs = SHELL
|
||||||
|
actions.window_verbs = WINDOW
|
||||||
|
actions.workspace_min = 1
|
||||||
|
actions.workspace_max = 10
|
||||||
|
|
||||||
|
-- ── Targets ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Letters, digits, and the four punctuation marks a desktop id actually uses.
|
||||||
|
-- Deliberately excludes every shell metacharacter, quote, slash and space, so
|
||||||
|
-- an id that passes cannot change the meaning of a command line even before it
|
||||||
|
-- is quoted -- the quoting below is the second lock on the same door.
|
||||||
|
local APP_TARGET = "^[A-Za-z0-9@._%-]+$"
|
||||||
|
|
||||||
|
local function valid_app_target(target)
|
||||||
|
return type(target) == "string"
|
||||||
|
and #target >= 1 and #target <= 128
|
||||||
|
and target:match(APP_TARGET) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Single-quoted for the shell, with the one escape single quotes need. Same
|
||||||
|
-- function keybinds.lua uses for the go-to patterns, and used here for the
|
||||||
|
-- same reason: the value reaches the command as one argument.
|
||||||
|
local function shell_quote(value)
|
||||||
|
return "'" .. value:gsub("'", "'\\''") .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Google RE2 metacharacters, escaped so the id matches itself literally.
|
||||||
|
-- panama-launch takes a regular expression, and an unescaped "org.gnome.Files"
|
||||||
|
-- would also match "orgxgnomexFiles".
|
||||||
|
local function escape_regex(value)
|
||||||
|
return (value:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?%{%}%|\\]", "\\%0"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Resolution ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local launcher_bin = "$HOME/.local/share/Panama/bin/panama-launch"
|
||||||
|
|
||||||
|
-- The launch-or-focus path the shipped application keys use: raise the window
|
||||||
|
-- if it is already open, start it if it is not. The id is the class pattern
|
||||||
|
-- (anchored, escaped) and the thing to start; on Wayland an application's
|
||||||
|
-- desktop id and its window class are the same string often enough that this
|
||||||
|
-- is the right first guess, and the wrong guess costs a second window rather
|
||||||
|
-- than an error.
|
||||||
|
--
|
||||||
|
-- gtk-launch activates a desktop entry by id, which is what the applications
|
||||||
|
-- catalog in Settings offers -- a desktop id is not a binary and cannot be
|
||||||
|
-- exec'd directly.
|
||||||
|
local function app_action(target)
|
||||||
|
if not valid_app_target(target) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local launch_command = table.concat({
|
||||||
|
launcher_bin,
|
||||||
|
"--class", shell_quote("^" .. escape_regex(target) .. "$"),
|
||||||
|
"--", "gtk-launch", shell_quote(target),
|
||||||
|
}, " ")
|
||||||
|
return function() return hl.dsp.exec_cmd(launch_command) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shell_action(target)
|
||||||
|
if type(target) ~= "string" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local verb = SHELL[target]
|
||||||
|
if verb == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return function() return hl.dsp.exec_cmd(verb.command) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function window_action(target)
|
||||||
|
if type(target) ~= "string" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local verb = WINDOW[target]
|
||||||
|
if verb ~= nil then
|
||||||
|
return verb.build
|
||||||
|
end
|
||||||
|
|
||||||
|
local index = target:match("^workspace:(%d+)$")
|
||||||
|
if index == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local number = tonumber(index)
|
||||||
|
if number == nil or number < actions.workspace_min or number > actions.workspace_max then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return function() return hl.dsp.focus({ workspace = number }) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local KINDS = {
|
||||||
|
app = app_action,
|
||||||
|
shell = shell_action,
|
||||||
|
window = window_action,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- The builder for one stored entry, or nil when the entry is anything this
|
||||||
|
-- file does not recognise. Everything above funnels through here, so there is
|
||||||
|
-- exactly one place where a stored value becomes an action.
|
||||||
|
local function builder(entry)
|
||||||
|
if type(entry) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local resolve = KINDS[entry.kind]
|
||||||
|
if resolve == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return resolve(entry.target)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For hl.bind: the dispatcher itself, or nil.
|
||||||
|
function actions.dispatcher(entry)
|
||||||
|
local build = builder(entry)
|
||||||
|
if build == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return build()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- For hl.gesture: a function, which is what a gesture action has to be when it
|
||||||
|
-- is not one of Hyprland's own built-in names ("workspace" and friends).
|
||||||
|
function actions.gesture(entry)
|
||||||
|
local build = builder(entry)
|
||||||
|
if build == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return function() hl.dispatch(build()) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- True when an entry resolves to something. Cheap enough to call twice; used
|
||||||
|
-- where the caller wants to check before it commits to emitting anything.
|
||||||
|
function actions.valid(entry)
|
||||||
|
return builder(entry) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return actions
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Autostart
|
||||||
|
--
|
||||||
|
-- `exec-once` no longer exists in 0.56; startup is an event handler.
|
||||||
|
--
|
||||||
|
-- Session: Fedora installs two sessions, "Hyprland" and "Hyprland
|
||||||
|
-- (uwsm-managed)". Panama targets the uwsm one, because
|
||||||
|
-- xdg-desktop-portal-hyprland's systemd unit requires graphical-session.target
|
||||||
|
-- and Fedora ships no hyprland-session.target -- without uwsm, screen sharing
|
||||||
|
-- in OBS/Sunshine/Zoom silently fails. The target is started explicitly below
|
||||||
|
-- so the plain session works too.
|
||||||
|
--
|
||||||
|
-- Anything that ships a systemd user unit is started as a unit rather than as
|
||||||
|
-- a compositor child. That is not cosmetic: under uwsm, units get the correct
|
||||||
|
-- activation environment (see uwsm/env), they restart on failure, and they
|
||||||
|
-- shut down in order. Panama's `change-settings` script enables them once;
|
||||||
|
-- the explicit starts here make a fresh clone work before that has run.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
hl.on("hyprland.start", function()
|
||||||
|
-- Publish the session environment to systemd and D-Bus so user units and
|
||||||
|
-- the portals can see WAYLAND_DISPLAY. Cheap, and essential without uwsm.
|
||||||
|
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. 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")
|
||||||
|
|
||||||
|
-- Pending repairs for this machine, if any. Oneshot: it checks, tells the
|
||||||
|
-- user when there is something to tell, and exits. Started here rather
|
||||||
|
-- than enabled so it belongs to the Hyprland session; see the unit.
|
||||||
|
hl.exec_cmd("systemctl --user start panama-migrate-notify.service")
|
||||||
|
|
||||||
|
-- Notices when a program dumps core and says so once per session. Under
|
||||||
|
-- GNOME, ABRT does this; here nothing did, and applications died silently.
|
||||||
|
hl.exec_cmd("systemctl --user start panama-crash-watch.service")
|
||||||
|
|
||||||
|
-- Text expansion. change-settings runs `espanso service register`, which
|
||||||
|
-- writes and enables espanso's own user unit; the explicit start makes the
|
||||||
|
-- first Hyprland login after a fresh install work rather than the second.
|
||||||
|
-- Started on its own line: unlike the units above it carries no
|
||||||
|
-- ConditionEnvironment, and a missing unit (espanso not yet registered)
|
||||||
|
-- must not muddy the start of the four that lock and wallpaper depend on.
|
||||||
|
hl.exec_cmd("systemctl --user start espanso.service")
|
||||||
|
|
||||||
|
-- The shell: bar, dock, overview, quick settings, notifications, capture.
|
||||||
|
-- A supervised unit rather than a compositor child, for the one property
|
||||||
|
-- a child cannot have: if the shell crashes, systemd restarts it in two
|
||||||
|
-- seconds instead of leaving a desktop with no bar until someone knows
|
||||||
|
-- what to type. The unit ships in config/local/share/systemd/user and is
|
||||||
|
-- started per-session here like every other Panama unit.
|
||||||
|
hl.exec_cmd("systemctl --user start panama-quickshell.service")
|
||||||
|
|
||||||
|
-- Removable-media automounting. GNOME did this invisibly via gvfs+udisks;
|
||||||
|
-- outside GNOME something has to ask udisks to mount. No tray icon: the
|
||||||
|
-- Quickshell bar already has a tray, and udiskie's own icon would be
|
||||||
|
-- redundant. -f opens Nautilus when you click the mount notification.
|
||||||
|
hl.exec_cmd("udiskie --automount --notify --no-tray --file-manager nautilus")
|
||||||
|
|
||||||
|
-- Keyring unlock, for Nextcloud and Bitwarden credential storage.
|
||||||
|
hl.exec_cmd("/usr/bin/gnome-keyring-daemon --start --components=secrets,ssh,pkcs11")
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
--
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
-- Tear down what the start handler brought up.
|
||||||
|
--
|
||||||
|
-- This stopped `hyprland-session.target`, which does not exist on Fedora --
|
||||||
|
-- systemctl reports "Unit not found" and the handler achieves nothing. The
|
||||||
|
-- units started above are not children of the compositor, so nothing else
|
||||||
|
-- stopped them either: hyprpaper, hypridle, vicinae and the polkit agent kept
|
||||||
|
-- running after logout, and each one Requires=graphical-session.target, so
|
||||||
|
-- that target stayed active with no session behind it.
|
||||||
|
--
|
||||||
|
-- The next login then failed. Both uwsm and gnome-session refuse to start into
|
||||||
|
-- a session that is already running ("A compositor or graphical-session*
|
||||||
|
-- target is already active!"), so "Hyprland (uwsm-managed)" and GNOME both
|
||||||
|
-- bounced straight back to the login screen while plain Hyprland -- which
|
||||||
|
-- makes no such check -- kept working. The desktop appeared to be the only one
|
||||||
|
-- that functioned, when it was the one leaving the mess.
|
||||||
|
--
|
||||||
|
-- Stopping graphical-session.target is enough on its own: every unit above is
|
||||||
|
-- PartOf= it, so they come down with it, and it carries StopWhenUnneeded=yes.
|
||||||
|
-- It is stopped rather than the units listed individually so that a unit added
|
||||||
|
-- to the start handler later cannot be forgotten here.
|
||||||
|
hl.on("hyprland.shutdown", function()
|
||||||
|
hl.exec_cmd("systemctl --user stop graphical-session.target")
|
||||||
|
end)
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Environment
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
-- prefers these live in ~/.config/uwsm/env and ~/.config/uwsm/env-hyprland
|
||||||
|
-- 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.
|
||||||
|
--
|
||||||
|
-- /dev/dri/cardN numbering is not stable across boots, and the by-path names
|
||||||
|
-- contain colons -- which are AQ_DRM_DEVICES' own separator, so they cannot be
|
||||||
|
-- used directly. Panama installs a udev rule creating colon-free stable
|
||||||
|
-- symlinks (/dev/dri/amd-dgpu, /dev/dri/amd-igpu); see
|
||||||
|
-- config/copy/etc/udev/rules.d/99-panama-gpu.rules
|
||||||
|
--
|
||||||
|
-- First entry is the primary renderer. The iGPU stays in the list so anything
|
||||||
|
-- plugged into its outputs still lights up.
|
||||||
|
--
|
||||||
|
-- Guarded on the symlink actually existing: this config is shared across
|
||||||
|
-- machines, and pointing AQ_DRM_DEVICES at a device that isn't there stops
|
||||||
|
-- Hyprland from starting at all. If the udev rule hasn't been installed (or
|
||||||
|
-- this is a different box), we say nothing and let aquamarine choose.
|
||||||
|
local function exists(path)
|
||||||
|
local f = io.open(path, "r")
|
||||||
|
if f then f:close() return true end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if exists("/dev/dri/amd-dgpu") then
|
||||||
|
local devices = "/dev/dri/amd-dgpu"
|
||||||
|
if exists("/dev/dri/amd-igpu") then
|
||||||
|
devices = devices .. ":/dev/dri/amd-igpu"
|
||||||
|
end
|
||||||
|
hl.env("AQ_DRM_DEVICES", devices)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Cursor ──────────────────────────────────────────────────────────────────
|
||||||
|
-- Theme carried over from GNOME.
|
||||||
|
--
|
||||||
|
-- Deliberately no HYPRCURSOR_THEME: oreo_blue_cursors is a plain XCursor theme
|
||||||
|
-- (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", prefs.get("cursorTheme", "oreo_blue_cursors"))
|
||||||
|
hl.env("XCURSOR_SIZE", "24")
|
||||||
|
|
||||||
|
-- ── Toolkits ────────────────────────────────────────────────────────────────
|
||||||
|
-- Wayland first, X11 only as a fallback for apps that genuinely need it.
|
||||||
|
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/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
|
||||||
|
-- only the first token, so the commonly-copied "qt5ct:qt6ct" silently means
|
||||||
|
-- just "qt5ct".
|
||||||
|
hl.env("QT_QPA_PLATFORMTHEME", "gtk3")
|
||||||
|
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
|
||||||
|
hl.env("QT_AUTO_SCREEN_SCALE_FACTOR", "1")
|
||||||
|
hl.env("GDK_BACKEND", "wayland,x11")
|
||||||
|
hl.env("SDL_VIDEODRIVER", "wayland")
|
||||||
|
hl.env("CLUTTER_BACKEND", "wayland")
|
||||||
|
hl.env("MOZ_ENABLE_WAYLAND", "1")
|
||||||
|
|
||||||
|
-- Electron apps (Slack, Obsidian, Claude, Codex, BlueBubbles, Podman Desktop)
|
||||||
|
-- pick Wayland from this rather than needing per-app flags.
|
||||||
|
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
|
||||||
|
|
||||||
|
-- ── Desktop identity ────────────────────────────────────────────────────────
|
||||||
|
-- Portals and some GNOME apps key off these. uwsm sets them itself, so these
|
||||||
|
-- are only load-bearing for the plain (non-uwsm) session.
|
||||||
|
hl.env("XDG_CURRENT_DESKTOP", "Hyprland")
|
||||||
|
hl.env("XDG_SESSION_TYPE", "wayland")
|
||||||
|
hl.env("XDG_SESSION_DESKTOP", "Hyprland")
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# hypridle — idle behavior
|
||||||
|
#
|
||||||
|
# Mirrors the GNOME power profile this replaces, exactly:
|
||||||
|
# org.gnome.desktop.session idle-delay = 300 (screen off at 5min)
|
||||||
|
# org.gnome.desktop.screensaver lock-delay = 300 (lock 5min after that)
|
||||||
|
# org.gnome.settings-daemon.plugins.power
|
||||||
|
# sleep-inactive-ac-type = nothing (never auto-suspend)
|
||||||
|
# power-button-action = nothing
|
||||||
|
#
|
||||||
|
# So: screen off at 5 minutes, lock at 10, never suspend. This is a desktop.
|
||||||
|
#
|
||||||
|
# hyprlang syntax, not Lua — hypridle is a separate project from Hyprland.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
general {
|
||||||
|
# `pidof` guard prevents stacking lockers if this fires twice.
|
||||||
|
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" })'
|
||||||
|
|
||||||
|
# 2 = auto. Holds off sleep until the locker is actually up.
|
||||||
|
inhibit_sleep = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5 minutes -> screen off.
|
||||||
|
listener {
|
||||||
|
timeout = 300
|
||||||
|
on-timeout = hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
|
||||||
|
on-resume = hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 10 minutes -> lock. Matches GNOME's lock-delay of 300s after blanking.
|
||||||
|
listener {
|
||||||
|
timeout = 600
|
||||||
|
on-timeout = loginctl lock-session
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deliberately no suspend listener: sleep-inactive-ac-type was 'nothing'.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Panama :: Hyprland
|
||||||
|
--
|
||||||
|
-- Hyprland 0.55 replaced hyprlang (hyprland.conf) with Lua. The .conf format
|
||||||
|
-- still loads as a legacy fallback in 0.56 but is removed in 0.57, so this
|
||||||
|
-- config is Lua-native. Reference for every option below:
|
||||||
|
-- /usr/share/hypr/stubs/hl.meta.lua <- authoritative for THIS build
|
||||||
|
-- /usr/share/hypr/hyprland.lua <- upstream example
|
||||||
|
--
|
||||||
|
-- Entry point only. Each require() is its own error scope, so a mistake in one
|
||||||
|
-- module doesn't take the whole config down with it.
|
||||||
|
--
|
||||||
|
-- Reload: hyprctl reload
|
||||||
|
-- Check: Hyprland --verify-config
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Shared preferences first: looks/input/monitors read their defaults through it.
|
||||||
|
-- It never raises, so a missing or malformed settings file costs customizations
|
||||||
|
-- and nothing else.
|
||||||
|
require("prefs")
|
||||||
|
|
||||||
|
require("env")
|
||||||
|
require("monitors")
|
||||||
|
require("looks")
|
||||||
|
require("input")
|
||||||
|
require("rules")
|
||||||
|
require("keybinds")
|
||||||
|
require("autostart")
|
||||||
|
|
||||||
|
-- Per-machine overrides, loaded last so it wins. Ships effectively empty.
|
||||||
|
require("overrides")
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# hyprlock — lock screen.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
general {
|
||||||
|
hide_cursor = true
|
||||||
|
|
||||||
|
# 2 = auto. Matters at the 1.5x fractional scale this display runs.
|
||||||
|
fractional_scaling = 2
|
||||||
|
|
||||||
|
# 0 = GPU-accelerated screencopy. 1 is a CPU fallback and is very slow at
|
||||||
|
# 4500x3000.
|
||||||
|
screencopy_mode = 0
|
||||||
|
|
||||||
|
fail_timeout = 2000
|
||||||
|
}
|
||||||
|
|
||||||
|
auth {
|
||||||
|
pam:enabled = true
|
||||||
|
pam:module = hyprlock
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Background ──────────────────────────────────────────────────────────────
|
||||||
|
# `path = screenshot` grabs the desktop at lock time and blurs it in place,
|
||||||
|
# which is what GNOME does.
|
||||||
|
#
|
||||||
|
# Caveat: this uses screencopy. If the desktop is ever switched to full-time
|
||||||
|
# HDR (see hypr/overrides.lua), this background may come back black — that is
|
||||||
|
# the same upstream screencopy bug, not a hyprlock misconfiguration.
|
||||||
|
background {
|
||||||
|
monitor =
|
||||||
|
path = screenshot
|
||||||
|
|
||||||
|
blur_passes = 3
|
||||||
|
blur_size = 8
|
||||||
|
noise = 0.0117
|
||||||
|
contrast = 0.9
|
||||||
|
brightness = 0.8
|
||||||
|
vibrancy = 0.17
|
||||||
|
vibrancy_darkness = 0.05
|
||||||
|
|
||||||
|
# Shown if the screenshot is unavailable.
|
||||||
|
color = rgba(@BG@, 1.0)
|
||||||
|
|
||||||
|
zindex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Clock ───────────────────────────────────────────────────────────────────
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:1000] date +"%-I:%M"
|
||||||
|
color = rgba(@FG@, 1.0)
|
||||||
|
font_size = 120
|
||||||
|
font_family = Adwaita Sans Light
|
||||||
|
position = 0, 260
|
||||||
|
halign = center
|
||||||
|
valign = center
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = cmd[update:60000] date +"%A, %B %-d"
|
||||||
|
color = rgba(@MUTED@, 1.0)
|
||||||
|
font_size = 24
|
||||||
|
font_family = Adwaita Sans
|
||||||
|
position = 0, 160
|
||||||
|
halign = center
|
||||||
|
valign = center
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Password field ──────────────────────────────────────────────────────────
|
||||||
|
input-field {
|
||||||
|
monitor =
|
||||||
|
size = 340, 52
|
||||||
|
position = 0, -40
|
||||||
|
halign = center
|
||||||
|
valign = center
|
||||||
|
|
||||||
|
outline_thickness = 2
|
||||||
|
rounding = 26
|
||||||
|
|
||||||
|
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="##@MUTED_HEX@"><i>Password</i></span>
|
||||||
|
fail_text = <span foreground="##@ERROR_HEX@"><i>$FAIL ($ATTEMPTS)</i></span>
|
||||||
|
|
||||||
|
fade_on_empty = false
|
||||||
|
hide_input = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── User ────────────────────────────────────────────────────────────────────
|
||||||
|
label {
|
||||||
|
monitor =
|
||||||
|
text = $USER
|
||||||
|
color = rgba(@FG@, 0.9)
|
||||||
|
font_size = 16
|
||||||
|
font_family = Adwaita Sans
|
||||||
|
position = 0, -110
|
||||||
|
halign = center
|
||||||
|
valign = center
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# hyprpaper — static wallpaper
|
||||||
|
#
|
||||||
|
# NOTE: hyprpaper, hypridle, hyprlock and hyprtoolkit are separate projects and
|
||||||
|
# still use hyprlang. Only Hyprland itself moved to Lua. Don't "fix" these.
|
||||||
|
#
|
||||||
|
# SYNTAX NOTE: hyprpaper 0.8 was rewritten on hyprtoolkit/hyprwire and replaced
|
||||||
|
# the old flat syntax with blocks. The pre-0.8 form
|
||||||
|
#
|
||||||
|
# preload = /path/to.jpg
|
||||||
|
# wallpaper = DP-2,/path/to.jpg
|
||||||
|
#
|
||||||
|
# is silently ignored — it parses, then logs "Monitor DP-2 has no target: no wp
|
||||||
|
# will be created" and shows nothing. Almost every guide online still uses it.
|
||||||
|
#
|
||||||
|
# Static only, by choice: animated wallpapers repaint continuously and cost
|
||||||
|
# frame time for nothing.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# One block for every output: per-monitor wallpaper is applied at runtime by
|
||||||
|
# the shell (services/Wallpaper.qml) over IPC, so a per-connector block here
|
||||||
|
# would just be one machine's connector name in everyone's config.
|
||||||
|
wallpaper {
|
||||||
|
monitor =
|
||||||
|
path = ~/Pictures/Wallpapers/faroe_islands.jpg
|
||||||
|
# cover = fill the output, cropping overflow. Matches the "zoom" picture
|
||||||
|
# option this setup used under GNOME.
|
||||||
|
fit_mode = cover
|
||||||
|
}
|
||||||
|
|
||||||
|
ipc = on
|
||||||
|
splash = false
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# hyprtoolkit — shared theme for the first-party Hypr GUI apps
|
||||||
|
#
|
||||||
|
# One file themes all of these, which is why they're preferred over the usual
|
||||||
|
# grab-bag of GTK/Qt utilities:
|
||||||
|
# hyprshutdown session exit dialog (bound to the power menu)
|
||||||
|
# hyprpwcenter PipeWire / audio routing GUI (replaces a pavucontrol/TUI)
|
||||||
|
# hyprsysteminfo system info panel
|
||||||
|
# hyprland-guiutils hyprland-dialog / hyprland-run
|
||||||
|
# hyprpolkitagent authentication prompts (via hyprland-qt-support)
|
||||||
|
#
|
||||||
|
# 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).
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
background = 0xFF1E2030 # bg_dark — window background
|
||||||
|
base = 0xFF222436 # bg — raised surfaces
|
||||||
|
alternate_base = 0xFF2F334D # bg_highlight — rows, hover
|
||||||
|
text = 0xFFC8D3F5 # fg
|
||||||
|
bright_text = 0xFFFFFFFF
|
||||||
|
link_text = 0xFF82AAFF # blue
|
||||||
|
|
||||||
|
accent = 0xFF82AAFF # blue — same accent as everything else
|
||||||
|
accent_secondary = 0xFFC099FF # magenta
|
||||||
|
|
||||||
|
font_family = Adwaita Sans
|
||||||
|
font_family_monospace = VictorMono Nerd Font
|
||||||
|
|
||||||
|
icon_theme = Adwaita
|
||||||
|
|
||||||
|
# Match the rounding used by the Quickshell popovers and the Hyprland windows.
|
||||||
|
rounding_large = 18
|
||||||
|
rounding_small = 8
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Input
|
||||||
|
--
|
||||||
|
-- Deliberately GNOME-like: click-to-focus, no focus-follows-mouse, no mouse
|
||||||
|
-- acceleration changes. The goal is that muscle memory transfers untouched.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = require("prefs")
|
||||||
|
local actions = require("actions")
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
input = {
|
||||||
|
kb_layout = prefs.get("keyboardLayout", "us"),
|
||||||
|
kb_variant = prefs.get("keyboardVariant", ""),
|
||||||
|
kb_model = "",
|
||||||
|
kb_options = prefs.get("keyboardOptions", "caps:escape_shifted_capslock"),
|
||||||
|
kb_rules = "",
|
||||||
|
|
||||||
|
numlock_by_default = prefs.get("numlockByDefault", true),
|
||||||
|
|
||||||
|
-- GNOME's defaults are 500ms delay / 33Hz repeat.
|
||||||
|
repeat_delay = prefs.get("keyRepeatDelay", 500),
|
||||||
|
repeat_rate = prefs.get("keyRepeatRate", 33),
|
||||||
|
|
||||||
|
-- 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),
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
|
||||||
|
-- 0 = NEXT, the compositor's own default: focus goes to the next window
|
||||||
|
-- in the layout order. `hyprctl descriptions` publishes
|
||||||
|
-- map: [{"mru":2},{"cursor":1},{"next":0}], and Settings offers all
|
||||||
|
-- three; 0 is repeated here so nothing changes on a machine with no
|
||||||
|
-- settings file.
|
||||||
|
focus_on_close = prefs.getInt("focusOnClose", 0),
|
||||||
|
|
||||||
|
-- Flat pointer response by default, no acceleration. Matters for
|
||||||
|
-- gaming; Settings offers adaptive for people who want it back.
|
||||||
|
sensitivity = prefs.get("pointerSensitivity", 0),
|
||||||
|
accel_profile = prefs.get("accelProfile", "flat"),
|
||||||
|
|
||||||
|
natural_scroll = prefs.get("naturalScroll", false),
|
||||||
|
scroll_factor = prefs.get("scrollFactor", 1.0),
|
||||||
|
left_handed = prefs.get("leftHanded", false),
|
||||||
|
|
||||||
|
-- Empty is a value, not an omission: it is what `hyprctl getoption`
|
||||||
|
-- reports as "[[EMPTY]]" before anything writes the option, and it means
|
||||||
|
-- "let libinput pick per device" -- two fingers on a touchpad, the wheel
|
||||||
|
-- on a mouse. Writing it back explicitly is the same branch a stock
|
||||||
|
-- Hyprland takes, and it keeps the setting reversible: without an empty
|
||||||
|
-- choice in the schema there would be no way back from a scroll method
|
||||||
|
-- once one was picked. Settings offers 2fg / edge / on_button_down /
|
||||||
|
-- no_scroll alongside it, the four words the option's own description
|
||||||
|
-- names (it publishes no map).
|
||||||
|
scroll_method = prefs.get("scrollMethod", ""),
|
||||||
|
|
||||||
|
-- Only consulted while scroll_method is on_button_down. 0 means the
|
||||||
|
-- device's own middle button.
|
||||||
|
scroll_button = prefs.get("scrollButton", 0),
|
||||||
|
|
||||||
|
-- Clicking a floating window raises and focuses it.
|
||||||
|
float_switch_override_focus = 2,
|
||||||
|
|
||||||
|
-- Every touchpad preference Settings offers is read here as well as
|
||||||
|
-- applied live: the live half (hyprctl eval) reaches a running
|
||||||
|
-- compositor, and THIS half is what survives `hyprctl reload` and the
|
||||||
|
-- gap before the shell starts. A schema `hypr:` entry without a
|
||||||
|
-- prefs.get() here is a control that quietly reverts -- the whole
|
||||||
|
-- Mouse & Touchpad page once had exactly that bug. Defaults must
|
||||||
|
-- match PreferenceSchema's; the hypr-prefs-contract checks both.
|
||||||
|
touchpad = {
|
||||||
|
tap_to_click = prefs.get("touchpadTapToClick", true),
|
||||||
|
natural_scroll = prefs.get("touchpadNaturalScroll", true),
|
||||||
|
disable_while_typing = prefs.get("touchpadDisableWhileTyping", true),
|
||||||
|
scroll_factor = prefs.get("touchpadScrollFactor", 1.0),
|
||||||
|
drag_lock = prefs.getInt("touchpadDragLock", 0),
|
||||||
|
middle_button_emulation = prefs.get("touchpadMiddleButtonEmulation", false),
|
||||||
|
clickfinger_behavior = prefs.get("touchpadClickfinger", false),
|
||||||
|
|
||||||
|
-- Underscores here, hyphens in the option name getoption answers to
|
||||||
|
-- (input:touchpad:tap-and-drag) -- the same split tap_to_click has
|
||||||
|
-- at the top of this table. Hyprland's own default is true; it is
|
||||||
|
-- repeated rather than omitted so the schema, the Lua, and the
|
||||||
|
-- compositor all state the same value.
|
||||||
|
tap_and_drag = prefs.get("touchpadTapAndDrag", true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Pointer BEHAVIOUR, as opposed to pointer appearance: how the cursor reacts
|
||||||
|
-- to typing and to workspace switches. The cursor's looks -- theme, size,
|
||||||
|
-- hardware cursors, the inactivity fade -- are a separate cursor table in
|
||||||
|
-- looks.lua. hl.config calls are additive per option, so the two tables
|
||||||
|
-- coexist; they are split by what a person would go looking for, and these
|
||||||
|
-- two appear on Settings' Mouse & Touchpad page rather than in Appearance.
|
||||||
|
cursor = {
|
||||||
|
hide_on_key_press = prefs.get("cursorHideWhileTyping", false),
|
||||||
|
|
||||||
|
-- An integer with three states, written from a switch: disable = 0,
|
||||||
|
-- enable = 1, force = 2. Settings offers the first two, so getInt is
|
||||||
|
-- what bridges a stored boolean to the number Hyprland wants -- the same
|
||||||
|
-- pairing render.cm_auto_hdr uses in looks.lua.
|
||||||
|
warp_on_change_workspace = prefs.getInt("cursorWarpOnWorkspaceChange", 0),
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Tuning for the three-finger gestures registered below.
|
||||||
|
gestures = {
|
||||||
|
workspace_swipe_distance = prefs.getInt("swipeDistance", 300),
|
||||||
|
workspace_swipe_invert = prefs.get("swipeInvert", true),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Touchpad gestures ───────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- GNOME's gestures, reproduced: three fingers sideways moves between
|
||||||
|
-- workspaces, three fingers up opens the overview, three fingers down closes
|
||||||
|
-- it. That is the same muscle memory the keybinds were built to preserve.
|
||||||
|
--
|
||||||
|
-- Registered unconditionally rather than behind a preference. Hyprland 0.56
|
||||||
|
-- dropped `gestures:workspace_swipe` in favour of this `gesture` keyword, and a
|
||||||
|
-- registration is read at config time -- so a toggle would need a reload to
|
||||||
|
-- take effect, which is worse than the nothing these cost on a machine with no
|
||||||
|
-- touchpad. What IS tunable at runtime lives in Settings: how far a swipe has
|
||||||
|
-- to travel, and which way round it goes.
|
||||||
|
--
|
||||||
|
-- open and close rather than toggle twice: with a toggle on both directions,
|
||||||
|
-- swiping up from an already-open overview would close it, and swiping down
|
||||||
|
-- would reopen it. GNOME does not do that, and neither does this.
|
||||||
|
local overview = function(fn)
|
||||||
|
return function() hl.exec_cmd("qs ipc call overview " .. fn) end
|
||||||
|
end
|
||||||
|
|
||||||
|
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||||
|
hl.gesture({ fingers = 3, direction = "up", action = overview("open") })
|
||||||
|
hl.gesture({ fingers = 3, direction = "down", action = overview("close") })
|
||||||
|
|
||||||
|
-- ── Four-finger gestures ────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- The three above are the desktop's, fixed. These four are the user's: each
|
||||||
|
-- holds a named action from settings.json, or {} for unassigned, and the
|
||||||
|
-- vocabulary is exactly the one custom shortcuts use -- actions.lua resolves
|
||||||
|
-- both, through the same whitelist tables, so a gesture can no more introduce
|
||||||
|
-- a command than a keybind can.
|
||||||
|
--
|
||||||
|
-- Nothing is emitted for an unassigned direction. That matters more here than
|
||||||
|
-- it looks: a registration is read at config time and there is no way to
|
||||||
|
-- remove one afterwards, so emitting a no-op gesture for every direction would
|
||||||
|
-- consume the four-finger swipes permanently, including for whatever the
|
||||||
|
-- compositor might do with them later.
|
||||||
|
--
|
||||||
|
-- Four rather than three because three is spoken for, and because four fingers
|
||||||
|
-- is the largest number of them a touchpad this size can tell apart.
|
||||||
|
local custom_gestures = {
|
||||||
|
{ pref = "gestureFourUp", direction = "up" },
|
||||||
|
{ pref = "gestureFourDown", direction = "down" },
|
||||||
|
{ pref = "gestureFourLeft", direction = "left" },
|
||||||
|
{ pref = "gestureFourRight", direction = "right" },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gesture in ipairs(custom_gestures) do
|
||||||
|
local action = actions.gesture(prefs.get(gesture.pref, {}))
|
||||||
|
if action ~= nil then
|
||||||
|
hl.gesture({ fingers = 4, direction = gesture.direction, action = action })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Keybindings
|
||||||
|
--
|
||||||
|
-- Ported 1:1 from the GNOME + Forge setup this replaces. Where Hyprland has no
|
||||||
|
-- equivalent, the deviation is called out in a comment rather than silently
|
||||||
|
-- dropped.
|
||||||
|
--
|
||||||
|
-- The mental model, unchanged from Forge:
|
||||||
|
-- SUPER -> act on windows (focus / move / swap / resize)
|
||||||
|
-- ALT -> act on workspaces
|
||||||
|
-- SUPER + CTRL -> change layout structure (split, float, swap)
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = require("prefs")
|
||||||
|
local actions = require("actions")
|
||||||
|
|
||||||
|
local mod = "SUPER"
|
||||||
|
|
||||||
|
-- Programs, matched to the GNOME custom keybindings and media-key settings.
|
||||||
|
local terminal = "kitty"
|
||||||
|
local editor = "kitty nvim ."
|
||||||
|
local browser = "helium-browser-bin"
|
||||||
|
local files = "nautilus --new-window"
|
||||||
|
local calculator = "gnome-calculator"
|
||||||
|
local mail = "flatpak run org.mozilla.thunderbird_esr"
|
||||||
|
-- Panama owns the Hyprland and shell controls. GNOME Settings remains installed
|
||||||
|
-- and searchable in Vicinae for its hardware/account panels.
|
||||||
|
local settings = "qs ipc call settings toggle"
|
||||||
|
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
|
||||||
|
|
||||||
|
local lid = function(action)
|
||||||
|
return "$HOME/.config/quickshell/scripts/panama-lid " .. action
|
||||||
|
end
|
||||||
|
|
||||||
|
local dictate = function(action)
|
||||||
|
return "$HOME/.config/quickshell/scripts/panama-dictate " .. action
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Quickshell IPC targets. See quickshell/shell.qml for the handlers.
|
||||||
|
local qs = function(target, fn) return "qs ipc call " .. target .. " " .. fn end
|
||||||
|
|
||||||
|
-- ── Applications ────────────────────────────────────────────────────────────
|
||||||
|
-- ── User rebinding ──────────────────────────────────────────────────────────
|
||||||
|
-- Every bind below goes through `bind` rather than `hl.bind` directly, so a
|
||||||
|
-- chord can be replaced from Panama Settings without this file changing.
|
||||||
|
--
|
||||||
|
-- Overrides are keyed by the bind's SHIPPED chord, and ONLY the chord is taken
|
||||||
|
-- from settings -- the action is always the Lua value written here. A stored
|
||||||
|
-- override can therefore move a shortcut but can never make one do something
|
||||||
|
-- else, which is the property that makes reading them from a JSON file the
|
||||||
|
-- user can edit safe.
|
||||||
|
--
|
||||||
|
-- Keyed by chord rather than by description because chords are unique and
|
||||||
|
-- descriptions are not: "Calculator" is both SUPER+C and the XF86Calculator
|
||||||
|
-- hardware key, and keying by description moved both of them onto the same new
|
||||||
|
-- chord, silently costing the hardware key.
|
||||||
|
--
|
||||||
|
-- An override whose chord is not a plausible chord is ignored and the shipped
|
||||||
|
-- one is used, so a hand-edited settings file cannot cost you a keymap.
|
||||||
|
|
||||||
|
local overrides = prefs.get("keybindOverrides", {})
|
||||||
|
|
||||||
|
local function valid_chord(chord)
|
||||||
|
if type(chord) ~= "string" or chord == "" or #chord > 64 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
-- "SUPER + SHIFT + K", "XF86AudioPlay", "Print", "SUPER + mouse:272"
|
||||||
|
return chord:match("^[%w_+%s:]+$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Categories ──────────────────────────────────────────────────────────────
|
||||||
|
-- What a bind is FOR, as opposed to what it does.
|
||||||
|
--
|
||||||
|
-- The cheatsheet groups by this, and the Shortcuts settings page uses it too.
|
||||||
|
-- It is recorded here rather than guessed from the description, which is what
|
||||||
|
-- Keybinds.qml used to do: matching substrings put "Close window" and "Close
|
||||||
|
-- the notification list" in the same group and left anything phrased unusually
|
||||||
|
-- in whichever bucket matched first.
|
||||||
|
--
|
||||||
|
-- The sections of this file already ARE the categories, so a section sets one
|
||||||
|
-- and every bind below it inherits it. That keeps the annotation to one line
|
||||||
|
-- per section instead of one per bind, and makes the grouping impossible to
|
||||||
|
-- forget: a new bind lands in the category of the section it was written in.
|
||||||
|
local categories = {}
|
||||||
|
local current_category = "Other"
|
||||||
|
|
||||||
|
local function category(name)
|
||||||
|
current_category = name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bind(chord, action, opts)
|
||||||
|
local override = overrides[chord]
|
||||||
|
if valid_chord(override) then
|
||||||
|
chord = override
|
||||||
|
end
|
||||||
|
-- Keyed by the chord actually bound, so the shell can join on what
|
||||||
|
-- hyprctl reports without having to know about overrides.
|
||||||
|
categories[chord] = current_category
|
||||||
|
return hl.bind(chord, action, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Written where the shell can read it. Hyprland reports a Lua bind's
|
||||||
|
-- dispatcher as `__lua` with a bytecode offset, so there is no way to attach
|
||||||
|
-- anything to a bind that survives into `hyprctl binds` -- the manifest is
|
||||||
|
-- how this side of the desktop tells the other what these binds are for.
|
||||||
|
--
|
||||||
|
-- Never raises. A read-only or missing state directory costs the categories,
|
||||||
|
-- which the shell falls back from, and must never cost the keymap.
|
||||||
|
local function write_categories()
|
||||||
|
local state_home = os.getenv("XDG_STATE_HOME")
|
||||||
|
if state_home == nil or state_home == "" then
|
||||||
|
local home = os.getenv("HOME")
|
||||||
|
if home == nil or home == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state_home = home .. "/.local/state"
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
for chord, name in pairs(categories) do
|
||||||
|
-- Chords and category names are both from this file, so the only
|
||||||
|
-- escaping that can matter is the quote character itself.
|
||||||
|
parts[#parts + 1] = string.format('%q:%q', chord, name)
|
||||||
|
end
|
||||||
|
table.sort(parts)
|
||||||
|
|
||||||
|
local path = state_home .. "/panama/keybind-categories.json"
|
||||||
|
os.execute("mkdir -p " .. string.format("%q", state_home .. "/panama"))
|
||||||
|
local file = io.open(path, "w")
|
||||||
|
if file == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
file:write("{" .. table.concat(parts, ",") .. "}\n")
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- SUPER opens a new one. SUPER+ALT goes to the one you already have.
|
||||||
|
--
|
||||||
|
-- That order matters and was chosen deliberately after trying the reverse.
|
||||||
|
-- Making the plain key focus an existing window reads well in a demo and is
|
||||||
|
-- what macOS does, but it makes "give me another terminal" the awkward case --
|
||||||
|
-- and on a tiling desktop, opening a second terminal beside the first is not
|
||||||
|
-- an edge case, it is the normal way to work. So the plain key keeps doing
|
||||||
|
-- what it has always done, and the modifier is the new capability rather than
|
||||||
|
-- a tax on the old one.
|
||||||
|
--
|
||||||
|
-- ALT rather than SHIFT because SUPER+SHIFT is already the window-manipulation
|
||||||
|
-- space: Files, Neovim and Settings would have collided with Focus session,
|
||||||
|
-- Taller and Shorter, and breaking two keys out of the eight-key resize set to
|
||||||
|
-- make room is a worse trade than borrowing a modifier.
|
||||||
|
--
|
||||||
|
-- The go-to binds still launch when nothing is open. A key that silently does
|
||||||
|
-- nothing is worse than one that does the obvious thing.
|
||||||
|
--
|
||||||
|
-- Patterns are regular expressions and are anchored. An unanchored "mail"
|
||||||
|
-- would match gmail-notifier, and the go-to-mail key would raise somebody's
|
||||||
|
-- notifier instead. Single-quoted for the shell so a backslash reaches the
|
||||||
|
-- matcher rather than being eaten on the way.
|
||||||
|
local function shell_quote(value)
|
||||||
|
return "'" .. value:gsub("'", "'\\''") .. "'"
|
||||||
|
end
|
||||||
|
|
||||||
|
local launcher_bin = "$HOME/.local/share/Panama/bin/panama-launch"
|
||||||
|
|
||||||
|
local function go_to(class, command, title)
|
||||||
|
local parts = { launcher_bin, "--class", shell_quote(class) }
|
||||||
|
if title then
|
||||||
|
parts[#parts + 1] = "--title"
|
||||||
|
parts[#parts + 1] = shell_quote(title)
|
||||||
|
end
|
||||||
|
parts[#parts + 1] = "--"
|
||||||
|
parts[#parts + 1] = command
|
||||||
|
return table.concat(parts, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
category("Applications")
|
||||||
|
bind(mod .. " + T", hl.dsp.exec_cmd(terminal), { description = "Terminal" })
|
||||||
|
bind(mod .. " + N", hl.dsp.exec_cmd(editor), { description = "Neovim" })
|
||||||
|
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" })
|
||||||
|
|
||||||
|
-- Go to the one already open, or start it if there is none.
|
||||||
|
bind(mod .. " + ALT + T", hl.dsp.exec_cmd(go_to("^kitty$", terminal)),
|
||||||
|
{ description = "Go to terminal" })
|
||||||
|
bind(mod .. " + ALT + N", hl.dsp.exec_cmd(go_to("^kitty$", editor, "nvim")),
|
||||||
|
{ description = "Go to Neovim" })
|
||||||
|
bind(mod .. " + ALT + W", hl.dsp.exec_cmd(go_to("^helium", browser)),
|
||||||
|
{ description = "Go to browser" })
|
||||||
|
bind(mod .. " + ALT + F", hl.dsp.exec_cmd(go_to("^org\\.gnome\\.Nautilus$", files)),
|
||||||
|
{ description = "Go to files" })
|
||||||
|
bind(mod .. " + ALT + C", hl.dsp.exec_cmd(go_to("^org\\.gnome\\.Calculator$", calculator)),
|
||||||
|
{ description = "Go to calculator" })
|
||||||
|
bind(mod .. " + ALT + E", hl.dsp.exec_cmd(go_to("^org\\.mozilla\\.thunderbird", mail)),
|
||||||
|
{ description = "Go to mail" })
|
||||||
|
bind(mod .. " + I", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||||
|
bind("CTRL + SHIFT + Escape", hl.dsp.exec_cmd(sysmonitor), { description = "System monitor" })
|
||||||
|
|
||||||
|
-- ── Launcher ────────────────────────────────────────────────────────────────
|
||||||
|
category("Applications")
|
||||||
|
-- All three keys open the same launcher, on purpose: SUPER+A and SUPER+R were
|
||||||
|
-- the GNOME app-grid and run-dialog shortcuts, and SUPER+SPACE is here as a
|
||||||
|
-- third option to settle on. Vicinae covers apps, calculator, files, clipboard,
|
||||||
|
-- emoji and window switching, so a separate run dialog and app grid are gone.
|
||||||
|
bind(mod .. " + A", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||||
|
bind(mod .. " + R", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||||
|
bind(mod .. " + Space", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||||
|
|
||||||
|
-- Emergency fallback launcher. Vicinae runs as a systemd user service and the
|
||||||
|
-- bar is Quickshell; if either fails to come up, this is how you start an
|
||||||
|
-- application without dropping to a TTY. Depends on nothing but wofi itself.
|
||||||
|
bind(mod .. " + SHIFT + R", hl.dsp.exec_cmd("wofi"), { description = "Fallback launcher" })
|
||||||
|
|
||||||
|
-- Every shortcut, on one key. Slash because "what are the keys" is a question,
|
||||||
|
-- and because it is the one punctuation key no other bind wants.
|
||||||
|
bind(mod .. " + slash", hl.dsp.exec_cmd(qs("cheatsheet", "toggle")), { description = "Keyboard shortcuts" })
|
||||||
|
|
||||||
|
-- Clipboard history and emoji, straight into the relevant launcher view.
|
||||||
|
-- Deeplink form is the one from vicinae's own Hyprland quickstart.
|
||||||
|
bind(mod .. " + V", hl.dsp.exec_cmd("vicinae vicinae://launch/clipboard/history"),
|
||||||
|
{ description = "Clipboard history" })
|
||||||
|
bind(mod .. " + Period", hl.dsp.exec_cmd("vicinae vicinae://launch/emoji/search"),
|
||||||
|
{ description = "Emoji picker" })
|
||||||
|
|
||||||
|
-- ── Shell surfaces (Quickshell) ─────────────────────────────────────────────
|
||||||
|
category("Shell")
|
||||||
|
-- SUPER+S was GNOME's quick settings; kept.
|
||||||
|
bind(mod .. " + S", hl.dsp.exec_cmd(qs("quicksettings", "toggle")), { description = "Quick settings" })
|
||||||
|
|
||||||
|
-- Start a 45-minute focus session on the current workspace, or reveal its
|
||||||
|
-- Signal Glass controls if one is already running.
|
||||||
|
bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd(qs("focus", "reveal")), { description = "Focus session" })
|
||||||
|
|
||||||
|
-- Workspace overview. GNOME put this on a bare SUPER tap; tap-detection on a
|
||||||
|
-- modifier misfires when you're fast with SUPER+key combos, so it lives on a
|
||||||
|
-- real chord instead. SUPER+grave was Forge's "cycle windows of same app",
|
||||||
|
-- which Hyprland has no equivalent for.
|
||||||
|
bind(mod .. " + grave", hl.dsp.exec_cmd(qs("overview", "toggle")), { description = "Overview" })
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
-- whether to capture or record -- reproducing GNOME's Print-screen UI.
|
||||||
|
bind("Print", hl.dsp.exec_cmd(qs("capture", "open")), { description = "Screenshot / record" })
|
||||||
|
-- The GNOME direct-capture variants, kept as shortcuts past the picker.
|
||||||
|
bind("SHIFT + Print", hl.dsp.exec_cmd(qs("capture", "screenNow")), { description = "Screenshot: whole screen" })
|
||||||
|
bind("ALT + Print", hl.dsp.exec_cmd(qs("capture", "windowNow")), { description = "Screenshot: window" })
|
||||||
|
|
||||||
|
-- Local OCR and QR/barcode recognition through the same region picker. This
|
||||||
|
-- opens directly in Selection + Read mode; Print still exposes every mode.
|
||||||
|
bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd(qs("screen-intelligence", "open")),
|
||||||
|
{ description = "Screen Intelligence" })
|
||||||
|
|
||||||
|
-- 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" })
|
||||||
|
|
||||||
|
-- ── Magnifier ───────────────────────────────────────────────────────────────
|
||||||
|
category("Shell")
|
||||||
|
--
|
||||||
|
-- The chords are NOT the obvious SUPER+=/-/0. SUPER+equal is already "Reset
|
||||||
|
-- split" (Window management, below), and taking a daily tiling key away to
|
||||||
|
-- give the magnifier the prettiest chord on the keyboard is the wrong trade.
|
||||||
|
--
|
||||||
|
-- SUPER+ALT is where they went instead, which is also where GNOME's magnifier
|
||||||
|
-- lives: gsettings' magnifier-zoom-in / magnifier-zoom-out ship as
|
||||||
|
-- <Alt><Super>= and <Alt><Super>-, so this is the shortcut the machine this
|
||||||
|
-- desktop replaced already had. SUPER+ALT+0 -- free; the workspace digits are
|
||||||
|
-- plain ALT -- resets to 1.00 ×, reading as "back to zero magnification".
|
||||||
|
--
|
||||||
|
-- These go THROUGH the shell rather than calling `hyprctl keyword
|
||||||
|
-- cursor:zoom_factor` directly. Setting the compositor option behind Panama's
|
||||||
|
-- back would leave the stored preference and the Magnifier slider claiming a
|
||||||
|
-- magnification that is not the one on screen; the IPC call commits through
|
||||||
|
-- the same verified-preference path the slider uses, so the store, the
|
||||||
|
-- compositor and the settings page can never disagree. It also posts the OSD,
|
||||||
|
-- which is the only way to see what the factor now is with the pointer
|
||||||
|
-- somewhere else entirely.
|
||||||
|
--
|
||||||
|
-- Not `repeating`: the step is multiplicative (×1.25), so a held key repeating
|
||||||
|
-- at the keyboard rate would arrive at the 5.00 × ceiling in about a tenth of
|
||||||
|
-- a second. One press, one step.
|
||||||
|
--
|
||||||
|
-- Written as literal chords rather than `mod .. " + ALT + ..."` (as
|
||||||
|
-- "SUPER + Backspace" already is, above) because these three are the most
|
||||||
|
-- collision-prone binds in the file -- they were placed around one -- and a
|
||||||
|
-- literal is the form both the duplicate-chord check and the settings page's
|
||||||
|
-- chord display can actually read.
|
||||||
|
bind("SUPER + ALT + equal", hl.dsp.exec_cmd(qs("accessibility", "zoom in")),
|
||||||
|
{ description = "Zoom in" })
|
||||||
|
bind("SUPER + ALT + minus", hl.dsp.exec_cmd(qs("accessibility", "zoom out")),
|
||||||
|
{ description = "Zoom out" })
|
||||||
|
bind("SUPER + ALT + 0", hl.dsp.exec_cmd(qs("accessibility", "zoom reset")),
|
||||||
|
{ description = "Reset zoom" })
|
||||||
|
|
||||||
|
-- ── Window management ───────────────────────────────────────────────────────
|
||||||
|
category("Windows")
|
||||||
|
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||||
|
bind(mod .. " + U", hl.dsp.window.fullscreen({ mode = "fullscreen" }), { description = "Fullscreen" })
|
||||||
|
|
||||||
|
-- Forge: window-toggle-float / window-toggle-always-float.
|
||||||
|
-- "Always float" has no Hyprland equivalent (it wrote a persistent rule); pin
|
||||||
|
-- is the nearest useful thing -- the window floats above every workspace.
|
||||||
|
bind(mod .. " + CTRL + C", hl.dsp.window.float({ action = "toggle" }), { description = "Toggle float" })
|
||||||
|
bind(mod .. " + CTRL + SHIFT + C", hl.dsp.window.pin({ action = "toggle" }), { description = "Pin window" })
|
||||||
|
|
||||||
|
-- Forge: con-split-layout-toggle / con-split-horizontal / con-split-vertical.
|
||||||
|
bind(mod .. " + CTRL + G", hl.dsp.layout("togglesplit"), { description = "Toggle split direction" })
|
||||||
|
bind(mod .. " + CTRL + Z", hl.dsp.layout("preselect r"), { description = "Next window splits right" })
|
||||||
|
bind(mod .. " + CTRL + V", hl.dsp.layout("preselect d"), { description = "Next window splits down" })
|
||||||
|
|
||||||
|
-- Forge: window-shrink / window-expand / window-reset-sizes.
|
||||||
|
bind(mod .. " + bracketleft", hl.dsp.layout("splitratio -0.05"), { repeating = true, description = "Shrink" })
|
||||||
|
bind(mod .. " + bracketright", hl.dsp.layout("splitratio +0.05"), { repeating = true, description = "Expand" })
|
||||||
|
bind(mod .. " + equal", hl.dsp.layout("splitratio exact 0.5"), { description = "Reset split" })
|
||||||
|
|
||||||
|
-- Focus (Forge: window-focus-*). Both vim keys and arrows, as in Forge.
|
||||||
|
bind(mod .. " + H", hl.dsp.focus({ direction = "l" }), { description = "Focus left" })
|
||||||
|
bind(mod .. " + J", hl.dsp.focus({ direction = "d" }), { description = "Focus down" })
|
||||||
|
bind(mod .. " + K", hl.dsp.focus({ direction = "u" }), { description = "Focus up" })
|
||||||
|
bind(mod .. " + L", hl.dsp.focus({ direction = "r" }), { description = "Focus right" })
|
||||||
|
bind(mod .. " + left", hl.dsp.focus({ direction = "l" }), { description = "Focus left" })
|
||||||
|
bind(mod .. " + down", hl.dsp.focus({ direction = "d" }), { description = "Focus down" })
|
||||||
|
bind(mod .. " + up", hl.dsp.focus({ direction = "u" }), { description = "Focus up" })
|
||||||
|
bind(mod .. " + right", hl.dsp.focus({ direction = "r" }), { description = "Focus right" })
|
||||||
|
|
||||||
|
-- Move (Forge: window-move-*).
|
||||||
|
bind(mod .. " + SHIFT + H", hl.dsp.window.move({ direction = "l" }), { description = "Move window left" })
|
||||||
|
bind(mod .. " + SHIFT + J", hl.dsp.window.move({ direction = "d" }), { description = "Move window down" })
|
||||||
|
bind(mod .. " + SHIFT + K", hl.dsp.window.move({ direction = "u" }), { description = "Move window up" })
|
||||||
|
bind(mod .. " + SHIFT + L", hl.dsp.window.move({ direction = "r" }), { description = "Move window right" })
|
||||||
|
|
||||||
|
-- Swap (Forge: window-swap-*).
|
||||||
|
bind(mod .. " + CTRL + H", hl.dsp.window.swap({ direction = "l" }), { description = "Swap left" })
|
||||||
|
bind(mod .. " + CTRL + J", hl.dsp.window.swap({ direction = "d" }), { description = "Swap down" })
|
||||||
|
bind(mod .. " + CTRL + K", hl.dsp.window.swap({ direction = "u" }), { description = "Swap up" })
|
||||||
|
bind(mod .. " + CTRL + L", hl.dsp.window.swap({ direction = "r" }), { description = "Swap right" })
|
||||||
|
|
||||||
|
-- Resize (Forge: window-resize-<edge>-<increase|decrease>).
|
||||||
|
--
|
||||||
|
-- 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 = "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), 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" })
|
||||||
|
|
||||||
|
-- Mouse: drag to move, right-drag to resize.
|
||||||
|
bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true, description = "Move window with pointer" })
|
||||||
|
bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true, description = "Resize window with pointer" })
|
||||||
|
|
||||||
|
-- ── Workspaces ──────────────────────────────────────────────────────────────
|
||||||
|
category("Workspaces")
|
||||||
|
-- ALT is the workspace modifier, matching the GNOME setup.
|
||||||
|
--
|
||||||
|
-- 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" })
|
||||||
|
bind("ALT + SHIFT + L", hl.dsp.window.move({ workspace = "+1" }), { description = "Move window to workspace right" })
|
||||||
|
|
||||||
|
-- GNOME also had these on CTRL+ALT+Up/Down.
|
||||||
|
bind("CTRL + ALT + up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||||
|
bind("CTRL + ALT + down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||||
|
|
||||||
|
-- Direct jump. ALT+0 is workspace 10.
|
||||||
|
for i = 1, 10 do
|
||||||
|
local key = i % 10
|
||||||
|
bind("ALT + " .. key, hl.dsp.focus({ workspace = i }), { description = "Workspace " .. i })
|
||||||
|
bind("ALT + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }), { description = "Move window to workspace " .. i })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scroll the mouse wheel over the desktop with SUPER held to change workspace.
|
||||||
|
-- (Scrolling the workspace indicator in the bar does the same; that's handled
|
||||||
|
-- in quickshell/modules/bar/Workspaces.qml.)
|
||||||
|
bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" }), { description = "Workspace right" })
|
||||||
|
bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" }), { description = "Workspace left" })
|
||||||
|
|
||||||
|
-- Minimize, as far as Hyprland has one.
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
-- 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 minimized)" })
|
||||||
|
bind(mod .. " + SHIFT + X", hl.dsp.window.move({ workspace = "special:scratch" }), { description = "Minimize to scratchpad" })
|
||||||
|
|
||||||
|
-- ── Session ─────────────────────────────────────────────────────────────────
|
||||||
|
category("Session")
|
||||||
|
-- GNOME's lock was SUPER+L, which is "focus right" here, so lock moves to
|
||||||
|
-- CTRL+ALT+L -- the other binding most people already have in muscle memory.
|
||||||
|
bind("CTRL + ALT + L", hl.dsp.exec_cmd("loginctl lock-session"), { description = "Lock" })
|
||||||
|
bind("SUPER + Backspace", hl.dsp.exec_cmd("loginctl lock-session"), { description = "Lock" })
|
||||||
|
bind("CTRL + ALT + Delete", hl.dsp.exec_cmd(qs("powermenu", "toggle")), { description = "Power menu" })
|
||||||
|
|
||||||
|
-- ── Media and volume ────────────────────────────────────────────────────────
|
||||||
|
category("Media & hardware")
|
||||||
|
-- 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(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" })
|
||||||
|
|
||||||
|
-- ── Dictation ───────────────────────────────────────────────────────────────
|
||||||
|
category("Shell")
|
||||||
|
--
|
||||||
|
-- Hold to talk, exactly like push-to-talk anywhere else: the mic is open only
|
||||||
|
-- while the key is down, so it cannot be left listening by forgetting about it.
|
||||||
|
-- Two binds on one chord, the second flagged `release`.
|
||||||
|
--
|
||||||
|
-- No `repeating`: holding a key normally repeats the press, which would restart
|
||||||
|
-- the recording several times a second. The daemon refuses a second start while
|
||||||
|
-- one is running, so a repeat would be harmless -- but not asking for it is
|
||||||
|
-- better than relying on being refused.
|
||||||
|
bind(mod .. " + D", hl.dsp.exec_cmd(dictate("start")),
|
||||||
|
{ description = "Dictate (hold to talk)" })
|
||||||
|
bind(mod .. " + D", hl.dsp.exec_cmd(dictate("stop")),
|
||||||
|
{ release = true, description = "Dictate (transcribe on release)" })
|
||||||
|
|
||||||
|
-- Escape out of a recording without transcribing it. Bound to the same modifier
|
||||||
|
-- so it can be reached with the dictation key still held.
|
||||||
|
bind(mod .. " + SHIFT + D", hl.dsp.exec_cmd(dictate("cancel")),
|
||||||
|
{ description = "Cancel dictation" })
|
||||||
|
|
||||||
|
-- Back to media: the dictation binds sit here for historical reasons, and the
|
||||||
|
-- category has to be set again or everything below inherits theirs.
|
||||||
|
category("Media & hardware")
|
||||||
|
|
||||||
|
-- Fine-grained steps, matching GNOME's shift/alt volume modifiers.
|
||||||
|
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(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(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" })
|
||||||
|
|
||||||
|
-- Airplane mode. The Framework's F10 emits exactly this keysym, and for a
|
||||||
|
-- while it emitted it into silence. panama-osd owns the toggle so the OSD
|
||||||
|
-- can say which way it went.
|
||||||
|
bind("XF86RFKill", hl.dsp.exec_cmd(osd("airplane toggle")), { locked = true, description = "Airplane mode" })
|
||||||
|
|
||||||
|
-- F9 on the same row. GNOME shows a display-switching OSD here; until
|
||||||
|
-- mirroring exists (DESKTOP-PARITY gap), the honest action is the page where
|
||||||
|
-- displays are actually arranged.
|
||||||
|
bind("XF86Display", hl.dsp.exec_cmd("qs ipc call settings page displays"), { description = "Display settings" })
|
||||||
|
|
||||||
|
-- ── The power button ────────────────────────────────────────────────────────
|
||||||
|
category("Media & hardware")
|
||||||
|
--
|
||||||
|
-- logind is told to ignore the power key (config/copy ships the drop-in) so a
|
||||||
|
-- stray press is a question, not an instant poweroff. That makes what the
|
||||||
|
-- question IS Panama's to choose, and `powerButtonAction` is where the choice
|
||||||
|
-- is recorded. Until the next boot after that drop-in lands, logind still acts
|
||||||
|
-- on the key; this bind costs nothing extra then.
|
||||||
|
--
|
||||||
|
-- The branch runs ON EVERY PRESS rather than here at config time.
|
||||||
|
--
|
||||||
|
-- A config-time branch would be shorter -- `prefs.get` and four `if`s -- and it
|
||||||
|
-- would also make this the one control on the Power page that does nothing
|
||||||
|
-- until the compositor is reloaded. Every other setting in Panama applies as
|
||||||
|
-- you change it, and a power button that ignores what the settings app says it
|
||||||
|
-- does is a worse thing to ship than a long command string. So the bind is a
|
||||||
|
-- `case` over what the settings file says at the moment the key goes down.
|
||||||
|
--
|
||||||
|
-- Everything that can go wrong lands on the shipped default: no jq, no file, a
|
||||||
|
-- truncated file, or a value nobody recognises all fall through to `*)` and
|
||||||
|
-- open the menu. The failure direction is "the power button opens a menu",
|
||||||
|
-- never "the power button does something you did not ask for".
|
||||||
|
--
|
||||||
|
-- Powering off goes THROUGH the menu with Power Off pre-armed rather than
|
||||||
|
-- calling `systemctl poweroff` here. The menu's two-press confirm is what
|
||||||
|
-- stands between a pocketed key and an unsaved afternoon, and a direct
|
||||||
|
-- poweroff would quietly throw it away -- so a person who picks "Powers off"
|
||||||
|
-- gets a fast poweroff, not an unguarded one.
|
||||||
|
local power_button = {
|
||||||
|
menu = qs("powermenu", "toggle"),
|
||||||
|
suspend = "systemctl suspend",
|
||||||
|
poweroff = qs("powermenu", "open") .. " poweroff",
|
||||||
|
nothing = ":",
|
||||||
|
}
|
||||||
|
|
||||||
|
local power_button_command = table.concat({
|
||||||
|
[[case "$(jq -r '.powerButtonAction // empty' "${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" 2>/dev/null)" in]],
|
||||||
|
"suspend) " .. power_button.suspend .. " ;;",
|
||||||
|
"poweroff) " .. power_button.poweroff .. " ;;",
|
||||||
|
"nothing) " .. power_button.nothing .. " ;;",
|
||||||
|
"*) " .. power_button.menu .. " ;;",
|
||||||
|
"esac",
|
||||||
|
}, " ")
|
||||||
|
|
||||||
|
bind("XF86PowerOff", hl.dsp.exec_cmd(power_button_command),
|
||||||
|
{ locked = true, description = "Power button" })
|
||||||
|
|
||||||
|
-- The lid, as a switch rather than a key. Closing a docked lid turns the
|
||||||
|
-- internal panel off so nothing renders inside a closed shell and no
|
||||||
|
-- workspace strands on an invisible output; opening it turns the panel back
|
||||||
|
-- on. panama-lid owns the decision -- undocked machines suspend via logind
|
||||||
|
-- before this matters, and the guard's inhibitor handles staying awake.
|
||||||
|
bind("switch:on:Lid Switch", hl.dsp.exec_cmd(lid("close")), { locked = true, description = "Lid closed" })
|
||||||
|
bind("switch:off:Lid Switch", hl.dsp.exec_cmd(lid("open")), { locked = true, description = "Lid opened" })
|
||||||
|
|
||||||
|
category("Applications")
|
||||||
|
-- Hardware keys GNOME mapped that have obvious equivalents.
|
||||||
|
bind("XF86Tools", hl.dsp.exec_cmd(settings), { description = "Settings" })
|
||||||
|
bind("XF86Calculator", hl.dsp.exec_cmd(calculator), { description = "Calculator" })
|
||||||
|
bind("XF86Explorer", hl.dsp.exec_cmd(files), { description = "Files" })
|
||||||
|
bind("XF86WWW", hl.dsp.exec_cmd(browser), { description = "Browser" })
|
||||||
|
bind("XF86Mail", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
||||||
|
bind("XF86Search", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||||
|
|
||||||
|
-- ── Custom shortcuts ────────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- Shortcuts the user invented, from `customBinds` in settings.json. Each entry
|
||||||
|
-- is { chord, kind, target, label } -- data, never a command. actions.lua turns
|
||||||
|
-- the kind/target pair into a dispatcher through whitelist tables; an entry it
|
||||||
|
-- does not recognise resolves to nil and is silently not emitted.
|
||||||
|
--
|
||||||
|
-- Emitted LAST, and through hl.bind rather than the `bind` wrapper above. Two
|
||||||
|
-- separate reasons, both about keeping the two rebinding mechanisms apart:
|
||||||
|
--
|
||||||
|
-- * `keybindOverrides` is keyed by a SHIPPED chord. A custom bind has no
|
||||||
|
-- shipped chord -- it is rebound by rewriting its own entry -- so putting
|
||||||
|
-- one through `bind` would let an override for some shipped key silently
|
||||||
|
-- move a custom one that happened to share a chord.
|
||||||
|
-- * last means a custom chord that collides with a shipped one loses, which
|
||||||
|
-- is checked explicitly below rather than left to Hyprland's ordering.
|
||||||
|
--
|
||||||
|
-- Settings prevents a collision upstream; this is the second lock, because the
|
||||||
|
-- file is hand-editable and losing a shipped key to a typo is not acceptable.
|
||||||
|
category("Custom")
|
||||||
|
|
||||||
|
local function custom_bind(chord, dispatcher, label)
|
||||||
|
categories[chord] = current_category
|
||||||
|
return hl.bind(chord, dispatcher, { description = label })
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, entry in ipairs(prefs.get("customBinds", {})) do
|
||||||
|
if type(entry) == "table" then
|
||||||
|
local chord = entry.chord
|
||||||
|
local label = entry.label
|
||||||
|
|
||||||
|
-- A description is not decoration: keybinds-contract fails a build
|
||||||
|
-- with a description-less bind, and the cheatsheet and Shortcuts page
|
||||||
|
-- both list what they find. A nameless shortcut is unfindable.
|
||||||
|
if valid_chord(chord)
|
||||||
|
and type(label) == "string" and label ~= ""
|
||||||
|
and categories[chord] == nil
|
||||||
|
then
|
||||||
|
local dispatcher = actions.dispatcher(entry)
|
||||||
|
if dispatcher ~= nil then
|
||||||
|
custom_bind(chord, dispatcher, label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
write_categories()
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Look and feel -- Tokyo Night Moon
|
||||||
|
--
|
||||||
|
-- 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
|
||||||
|
-- "loop" style on borderangle/shadowangle/glowangle -- that style forces
|
||||||
|
-- Hyprland to render at full refresh rate forever, even when nothing is
|
||||||
|
-- visible, which is exactly the kind of idle GPU burn to avoid on a 4500x3000
|
||||||
|
-- display.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = require("prefs")
|
||||||
|
|
||||||
|
-- The eight accents, from config/palette.json -- the one place they are
|
||||||
|
-- written down for everything outside QML. They used to be restated here,
|
||||||
|
-- which made adding a ninth accent a five-file edit; the file that got
|
||||||
|
-- forgotten was always the one that fails silently.
|
||||||
|
--
|
||||||
|
-- Falls back to the shipped blue pair if the palette cannot be read, for the
|
||||||
|
-- same reason prefs never raises: a missing file costs the accent colour, not
|
||||||
|
-- the compositor config.
|
||||||
|
local palette = prefs.readJson(os.getenv("HOME") .. "/.config/quickshell/config/palette.json")
|
||||||
|
local accents = palette.accents or {
|
||||||
|
blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- ── Color filters ───────────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- Grayscale and three color-blindness corrections, as end-of-pipe screen
|
||||||
|
-- shaders. Hyprland composites the desktop and then runs one fragment shader
|
||||||
|
-- over the result, so a filter here covers every window, the shell, the cursor
|
||||||
|
-- and video alike -- which is the only way a filter is honest.
|
||||||
|
--
|
||||||
|
-- The PREFERENCE is the enum, not the path. That split is deliberate: storing
|
||||||
|
-- the path would put a filesystem location a person can edit into the value
|
||||||
|
-- that becomes `decoration:screen_shader`, and it would make the stored value
|
||||||
|
-- disagree with what hyprctl reports back (which is the path), failing the
|
||||||
|
-- schema shape and write-sweep contracts. So PreferenceSchema's colorFilter
|
||||||
|
-- entry carries no `hypr:` block, and the enum→path mapping is written twice
|
||||||
|
-- on purpose: here, for reloads and for the moment before the shell starts,
|
||||||
|
-- and in services/SystemSettings.qml's applyColorFilter for the live apply.
|
||||||
|
-- The two lists have to be edited together.
|
||||||
|
--
|
||||||
|
-- "none" and anything unrecognised both produce the empty string, which is
|
||||||
|
-- what Hyprland reads as "no shader" -- and is the value it needs to be given
|
||||||
|
-- to turn one OFF, since there is no way to unset the option.
|
||||||
|
local shaderDir = (function()
|
||||||
|
local configHome = os.getenv("XDG_CONFIG_HOME")
|
||||||
|
if configHome == nil or configHome == "" then
|
||||||
|
local home = os.getenv("HOME")
|
||||||
|
if home == nil or home == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
configHome = home .. "/.config"
|
||||||
|
end
|
||||||
|
return configHome .. "/hypr/shaders"
|
||||||
|
end)()
|
||||||
|
|
||||||
|
local colorFilters = {
|
||||||
|
grayscale = "grayscale.frag",
|
||||||
|
protanopia = "protanopia.frag",
|
||||||
|
deuteranopia = "deuteranopia.frag",
|
||||||
|
tritanopia = "tritanopia.frag",
|
||||||
|
}
|
||||||
|
|
||||||
|
local colorFilterShader = ""
|
||||||
|
do
|
||||||
|
local file = colorFilters[prefs.get("colorFilter", "none")]
|
||||||
|
if file ~= nil and shaderDir ~= nil then
|
||||||
|
colorFilterShader = shaderDir .. "/" .. file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
hl.config({
|
||||||
|
general = {
|
||||||
|
gaps_in = prefs.get("gapsIn", 5),
|
||||||
|
gaps_out = prefs.get("gapsOut", 10),
|
||||||
|
|
||||||
|
border_size = prefs.get("borderSize", 2),
|
||||||
|
|
||||||
|
col = {
|
||||||
|
-- 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 = 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.
|
||||||
|
allow_tearing = true,
|
||||||
|
|
||||||
|
layout = prefs.get("windowLayout", "dwindle"),
|
||||||
|
|
||||||
|
snap = {
|
||||||
|
enabled = prefs.get("windowSnapping", 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 = prefs.get("roundingPower", 2),
|
||||||
|
|
||||||
|
active_opacity = prefs.get("activeOpacity", 1.0),
|
||||||
|
fullscreen_opacity = prefs.get("fullscreenOpacity", 1.0),
|
||||||
|
inactive_opacity = prefs.get("inactiveOpacity", 1.0),
|
||||||
|
|
||||||
|
-- Accessibility: dim every window but the focused one.
|
||||||
|
dim_inactive = prefs.get("dimInactive", false),
|
||||||
|
dim_strength = prefs.get("dimStrength", 0.5),
|
||||||
|
|
||||||
|
blur = {
|
||||||
|
enabled = prefs.get("blurEnabled", true),
|
||||||
|
size = prefs.get("blurSize", 8),
|
||||||
|
passes = prefs.get("blurPasses", 3),
|
||||||
|
|
||||||
|
-- Required for blur to be affordable. Never turn this off.
|
||||||
|
new_optimizations = true,
|
||||||
|
-- Floating windows skip blurring what's behind tiled ones. Big win.
|
||||||
|
xray = true,
|
||||||
|
|
||||||
|
ignore_opacity = true,
|
||||||
|
noise = 0.0117,
|
||||||
|
contrast = 0.9,
|
||||||
|
brightness = 0.85,
|
||||||
|
vibrancy = 0.17,
|
||||||
|
vibrancy_darkness = 0.05,
|
||||||
|
|
||||||
|
popups = true,
|
||||||
|
popups_ignorealpha = 0.2,
|
||||||
|
|
||||||
|
-- Blurring the special workspace is expensive and rarely seen.
|
||||||
|
special = false,
|
||||||
|
},
|
||||||
|
|
||||||
|
shadow = {
|
||||||
|
enabled = prefs.get("shadowEnabled", true),
|
||||||
|
range = prefs.get("shadowRange", 20),
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- 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(" .. accentStart .. "33)",
|
||||||
|
color_inactive = "rgba(00000000)",
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Off: costs real frame time and reads as smeary on a 60Hz panel.
|
||||||
|
motion_blur = { enabled = false },
|
||||||
|
|
||||||
|
-- The accessibility color filter, resolved above. Empty when off, and
|
||||||
|
-- empty costs nothing: Hyprland skips the pass entirely rather than
|
||||||
|
-- running an identity shader.
|
||||||
|
screen_shader = colorFilterShader,
|
||||||
|
},
|
||||||
|
|
||||||
|
animations = { enabled = prefs.get("animationsEnabled", true) },
|
||||||
|
|
||||||
|
dwindle = {
|
||||||
|
-- Keep the split orientation a window was created with. Closest match
|
||||||
|
-- to how the Forge extension behaved on GNOME.
|
||||||
|
preserve_split = prefs.get("preserveSplit", true),
|
||||||
|
smart_resizing = true,
|
||||||
|
force_split = prefs.getInt("forceSplit", 0),
|
||||||
|
},
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
-- 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),
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
-- games. Keeps VRR off the desktop, where it causes visible flicker.
|
||||||
|
vrr = prefs.getInt("vrrPolicy", 3),
|
||||||
|
|
||||||
|
-- Blur behind the lock screen.
|
||||||
|
session_lock_blur = true,
|
||||||
|
|
||||||
|
-- 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 behavior.
|
||||||
|
focus_on_activate = prefs.get("focusOnActivate", false),
|
||||||
|
|
||||||
|
-- Whether pointing at another monitor is enough to move focus there,
|
||||||
|
-- or it takes a click. GNOME moves on pointer; both are offered.
|
||||||
|
mouse_move_focuses_monitor = prefs.get("mouseMoveFocusesMonitor", true),
|
||||||
|
|
||||||
|
-- Window swallowing: a terminal hides itself while a graphical
|
||||||
|
-- application launched from it is open, and comes back when that
|
||||||
|
-- application exits. Off by default -- it is a real change in how the
|
||||||
|
-- desktop behaves, and one that is confusing rather than broken if you
|
||||||
|
-- did not ask for it: your terminal appears to vanish.
|
||||||
|
--
|
||||||
|
-- The regex is narrow on purpose. Anything matching it can swallow, so
|
||||||
|
-- a permissive pattern means windows disappearing in cases nobody
|
||||||
|
-- intended. Only the two terminals this desktop actually ships.
|
||||||
|
enable_swallow = prefs.get("windowSwallow", false),
|
||||||
|
swallow_regex = "^(kitty|com\\.mitchellh\\.ghostty)$",
|
||||||
|
},
|
||||||
|
|
||||||
|
render = {
|
||||||
|
-- Color management is stable and on by default in 0.56.
|
||||||
|
cm_enabled = true,
|
||||||
|
|
||||||
|
-- 1 = automatically flip the monitor into HDR for fullscreen content
|
||||||
|
-- that asks for it, and back out afterwards. This is how games get HDR
|
||||||
|
-- without the desktop paying the screencopy cost. See monitors.lua.
|
||||||
|
cm_auto_hdr = prefs.getInt("autoHdr", 1),
|
||||||
|
|
||||||
|
-- 2 = direct scanout only for windows tagged content = "game"
|
||||||
|
-- (set by the rules in rules.lua). Bypasses compositing for real
|
||||||
|
-- fullscreen games.
|
||||||
|
direct_scanout = prefs.getInt("directScanoutPolicy", 2),
|
||||||
|
},
|
||||||
|
|
||||||
|
cursor = {
|
||||||
|
-- 2 = auto. Disables the hardware cursor when tearing, which is what
|
||||||
|
-- keeps the cursor from ghosting in games.
|
||||||
|
no_hardware_cursors = 2,
|
||||||
|
|
||||||
|
-- Don't let the cursor break VRR on content tagged as a game.
|
||||||
|
no_break_fs_vrr = 2,
|
||||||
|
min_refresh_rate = 48,
|
||||||
|
|
||||||
|
-- Pull the cursor theme from gsettings so GTK apps and Hyprland agree.
|
||||||
|
sync_gsettings_theme = true,
|
||||||
|
|
||||||
|
-- Fade the cursor out after 4s of no movement, like GNOME does.
|
||||||
|
inactive_timeout = prefs.get("cursorInactiveTimeout", 4),
|
||||||
|
|
||||||
|
-- The accessibility magnifier. 1.0 = off.
|
||||||
|
zoom_factor = prefs.get("magnifierFactor", 1.0),
|
||||||
|
zoom_rigid = prefs.get("magnifierRigid", false),
|
||||||
|
},
|
||||||
|
|
||||||
|
ecosystem = {
|
||||||
|
no_update_news = not prefs.get("hyprlandUpdateNews", false),
|
||||||
|
no_donation_nag = not prefs.get("hyprlandDonationNag", false),
|
||||||
|
},
|
||||||
|
|
||||||
|
xwayland = {
|
||||||
|
-- Render X11 apps at 1:1 device pixels so they're sharp rather than
|
||||||
|
-- bilinearly upscaled. See the note in README.md about the unavoidable
|
||||||
|
-- sizing tradeoff at 1.5x.
|
||||||
|
force_zero_scaling = true,
|
||||||
|
},
|
||||||
|
|
||||||
|
quirks = {
|
||||||
|
-- 2 = report HDR as preferred to gamescope only. Stops gamescope from
|
||||||
|
-- white-screening when it expects HDR to already be live.
|
||||||
|
prefer_hdr = 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Motion ──────────────────────────────────────────────────────────────────
|
||||||
|
-- speed is in deciseconds: speed = 4 means 400ms. Higher = slower.
|
||||||
|
|
||||||
|
hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } })
|
||||||
|
hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } })
|
||||||
|
hl.curve("quick", { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } })
|
||||||
|
hl.curve("easy", { type = "spring", mass = 1, stiffness = 238.1191, dampening = 24.21279 })
|
||||||
|
-- A touch bouncier than "easy"; used for windows appearing.
|
||||||
|
hl.curve("snappy", { type = "spring", mass = 1, stiffness = 320, dampening = 22 })
|
||||||
|
|
||||||
|
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
|
||||||
|
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
|
||||||
|
|
||||||
|
-- The focus sweep: when a window takes focus, its gradient border rotates into
|
||||||
|
-- place once. Style "once" is load-bearing -- the alternative, "loop", makes
|
||||||
|
-- Hyprland render at full refresh rate forever, even when no border is visible.
|
||||||
|
-- That is exactly the idle GPU cost this config exists to avoid, so: never loop.
|
||||||
|
hl.animation({ leaf = "borderangle", enabled = true, speed = 6, bezier = "easeOutQuint", style = "once" })
|
||||||
|
|
||||||
|
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
|
||||||
|
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "snappy", style = "popin 90%" })
|
||||||
|
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "almostLinear", style = "popin 90%" })
|
||||||
|
|
||||||
|
hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" })
|
||||||
|
|
||||||
|
-- Layer surfaces = the Quickshell bar, dock, popovers and overlays.
|
||||||
|
hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" })
|
||||||
|
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "easeOutQuint", style = "fade" })
|
||||||
|
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "almostLinear", style = "fade" })
|
||||||
|
|
||||||
|
-- Workspace switching: horizontal slide, matching Alt+H / Alt+L direction.
|
||||||
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 2.5, bezier = "easeOutQuint", style = "slidefade 15%" })
|
||||||
|
|
||||||
|
hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" })
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Monitors
|
||||||
|
--
|
||||||
|
-- Kuycon P20 (matched by description): 4500x3000 @ 60Hz, 1.5x fractional scale.
|
||||||
|
-- 4500/1.5 = 3000 and 3000/1.5 = 2000, both integers, so this is a "clean"
|
||||||
|
-- fractional scale and Hyprland will not complain.
|
||||||
|
--
|
||||||
|
-- On HDR --------------------------------------------------------------------
|
||||||
|
-- GNOME runs this panel in bt2100/HDR. Hyprland CAN do that (cm = "hdr"), but
|
||||||
|
-- on 0.56.x it currently breaks screencopy: grim screenshots come back empty,
|
||||||
|
-- and OBS/Sunshine capture and hyprlock's blurred background go with them.
|
||||||
|
-- Root cause is still open upstream. Since screen capture matters more here
|
||||||
|
-- than an HDR desktop, the desktop runs SDR at 10-bit and HDR is handed to
|
||||||
|
-- fullscreen games only, via render.cm_auto_hdr in looks.lua.
|
||||||
|
--
|
||||||
|
-- To try full-time HDR anyway, set shipped_cm = "hdr" below (or pick HDR for
|
||||||
|
-- this display in Settings, which saves it as the display's colorProfile and
|
||||||
|
-- wins over the shipped value) and read the notes in overrides.lua first.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = require("prefs")
|
||||||
|
|
||||||
|
-- Per-output overrides written by Panama Settings, keyed by output name:
|
||||||
|
-- { ["DP-2"] = {
|
||||||
|
-- mode = "3840x2160@60", scale = 2, transform = 0,
|
||||||
|
-- x = 0, y = 0, primary = true,
|
||||||
|
-- vrrMode = -1, colorProfile = "auto", bitdepth = 10,
|
||||||
|
-- sdrBrightness = 1, sdrSaturation = 1, mirrorOf = "",
|
||||||
|
-- } }
|
||||||
|
--
|
||||||
|
-- The fields past `primary` are optional: records written before they existed
|
||||||
|
-- carry none of them, and the shipped values below stand instead. That is the
|
||||||
|
-- relationship in both directions -- what is written here is what a saved
|
||||||
|
-- record inherits, and what Settings saves is what overrides it, so the two
|
||||||
|
-- stop fighting over the same monitor rule.
|
||||||
|
--
|
||||||
|
-- Geometry and colour fail differently on purpose. A half-written position is
|
||||||
|
-- refused outright (below), because guessing one can strand an output where
|
||||||
|
-- nothing can reach it. An unreadable colour, VRR or mirror value is dropped
|
||||||
|
-- on its own and the shipped default stands: the worst it costs is a wrong
|
||||||
|
-- shade, and taking the whole record down with it would cost the arrangement.
|
||||||
|
local displays = prefs.get("displays", {})
|
||||||
|
if type(displays) ~= "table" then
|
||||||
|
displays = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mode_dimensions(mode)
|
||||||
|
if type(mode) ~= "string" then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
local width, height, refresh = mode:match("^(%d+)x(%d+)@(%d+%.%d+)$")
|
||||||
|
if width == nil then
|
||||||
|
width, height, refresh = mode:match("^(%d+)x(%d+)@(%d+)$")
|
||||||
|
end
|
||||||
|
width, height, refresh = tonumber(width), tonumber(height), tonumber(refresh)
|
||||||
|
if width == nil or height == nil or refresh == nil
|
||||||
|
or width <= 0 or height <= 0 or refresh <= 0 then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
return width, height
|
||||||
|
end
|
||||||
|
|
||||||
|
local function valid_mode(mode)
|
||||||
|
local width = mode_dimensions(mode)
|
||||||
|
return width ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function valid_scale(mode, scale)
|
||||||
|
local width, height = mode_dimensions(mode)
|
||||||
|
if width == nil or type(scale) ~= "number" or scale ~= scale
|
||||||
|
or scale <= 0 or scale > 4 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local logical_width = width / scale
|
||||||
|
local logical_height = height / scale
|
||||||
|
return math.abs(logical_width - math.floor(logical_width + 0.5)) < 0.0001
|
||||||
|
and math.abs(logical_height - math.floor(logical_height + 0.5)) < 0.0001
|
||||||
|
end
|
||||||
|
|
||||||
|
local function valid_transform(transform)
|
||||||
|
return type(transform) == "number"
|
||||||
|
and transform == math.floor(transform)
|
||||||
|
and transform >= 0
|
||||||
|
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
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local entry = displays[output]
|
||||||
|
if type(entry) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if not valid_mode(entry.mode)
|
||||||
|
or not valid_scale(entry.mode, entry.scale)
|
||||||
|
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 color_profiles = { auto = true, srgb = true, wide = true, hdr = true }
|
||||||
|
|
||||||
|
local function color_profile(entry, fallback)
|
||||||
|
local value = entry ~= nil and entry.colorProfile or nil
|
||||||
|
if type(value) == "string" and color_profiles[value] then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
|
||||||
|
local function bitdepth_value(entry, fallback)
|
||||||
|
local value = entry ~= nil and entry.bitdepth or nil
|
||||||
|
if value == 8 or value == 10 then
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
|
||||||
|
-- -1 means the display follows the global misc.vrr policy, which is said by
|
||||||
|
-- leaving the key out. 3 is the global policy's own value and not a per-display
|
||||||
|
-- choice, so it is not accepted here either.
|
||||||
|
local function vrr_value(entry)
|
||||||
|
local value = entry ~= nil and entry.vrrMode or nil
|
||||||
|
if type(value) ~= "number" or value ~= math.floor(value)
|
||||||
|
or value < 0 or value > 2 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Neutral is 1.0, and the neutral value is left out rather than written: a rule
|
||||||
|
-- that names it pins the display to it, which is not the same as leaving the
|
||||||
|
-- trim alone.
|
||||||
|
local function sdr_value(value, minimum, maximum)
|
||||||
|
if type(value) ~= "number" or value ~= value
|
||||||
|
or value < minimum or value > maximum
|
||||||
|
or math.abs(value - 1) < 0.001 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A mirror needs a target that is not itself and not another mirror -- Hyprland
|
||||||
|
-- has no chain to follow -- and the primary may not mirror at all, since the
|
||||||
|
-- arrangement is anchored on it.
|
||||||
|
local function mirror_value(entry, output)
|
||||||
|
local value = entry ~= nil and entry.mirrorOf or nil
|
||||||
|
if type(value) ~= "string" or value == "" or value == output
|
||||||
|
or value:match("^[%w_.-]+$") == nil
|
||||||
|
or entry.primary == true then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local target = displays[value]
|
||||||
|
if type(target) == "table" and type(target.mirrorOf) == "string"
|
||||||
|
and target.mirrorOf ~= "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Colour, VRR and mirroring layered onto a rule that already carries
|
||||||
|
-- mode/position/scale/transform. A monitor rule replaces the previous rule for
|
||||||
|
-- that output whole, so the shipped defaults are passed in here rather than
|
||||||
|
-- written in a rule of their own. `connector` is the output name the record was
|
||||||
|
-- saved under, which is not always the rule's own output: the Kuycon rule
|
||||||
|
-- matches by description.
|
||||||
|
local function with_display_fields(rule, entry, connector, default_bitdepth, default_cm)
|
||||||
|
rule.bitdepth = bitdepth_value(entry, default_bitdepth)
|
||||||
|
rule.cm = color_profile(entry, default_cm)
|
||||||
|
rule.vrr = vrr_value(entry)
|
||||||
|
rule.sdrbrightness = entry ~= nil and sdr_value(entry.sdrBrightness, 0.8, 2.0) or nil
|
||||||
|
rule.sdrsaturation = entry ~= nil and sdr_value(entry.sdrSaturation, 0.8, 1.2) or nil
|
||||||
|
|
||||||
|
-- A mirror shows its target's picture in its target's place, so the saved
|
||||||
|
-- position is not the compositor's to honour or ours to ask for.
|
||||||
|
local mirror = mirror_value(entry, connector)
|
||||||
|
if mirror ~= nil then
|
||||||
|
rule.mirror = mirror
|
||||||
|
rule.position = "auto"
|
||||||
|
end
|
||||||
|
return rule
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Every connected output uses the same validated per-output store. Automatic
|
||||||
|
-- placement and the compositor's normal color policy unless the entry says
|
||||||
|
-- otherwise.
|
||||||
|
for output, _ in pairs(displays) do
|
||||||
|
local entry = display_entry(output)
|
||||||
|
if entry ~= nil then
|
||||||
|
hl.monitor(with_display_fields({
|
||||||
|
output = output,
|
||||||
|
mode = entry.mode,
|
||||||
|
position = display_position(entry, "auto"),
|
||||||
|
scale = entry.scale,
|
||||||
|
transform = entry.transform,
|
||||||
|
}, entry, output, nil, nil))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The Kuycon P20, matched by what it is rather than where it is plugged in.
|
||||||
|
-- This used to be a rule for connector DP-2 outright, which handed the panel's
|
||||||
|
-- 4500x3000 mode and 1.5 scale to whatever monitor a stranger's machine had on
|
||||||
|
-- its most common DisplayPort connector. Emitted after the prefs loop so a
|
||||||
|
-- saved entry for its connector still carries the mode/scale/position, while
|
||||||
|
-- this rule holds the shipped defaults and the panel-specific color policy.
|
||||||
|
local shipped_mode = "4500x3000@60"
|
||||||
|
local shipped_scale = 1.5
|
||||||
|
local shipped_transform = 0
|
||||||
|
|
||||||
|
-- 10-bit output. 4500x3000@60 at 10bpc is ~24 Gbps, right at the edge of DP 1.4
|
||||||
|
-- HBR3, so this relies on DSC. If the display fails to light up or falls back to
|
||||||
|
-- a lower mode, drop this to 8 first.
|
||||||
|
local shipped_bitdepth = 10
|
||||||
|
|
||||||
|
-- "auto" = sRGB at 8bpc, wide gamut at 10bpc. Not HDR; see header.
|
||||||
|
local shipped_cm = "auto"
|
||||||
|
|
||||||
|
local kuycon = display_entry("DP-2")
|
||||||
|
hl.monitor(with_display_fields({
|
||||||
|
output = "desc:GVT Kuycon P20",
|
||||||
|
mode = kuycon and kuycon.mode or shipped_mode,
|
||||||
|
position = display_position(kuycon, "0x0"),
|
||||||
|
scale = kuycon and kuycon.scale or shipped_scale,
|
||||||
|
transform = kuycon and kuycon.transform or shipped_transform,
|
||||||
|
}, kuycon, "DP-2", shipped_bitdepth, shipped_cm))
|
||||||
|
|
||||||
|
-- Any monitor not named above: sane defaults rather than nothing.
|
||||||
|
hl.monitor({
|
||||||
|
output = "",
|
||||||
|
mode = "preferred",
|
||||||
|
position = "auto",
|
||||||
|
scale = "auto",
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Workspaces on the primary display only ──────────────────────────────────
|
||||||
|
--
|
||||||
|
-- GNOME offered one workspace choice worth reproducing: whether the other
|
||||||
|
-- screens join in. Off, every monitor has its own workspaces and switching
|
||||||
|
-- affects whichever one has focus -- Hyprland's own behaviour, so it needs no
|
||||||
|
-- rules at all. On, workspaces 1-10 are pinned to the primary display and a
|
||||||
|
-- second screen keeps a workspace of its own that stays put.
|
||||||
|
--
|
||||||
|
-- Ten because that is how many the keybinds reach: ALT+1 through ALT+0 in
|
||||||
|
-- keybinds.lua. Binding more would pin workspaces nothing can navigate to, and
|
||||||
|
-- binding fewer would leave the last few behaving differently from the rest for
|
||||||
|
-- no reason a person could see.
|
||||||
|
--
|
||||||
|
-- The rules are emitted here rather than written live because Hyprland reads
|
||||||
|
-- them at config time and offers no way to remove one afterwards: writing an
|
||||||
|
-- empty monitor leaves the previous binding in place. So the config is the only
|
||||||
|
-- honest source, and applying a change is a reload.
|
||||||
|
if prefs.get("workspacesOnPrimaryOnly", false) == true then
|
||||||
|
-- Only a record display_entry accepts counts. A half-written entry is one
|
||||||
|
-- the monitor rules above already refuse, so pinning ten workspaces to it on
|
||||||
|
-- the strength of a `primary` flag nothing else trusts would put them on a
|
||||||
|
-- screen that never got a rule of its own.
|
||||||
|
local primaries = {}
|
||||||
|
for output, _ in pairs(displays) do
|
||||||
|
local entry = display_entry(output)
|
||||||
|
if entry ~= nil and entry.primary == true then
|
||||||
|
primaries[#primaries + 1] = output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Without a primary there is nothing to pin to, and guessing one would move
|
||||||
|
-- every workspace onto whichever screen happened to sort first. Two records
|
||||||
|
-- both claiming primary is the same problem wearing a different hat: pairs()
|
||||||
|
-- has no order, so picking one of them would pin the workspaces to a
|
||||||
|
-- different screen from one reload to the next. Neither case guesses.
|
||||||
|
if #primaries == 1 then
|
||||||
|
local primary = primaries[1]
|
||||||
|
for i = 1, 10 do
|
||||||
|
hl.workspace_rule({ workspace = tostring(i), monitor = primary })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Per-machine overrides
|
||||||
|
--
|
||||||
|
-- Loaded last, so anything here beats the modules above. Keep machine-specific
|
||||||
|
-- settings in this file rather than editing the others -- it keeps the rest of
|
||||||
|
-- the config portable between boxes.
|
||||||
|
--
|
||||||
|
-- Full-time HDR ---------------------------------------------------------------
|
||||||
|
-- The desktop ships SDR at 10-bit with HDR handed to fullscreen games (see
|
||||||
|
-- monitors.lua for why). To run the whole desktop in HDR, uncomment this:
|
||||||
|
--
|
||||||
|
-- hl.monitor({
|
||||||
|
-- output = "DP-2",
|
||||||
|
-- mode = "4500x3000@60",
|
||||||
|
-- position = "0x0",
|
||||||
|
-- scale = 1.5,
|
||||||
|
-- bitdepth = 10,
|
||||||
|
-- cm = "hdr",
|
||||||
|
-- sdrbrightness = 1.2, -- how bright SDR content appears, 1.0 - 2.0
|
||||||
|
-- sdrsaturation = 0.98,
|
||||||
|
-- })
|
||||||
|
--
|
||||||
|
-- Then expect to also need some of these, in this order:
|
||||||
|
-- hl.config({ render = { keep_unmodified_copy = 1 } }) -- if captures go transparent
|
||||||
|
-- hl.config({ render = { use_shader_blur_blend = true } }) -- if that kills blur
|
||||||
|
-- hl.config({ render = { direct_scanout = 0 } }) -- if screenshots come back empty
|
||||||
|
-- hl.config({ cursor = { no_hardware_cursors = 1 } }) -- if the cursor looks oversaturated
|
||||||
|
--
|
||||||
|
-- If screenshots, OBS or Sunshine break and stay broken, that is the known
|
||||||
|
-- upstream issue -- go back to cm = "auto".
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Shared preferences
|
||||||
|
--
|
||||||
|
-- Reads the same file Panama Settings writes:
|
||||||
|
-- $XDG_CONFIG_HOME/panama/settings.json (default ~/.config/panama/settings.json)
|
||||||
|
--
|
||||||
|
-- This is what makes the desktop one product rather than two that happen to
|
||||||
|
-- agree. The relationship is:
|
||||||
|
--
|
||||||
|
-- * the Lua config is the DEFAULT -- every prefs.get() call passes the shipped
|
||||||
|
-- value as its fallback, so this config still works standalone with no JSON
|
||||||
|
-- file at all;
|
||||||
|
-- * the JSON file is the TRUTH -- both Hyprland and Quickshell read it;
|
||||||
|
-- * Panama Settings is the EDITOR -- it writes the file and applies the change
|
||||||
|
-- live through `hyprctl eval`, so nothing needs a reload and the two sides
|
||||||
|
-- cannot drift apart.
|
||||||
|
--
|
||||||
|
-- Nothing here may raise. A missing, empty, truncated, or actively malformed
|
||||||
|
-- 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.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = {}
|
||||||
|
|
||||||
|
-- ── A small JSON reader ─────────────────────────────────────────────────────
|
||||||
|
-- Hyprland's Lua has no JSON support and pulling in a rock for a flat object of
|
||||||
|
-- scalars is not worth the dependency. Handles the whole format apart from
|
||||||
|
-- non-ASCII \u escapes, which are replaced rather than decoded -- no setting is
|
||||||
|
-- a non-ASCII string, and mangling one is preferable to failing the parse.
|
||||||
|
|
||||||
|
local function decode(text)
|
||||||
|
local pos = 1
|
||||||
|
|
||||||
|
local function skipSpace()
|
||||||
|
pos = text:find("[^ \t\r\n]", pos) or #text + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local parseValue
|
||||||
|
|
||||||
|
local function parseString()
|
||||||
|
pos = pos + 1 -- opening quote
|
||||||
|
local parts = {}
|
||||||
|
while true do
|
||||||
|
local char = text:sub(pos, pos)
|
||||||
|
if char == "" then
|
||||||
|
error("unterminated string")
|
||||||
|
elseif char == '"' then
|
||||||
|
pos = pos + 1
|
||||||
|
break
|
||||||
|
elseif char == "\\" then
|
||||||
|
local escape = text:sub(pos + 1, pos + 1)
|
||||||
|
local simple = {
|
||||||
|
n = "\n", t = "\t", r = "\r", b = "\b", f = "\f",
|
||||||
|
['"'] = '"', ["\\"] = "\\", ["/"] = "/",
|
||||||
|
}
|
||||||
|
if simple[escape] then
|
||||||
|
parts[#parts + 1] = simple[escape]
|
||||||
|
pos = pos + 2
|
||||||
|
elseif escape == "u" then
|
||||||
|
local code = tonumber(text:sub(pos + 2, pos + 5), 16)
|
||||||
|
parts[#parts + 1] = (code and code < 128) and string.char(code) or "?"
|
||||||
|
pos = pos + 6
|
||||||
|
else
|
||||||
|
error("invalid escape")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
parts[#parts + 1] = char
|
||||||
|
pos = pos + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(parts)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parseNumber()
|
||||||
|
local literal = text:match("^-?%d+%.?%d*[eE]?[-+]?%d*", pos)
|
||||||
|
if not literal or literal == "" then
|
||||||
|
error("invalid number")
|
||||||
|
end
|
||||||
|
pos = pos + #literal
|
||||||
|
local value = tonumber(literal)
|
||||||
|
if not value then
|
||||||
|
error("invalid number")
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parseObject()
|
||||||
|
pos = pos + 1 -- opening brace
|
||||||
|
local out = {}
|
||||||
|
skipSpace()
|
||||||
|
if text:sub(pos, pos) == "}" then
|
||||||
|
pos = pos + 1
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
while true do
|
||||||
|
skipSpace()
|
||||||
|
if text:sub(pos, pos) ~= '"' then
|
||||||
|
error("expected key")
|
||||||
|
end
|
||||||
|
local key = parseString()
|
||||||
|
skipSpace()
|
||||||
|
if text:sub(pos, pos) ~= ":" then
|
||||||
|
error("expected colon")
|
||||||
|
end
|
||||||
|
pos = pos + 1
|
||||||
|
out[key] = parseValue()
|
||||||
|
skipSpace()
|
||||||
|
local char = text:sub(pos, pos)
|
||||||
|
pos = pos + 1
|
||||||
|
if char == "}" then
|
||||||
|
return out
|
||||||
|
elseif char ~= "," then
|
||||||
|
error("expected comma or closing brace")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parseArray()
|
||||||
|
pos = pos + 1 -- opening bracket
|
||||||
|
local out = {}
|
||||||
|
skipSpace()
|
||||||
|
if text:sub(pos, pos) == "]" then
|
||||||
|
pos = pos + 1
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
while true do
|
||||||
|
out[#out + 1] = parseValue()
|
||||||
|
skipSpace()
|
||||||
|
local char = text:sub(pos, pos)
|
||||||
|
pos = pos + 1
|
||||||
|
if char == "]" then
|
||||||
|
return out
|
||||||
|
elseif char ~= "," then
|
||||||
|
error("expected comma or closing bracket")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
parseValue = function()
|
||||||
|
skipSpace()
|
||||||
|
local char = text:sub(pos, pos)
|
||||||
|
if char == "{" then
|
||||||
|
return parseObject()
|
||||||
|
elseif char == "[" then
|
||||||
|
return parseArray()
|
||||||
|
elseif char == '"' then
|
||||||
|
return parseString()
|
||||||
|
elseif text:sub(pos, pos + 3) == "true" then
|
||||||
|
pos = pos + 4
|
||||||
|
return true
|
||||||
|
elseif text:sub(pos, pos + 4) == "false" then
|
||||||
|
pos = pos + 5
|
||||||
|
return false
|
||||||
|
elseif text:sub(pos, pos + 3) == "null" then
|
||||||
|
pos = pos + 4
|
||||||
|
return nil
|
||||||
|
elseif char == "" then
|
||||||
|
error("unexpected end of input")
|
||||||
|
else
|
||||||
|
return parseNumber()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local value = parseValue()
|
||||||
|
if type(value) ~= "table" then
|
||||||
|
error("top level value is not an object")
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Loading ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local function settingsPath()
|
||||||
|
local configHome = os.getenv("XDG_CONFIG_HOME")
|
||||||
|
if configHome == nil or configHome == "" then
|
||||||
|
local home = os.getenv("HOME")
|
||||||
|
if home == nil or home == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
configHome = home .. "/.config"
|
||||||
|
end
|
||||||
|
return configHome .. "/panama/settings.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function read()
|
||||||
|
local path = settingsPath()
|
||||||
|
if not path then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
return {} -- no file yet is the normal first-run case, not an error
|
||||||
|
end
|
||||||
|
local text = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
if not text or text:match("^%s*$") then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local ok, parsed = pcall(decode, text)
|
||||||
|
if ok and type(parsed) == "table" then
|
||||||
|
return parsed
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Loaded once at config time. A pcall around the whole thing so that even an
|
||||||
|
-- unanticipated failure in the reader degrades to shipped defaults.
|
||||||
|
local values = {}
|
||||||
|
do
|
||||||
|
local ok, parsed = pcall(read)
|
||||||
|
if ok and type(parsed) == "table" then
|
||||||
|
values = parsed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Public interface ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Returns the stored value for `key`, or `fallback` when it is absent or is not
|
||||||
|
-- the same type as the fallback. The type guard matters: a stale or hand-edited
|
||||||
|
-- file that puts a string where Hyprland needs a number would otherwise abort
|
||||||
|
-- the config, taking down far more than the one setting that was wrong.
|
||||||
|
function prefs.get(key, fallback)
|
||||||
|
local value = values[key]
|
||||||
|
if value == nil then
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
if type(value) ~= type(fallback) then
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hyprland has no boolean-to-integer coercion for options that take 0/1, and
|
||||||
|
-- several of them read more naturally as a toggle in the settings UI.
|
||||||
|
function prefs.getInt(key, fallback)
|
||||||
|
local value = values[key]
|
||||||
|
if type(value) == "boolean" then
|
||||||
|
return value and 1 or 0
|
||||||
|
end
|
||||||
|
if type(value) ~= "number" then
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
return math.floor(value + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Read and decode any JSON file, using the same never-raise parser the
|
||||||
|
-- settings store uses. Returns an empty table for a file that is missing,
|
||||||
|
-- empty, or malformed, so a caller can index the result without checking.
|
||||||
|
--
|
||||||
|
-- Exists so config/palette.json can be read by looks.lua rather than the eight
|
||||||
|
-- accents being written out a second time in Lua. A bad palette costs the
|
||||||
|
-- accent colours, never the compositor config.
|
||||||
|
function prefs.readJson(path)
|
||||||
|
if type(path) ~= "string" or path == "" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local file = io.open(path, "r")
|
||||||
|
if not file then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local text = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
if not text or text:match("^%s*$") then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local ok, parsed = pcall(decode, text)
|
||||||
|
if not ok or type(parsed) ~= "table" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return parsed
|
||||||
|
end
|
||||||
|
|
||||||
|
-- True when a settings file was actually read. Useful from overrides.lua.
|
||||||
|
function prefs.loaded()
|
||||||
|
return next(values) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return prefs
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Window, workspace and layer rules
|
||||||
|
--
|
||||||
|
-- Rules evaluate top to bottom, last match wins -- but every NAMED rule is
|
||||||
|
-- evaluated before every anonymous one, so a named rule can never override an
|
||||||
|
-- anonymous rule further down. Names are used sparingly here for that reason.
|
||||||
|
--
|
||||||
|
-- Matching is Google RE2 (no backtracking). Prefix a pattern with "negative:"
|
||||||
|
-- to invert it.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
local prefs = require("prefs")
|
||||||
|
|
||||||
|
-- ── Upstream sanity rules ───────────────────────────────────────────────────
|
||||||
|
hl.window_rule({
|
||||||
|
name = "suppress-maximize-events",
|
||||||
|
match = { class = ".*" },
|
||||||
|
suppress_event = "maximize",
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
name = "fix-xwayland-drags",
|
||||||
|
match = {
|
||||||
|
class = "^$",
|
||||||
|
title = "^$",
|
||||||
|
xwayland = true,
|
||||||
|
float = true,
|
||||||
|
fullscreen = false,
|
||||||
|
pin = false,
|
||||||
|
},
|
||||||
|
no_focus = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Floating apps ───────────────────────────────────────────────────────────
|
||||||
|
-- Carried over from the Forge windows.json override list, minus entries for
|
||||||
|
-- apps that are no longer installed.
|
||||||
|
hl.window_rule({
|
||||||
|
match = {
|
||||||
|
class = "^(jetbrains-toolbox|zoom|Cider|Hidamari|com\\.mattjakeman\\.ExtensionManager)$",
|
||||||
|
},
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^([Bb]itwarden|com\\.bitwarden\\.desktop)$" },
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(com\\.nextcloud\\.desktopclient\\.nextcloud|Nextcloud)$" },
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(org\\.gnome\\.Calculator|gnome-calculator)$" },
|
||||||
|
float = true,
|
||||||
|
size = { 400, 600 },
|
||||||
|
center = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(mpv|io\\.mpv\\.Mpv)$" },
|
||||||
|
float = true,
|
||||||
|
-- Video should never be blurred or dimmed behind another window.
|
||||||
|
no_blur = true,
|
||||||
|
no_dim = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(com\\.spotify\\.Client|Spotify)$" },
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
size = { "monitor_w * 0.45", "monitor_h * 0.6" },
|
||||||
|
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)$" },
|
||||||
|
float = true,
|
||||||
|
center = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
hl.window_rule({
|
||||||
|
match = { title = "^(Open File|Save File|Save As|Open Folder|Select a File|Choose Files)$" },
|
||||||
|
float = true,
|
||||||
|
center = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Polkit prompt.
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(hyprpolkitagent|polkit-gnome-authentication-agent-1)$" },
|
||||||
|
float = true,
|
||||||
|
center = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Gaming ──────────────────────────────────────────────────────────────────
|
||||||
|
-- content = "game" is the keystone: misc.vrr = 3, render.direct_scanout = 2 and
|
||||||
|
-- cursor.no_break_fs_vrr = 2 all key off it. Blur, animation, shadow and dim are
|
||||||
|
-- all disabled so the compositor gets out of the way entirely.
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^(steam_app_\\d+|gamescope|lutris|net\\.lutris\\.Lutris|com\\.heroicgameslauncher\\.hgl|Minecraft.*|moonlight|com\\.moonlight_stream\\.Moonlight)$" },
|
||||||
|
content = "game",
|
||||||
|
immediate = true,
|
||||||
|
no_blur = true,
|
||||||
|
no_anim = true,
|
||||||
|
no_shadow = true,
|
||||||
|
no_dim = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Steam itself is a normal window, but its transient popups are a mess.
|
||||||
|
hl.window_rule({
|
||||||
|
match = { class = "^steam$", title = "^(Friends List|Steam Settings|Special Offer.*)$" },
|
||||||
|
float = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Fullscreen video in a browser also benefits from no blur / no dim.
|
||||||
|
hl.window_rule({
|
||||||
|
match = { fullscreen = true },
|
||||||
|
no_blur = true,
|
||||||
|
no_dim = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- ── Per-application rules the user wrote ────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- `windowRules` in settings.json, edited from Settings' Windows page. Each
|
||||||
|
-- entry is data and nothing else:
|
||||||
|
--
|
||||||
|
-- { class, label, float, center, size = {w, h}, workspace, noAnim, game,
|
||||||
|
-- noDim, pin }
|
||||||
|
--
|
||||||
|
-- `class` is matched LITERALLY. Hyprland matches with RE2, so a class typed
|
||||||
|
-- into a text field is a regular expression unless something escapes it -- and
|
||||||
|
-- "org.gnome.Files" as a pattern also matches "orgxgnomexFiles", while a
|
||||||
|
-- half-typed "(" is a pattern error rather than a rule that matches nothing.
|
||||||
|
-- Escaped and anchored here, so what the user typed is what gets matched.
|
||||||
|
--
|
||||||
|
-- Emitted AFTER the shipped rules and deliberately WITHOUT a name. Hyprland
|
||||||
|
-- evaluates every named rule before every anonymous one, so a named user rule
|
||||||
|
-- would silently outrank the anonymous shipped rules above it -- the opposite
|
||||||
|
-- of the intended precedence. Anonymous, last, is what "the user's rule wins"
|
||||||
|
-- actually means here.
|
||||||
|
--
|
||||||
|
-- An entry that fails any check is skipped whole rather than emitted with the
|
||||||
|
-- bad field dropped: a rule that half-applies is harder to understand than one
|
||||||
|
-- that is not there, and the settings page can see the same thing is wrong.
|
||||||
|
|
||||||
|
local function escape_regex(value)
|
||||||
|
return (value:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?%{%}%|\\]", "\\%0"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The shell's own surfaces are layers, not windows -- but Quickshell's helper
|
||||||
|
-- windows are not, and a rule that floats or moves one of them would be a user
|
||||||
|
-- breaking their own desktop from the Windows page. Refused at both ends; this
|
||||||
|
-- is the end that matters, because the file is hand-editable.
|
||||||
|
local function reserved_class(class)
|
||||||
|
local lowered = class:lower()
|
||||||
|
return lowered:match("^quickshell") ~= nil or lowered:match("^qs%-") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function positive_integer(value, low, high)
|
||||||
|
if type(value) ~= "number" or value ~= math.floor(value) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if value < low or value > high then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, entry in ipairs(prefs.get("windowRules", {})) do
|
||||||
|
if type(entry) == "table" and type(entry.class) == "string" then
|
||||||
|
local class = entry.class
|
||||||
|
local rule = nil
|
||||||
|
|
||||||
|
if #class >= 1 and #class <= 128 and not reserved_class(class) then
|
||||||
|
rule = { match = { class = "^" .. escape_regex(class) .. "$" } }
|
||||||
|
|
||||||
|
if entry.float == true then rule.float = true end
|
||||||
|
if entry.center == true then rule.center = true end
|
||||||
|
if entry.noAnim == true then rule.no_anim = true end
|
||||||
|
if entry.noDim == true then rule.no_dim = true end
|
||||||
|
if entry.pin == true then rule.pin = true end
|
||||||
|
-- The keystone the gaming rules above use: misc.vrr,
|
||||||
|
-- render.direct_scanout and cursor.no_break_fs_vrr all key off it.
|
||||||
|
if entry.game == true then rule.content = "game" end
|
||||||
|
|
||||||
|
if entry.size ~= nil then
|
||||||
|
local size = entry.size
|
||||||
|
local width = type(size) == "table" and positive_integer(size[1], 50, 10000) or nil
|
||||||
|
local height = type(size) == "table" and positive_integer(size[2], 50, 10000) or nil
|
||||||
|
if width == nil or height == nil then
|
||||||
|
rule = nil
|
||||||
|
else
|
||||||
|
rule.size = { width, height }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if rule ~= nil and entry.workspace ~= nil then
|
||||||
|
local workspace = positive_integer(entry.workspace, 1, 10)
|
||||||
|
if workspace == nil then
|
||||||
|
rule = nil
|
||||||
|
else
|
||||||
|
rule.workspace = workspace
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if rule ~= nil then
|
||||||
|
hl.window_rule(rule)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ── Workspace rules ─────────────────────────────────────────────────────────
|
||||||
|
-- Deliberately NO "smart gaps".
|
||||||
|
--
|
||||||
|
-- The usual trick is to strip gaps, border and rounding when a workspace holds
|
||||||
|
-- a single tiled window (`workspace = "w[tv1]"` + matching window rules). It
|
||||||
|
-- was here and it was wrong for this setup: the common case is one window on a
|
||||||
|
-- workspace, so the desktop spent most of its time square-cornered and
|
||||||
|
-- edge-to-edge -- the opposite of the intended look. Gaps and rounding are
|
||||||
|
-- constant now, however many windows are open.
|
||||||
|
|
||||||
|
-- ── Layer rules (Quickshell surfaces) ───────────────────────────────────────
|
||||||
|
-- Namespaces are set in QML via WlrLayershell.namespace; keep these regexes and
|
||||||
|
-- those strings in sync. `hyprctl layers` lists what is actually live.
|
||||||
|
--
|
||||||
|
-- The bar intentionally has no rule: it is a transparent edge-to-edge input
|
||||||
|
-- surface, not glass. Its individual controls paint their own hover feedback.
|
||||||
|
-- Keeping a full-width blur rule here would spend compositor work on pixels
|
||||||
|
-- the bar does not draw.
|
||||||
|
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "qs-dock",
|
||||||
|
match = { namespace = "^qs-dock$" },
|
||||||
|
blur = true,
|
||||||
|
-- The dock's context menu and its window previews are xdg-popups of this
|
||||||
|
-- surface, not layers of their own, so they are only blurred if the rule
|
||||||
|
-- says to blur the popups too. Without it they are flat panes over the
|
||||||
|
-- wallpaper while everything else on the desktop is glass.
|
||||||
|
blur_popups = true,
|
||||||
|
ignore_alpha = 0.3,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Popovers: calendar, quick settings, notification center, tray menus.
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "qs-popover",
|
||||||
|
match = { namespace = "^qs-popover" },
|
||||||
|
blur = true,
|
||||||
|
blur_popups = true,
|
||||||
|
ignore_alpha = 0.2,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Overview, capture and local screen-reading UI dim the desktop behind them.
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "qs-overlay",
|
||||||
|
match = { namespace = "^qs-(overview|capture|screen-intelligence)$" },
|
||||||
|
blur = true,
|
||||||
|
ignore_alpha = 0.4,
|
||||||
|
dim_around = true,
|
||||||
|
no_screen_share = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Notification toasts. Blurred like every other shell surface -- without this
|
||||||
|
-- the cards are a near-transparent fill sitting directly on the wallpaper and
|
||||||
|
-- read as washed out rather than as glass.
|
||||||
|
--
|
||||||
|
-- no_anim because the toasts animate themselves in QML, and no_screen_share so
|
||||||
|
-- they don't leak into OBS or Sunshine streams.
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "qs-notifications",
|
||||||
|
match = { namespace = "^qs-notifications$" },
|
||||||
|
blur = true,
|
||||||
|
ignore_alpha = 0.15,
|
||||||
|
no_anim = true,
|
||||||
|
no_screen_share = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Signal Glass provides its own event-driven motion, so the compositor only
|
||||||
|
-- supplies the material blur and privacy behavior.
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "qs-signal-glass",
|
||||||
|
match = { namespace = "^qs-signal-glass$" },
|
||||||
|
blur = true,
|
||||||
|
ignore_alpha = 0.15,
|
||||||
|
no_anim = true,
|
||||||
|
no_screen_share = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- The launcher blurs and dims whatever is behind it.
|
||||||
|
hl.layer_rule({
|
||||||
|
name = "launcher",
|
||||||
|
match = { namespace = "^(vicinae|wofi)$" },
|
||||||
|
blur = true,
|
||||||
|
ignore_alpha = 0.5,
|
||||||
|
dim_around = true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Deuteranopia -- green-blind.
|
||||||
|
//
|
||||||
|
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||||
|
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||||
|
// services/SystemSettings.qml does the same live.
|
||||||
|
//
|
||||||
|
// Same family of matrices as protanopia.frag, weighted for the missing green
|
||||||
|
// cone instead of the red one. See that file for why this is a correction
|
||||||
|
// rather than a simulation, and for the column-major note.
|
||||||
|
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
in vec2 v_texcoord;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
|
const mat3 deuteranopia = mat3(
|
||||||
|
0.625, 0.700, 0.000,
|
||||||
|
0.375, 0.300, 0.300,
|
||||||
|
0.000, 0.000, 0.700
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 pixColor = texture(tex, v_texcoord);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(deuteranopia * pixColor.rgb, 0.0, 1.0), pixColor.a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Grayscale.
|
||||||
|
//
|
||||||
|
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||||
|
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||||
|
// services/SystemSettings.qml does the same live.
|
||||||
|
//
|
||||||
|
// Hyprland runs one fragment shader over the finished frame, so this covers
|
||||||
|
// every window, the shell, video and the cursor alike.
|
||||||
|
//
|
||||||
|
// Rec. 709 luminance weights -- the same ones an SVG <feColorMatrix
|
||||||
|
// type="saturate" values="0"> uses. A flat average would make reds and blues
|
||||||
|
// far too bright and greens far too dark, because the eye does not weigh the
|
||||||
|
// channels equally.
|
||||||
|
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
in vec2 v_texcoord;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 pixColor = texture(tex, v_texcoord);
|
||||||
|
|
||||||
|
float luminance = dot(pixColor.rgb, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
|
||||||
|
fragColor = vec4(vec3(luminance), pixColor.a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Protanopia -- red-blind.
|
||||||
|
//
|
||||||
|
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||||
|
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||||
|
// services/SystemSettings.qml does the same live.
|
||||||
|
//
|
||||||
|
// The matrix is the feColorMatrix set the mock uses, which is the widely
|
||||||
|
// carried HCIRN-derived one: it redistributes the red channel into the two the
|
||||||
|
// eye can still separate, so a red/green pair that was one colour becomes two
|
||||||
|
// distinguishable ones. It is a CORRECTION, not a simulation -- the point is to
|
||||||
|
// make the screen readable, not to show what protanopia looks like.
|
||||||
|
//
|
||||||
|
// Row-major here, column-major to GLSL: mat3 takes its arguments column by
|
||||||
|
// column, so the transpose below is the matrix as written in the SVG.
|
||||||
|
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
in vec2 v_texcoord;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
|
const mat3 protanopia = mat3(
|
||||||
|
0.567, 0.558, 0.000,
|
||||||
|
0.433, 0.442, 0.242,
|
||||||
|
0.000, 0.000, 0.758
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 pixColor = texture(tex, v_texcoord);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(protanopia * pixColor.rgb, 0.0, 1.0), pixColor.a);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Tritanopia -- blue-blind.
|
||||||
|
//
|
||||||
|
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||||
|
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||||
|
// services/SystemSettings.qml does the same live.
|
||||||
|
//
|
||||||
|
// Same family of matrices as protanopia.frag, weighted for the missing blue
|
||||||
|
// cone. See that file for why this is a correction rather than a simulation,
|
||||||
|
// and for the column-major note.
|
||||||
|
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
in vec2 v_texcoord;
|
||||||
|
layout(location = 0) out vec4 fragColor;
|
||||||
|
uniform sampler2D tex;
|
||||||
|
|
||||||
|
const mat3 tritanopia = mat3(
|
||||||
|
0.950, 0.000, 0.000,
|
||||||
|
0.050, 0.433, 0.475,
|
||||||
|
0.000, 0.567, 0.525
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 pixColor = texture(tex, v_texcoord);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(tritanopia * pixColor.rgb, 0.0, 1.0), pixColor.a);
|
||||||
|
}
|
||||||
@@ -183,7 +183,6 @@ font_size 14.0
|
|||||||
|
|
||||||
#: Cursor customization {{{
|
#: Cursor customization {{{
|
||||||
|
|
||||||
cursor #c8d3f5
|
|
||||||
|
|
||||||
#: Default cursor color. If set to the special value none the cursor
|
#: Default cursor color. If set to the special value none the cursor
|
||||||
#: will be rendered with a "reverse video" effect. It's color will be
|
#: will be rendered with a "reverse video" effect. It's color will be
|
||||||
@@ -193,7 +192,6 @@ cursor #c8d3f5
|
|||||||
#: precedence. Also, the cursor colors are modified if the cell
|
#: precedence. Also, the cursor colors are modified if the cell
|
||||||
#: background and foreground colors have very low contrast.
|
#: background and foreground colors have very low contrast.
|
||||||
|
|
||||||
cursor_text_color #111325
|
|
||||||
|
|
||||||
#: The color of text under the cursor. If you want it rendered with
|
#: The color of text under the cursor. If you want it rendered with
|
||||||
#: the background color of the cell underneath instead, use the
|
#: the background color of the cell underneath instead, use the
|
||||||
@@ -312,7 +310,6 @@ cursor_shape block
|
|||||||
#: robustly with the ever-changing sea of bugs that is Cocoa is too
|
#: robustly with the ever-changing sea of bugs that is Cocoa is too
|
||||||
#: much effort.
|
#: much effort.
|
||||||
|
|
||||||
url_color #4fd6be
|
|
||||||
# url_style curly
|
# url_style curly
|
||||||
|
|
||||||
#: The color and style for highlighting URLs on mouse-over. url_style
|
#: The color and style for highlighting URLs on mouse-over. url_style
|
||||||
@@ -734,12 +731,10 @@ window_border_width 1pt
|
|||||||
#: placed centrally. A value of top-left means the padding will be
|
#: placed centrally. A value of top-left means the padding will be
|
||||||
#: only at the bottom and right edges.
|
#: only at the bottom and right edges.
|
||||||
|
|
||||||
active_border_color #82aaff
|
|
||||||
|
|
||||||
#: The color for the border of the active window. Set this to none to
|
#: The color for the border of the active window. Set this to none to
|
||||||
#: not draw borders around the active window.
|
#: not draw borders around the active window.
|
||||||
|
|
||||||
inactive_border_color #2f334d
|
|
||||||
|
|
||||||
#: The color for the border of inactive windows.
|
#: The color for the border of inactive windows.
|
||||||
|
|
||||||
@@ -753,7 +748,7 @@ inactive_border_color #2f334d
|
|||||||
#: Fade the text in inactive windows by the specified amount (a number
|
#: Fade the text in inactive windows by the specified amount (a number
|
||||||
#: between zero and one, with zero being fully faded).
|
#: between zero and one, with zero being fully faded).
|
||||||
|
|
||||||
hide_window_decorations yes
|
hide_window_decorations no
|
||||||
|
|
||||||
#: Hide the window decorations (title-bar and window borders) with
|
#: Hide the window decorations (title-bar and window borders) with
|
||||||
#: yes. On macOS, titlebar-only can be used to only hide the titlebar.
|
#: yes. On macOS, titlebar-only can be used to only hide the titlebar.
|
||||||
@@ -960,16 +955,11 @@ tab_powerline_style round
|
|||||||
#: Template to use for active tabs. If not specified falls back to
|
#: Template to use for active tabs. If not specified falls back to
|
||||||
#: tab_title_template.
|
#: tab_title_template.
|
||||||
|
|
||||||
active_tab_foreground #1e2030
|
|
||||||
active_tab_background #82aaff
|
|
||||||
# active_tab_font_style bold-italic
|
# active_tab_font_style bold-italic
|
||||||
inactive_tab_foreground #c8d3f5
|
|
||||||
inactive_tab_background #42465a
|
|
||||||
# inactive_tab_font_style normal
|
# inactive_tab_font_style normal
|
||||||
|
|
||||||
#: Tab bar colors and styles.
|
#: Tab bar colors and styles.
|
||||||
|
|
||||||
tab_bar_background #222436
|
|
||||||
|
|
||||||
#: Background color for the tab bar. Defaults to using the terminal
|
#: Background color for the tab bar. Defaults to using the terminal
|
||||||
#: background color.
|
#: background color.
|
||||||
@@ -983,8 +973,8 @@ tab_bar_margin_color none
|
|||||||
|
|
||||||
#: Color scheme {{{
|
#: Color scheme {{{
|
||||||
|
|
||||||
foreground #c8d3f5
|
|
||||||
background #222436
|
include current-theme.conf
|
||||||
|
|
||||||
#: The foreground and background colors.
|
#: The foreground and background colors.
|
||||||
|
|
||||||
@@ -1043,8 +1033,6 @@ dim_opacity 1
|
|||||||
#: How much to dim text that has the DIM/FAINT attribute set. One
|
#: How much to dim text that has the DIM/FAINT attribute set. One
|
||||||
#: means no dimming and zero means fully dimmed (i.e. invisible).
|
#: means no dimming and zero means fully dimmed (i.e. invisible).
|
||||||
|
|
||||||
selection_foreground #2d3f76
|
|
||||||
selection_background #c8d3f5
|
|
||||||
|
|
||||||
#: The foreground and background colors for text selected with the
|
#: The foreground and background colors for text selected with the
|
||||||
#: mouse. Setting both of these to none will cause a "reverse video"
|
#: mouse. Setting both of these to none will cause a "reverse video"
|
||||||
@@ -1060,47 +1048,29 @@ selection_background #c8d3f5
|
|||||||
#: dull and bright version, for the first 16 colors. You can set the
|
#: dull and bright version, for the first 16 colors. You can set the
|
||||||
#: remaining 240 colors as color16 to color255.
|
#: remaining 240 colors as color16 to color255.
|
||||||
|
|
||||||
color0 #1b1d2b
|
|
||||||
color8 #444a73
|
|
||||||
|
|
||||||
#: black
|
#: black
|
||||||
|
|
||||||
color1 #ff757f
|
|
||||||
color9 #ff757f
|
|
||||||
|
|
||||||
#: red
|
#: red
|
||||||
|
|
||||||
color2 #a5e8b5
|
|
||||||
color10 #a5e8b5
|
|
||||||
|
|
||||||
#: green
|
#: green
|
||||||
color3 #ffc777
|
|
||||||
color11 #ffc777
|
|
||||||
|
|
||||||
#: yellow
|
#: yellow
|
||||||
|
|
||||||
color4 #82aaff
|
|
||||||
color12 #82aaff
|
|
||||||
|
|
||||||
#: blue
|
#: blue
|
||||||
|
|
||||||
color5 #c099ff
|
|
||||||
color13 #c099ff
|
|
||||||
|
|
||||||
#: magenta
|
#: magenta
|
||||||
|
|
||||||
color6 #86e1fc
|
|
||||||
color14 #86e1fc
|
|
||||||
|
|
||||||
#: cyan
|
#: cyan
|
||||||
|
|
||||||
color7 #828bb8
|
|
||||||
color15 #c8d3f5
|
|
||||||
|
|
||||||
#: white
|
#: white
|
||||||
|
|
||||||
color16 #ff966c
|
|
||||||
color17 #c53b53
|
|
||||||
|
|
||||||
# mark1_foreground black
|
# mark1_foreground black
|
||||||
|
|
||||||
@@ -1504,7 +1474,7 @@ wayland_titlebar_color #222436
|
|||||||
#: configuration will change this value only for newly created OS
|
#: configuration will change this value only for newly created OS
|
||||||
#: windows.
|
#: windows.
|
||||||
|
|
||||||
linux_display_server Wayland
|
# linux_display_server x11
|
||||||
|
|
||||||
#: Choose between Wayland and X11 backends. By default, an appropriate
|
#: Choose between Wayland and X11 backends. By default, an appropriate
|
||||||
#: backend based on the system state is chosen automatically. Set it
|
#: backend based on the system state is chosen automatically. Set it
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Open plain file paths with neovim in a new kitty tab
|
||||||
|
protocol file
|
||||||
|
mime text/*
|
||||||
|
action launch --type=tab nvim -- "$FILE_PATH"
|
||||||
|
|
||||||
|
# Also handle paths with line/column numbers if supported by the source
|
||||||
|
protocol file
|
||||||
|
fragment_matches ^(\d+)(?::(\d+))?$
|
||||||
|
action launch --type=tab nvim "+call cursor($1, ${2:-1})" -- "$FILE_PATH"
|
||||||
@@ -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,45 @@
|
|||||||
|
# Tokyo Night Moon — the dark theme Panama ships.
|
||||||
|
#
|
||||||
|
# Reference, not the source of the running colours. kitty.conf includes
|
||||||
|
# current-theme.conf, which panama-theme-apps RENDERS from the active theme's
|
||||||
|
# palette -- so any of the ten themes reaches the terminal, not only the two
|
||||||
|
# Tokyo Nights. This file used to be copied into that one; it is kept because
|
||||||
|
# it is a complete kitty theme somebody may want to include directly, and
|
||||||
|
# because it is what Moon is supposed to look like.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
tt.*
|
||||||
|
.tests
|
||||||
|
doc/tags
|
||||||
|
debug
|
||||||
|
.repro
|
||||||
|
foo.*
|
||||||
|
*.log
|
||||||
|
data
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"runtime.version": "LuaJIT",
|
|
||||||
"runtime.path": [
|
|
||||||
"lua/?.lua",
|
|
||||||
"lua/?/init.lua"
|
|
||||||
],
|
|
||||||
"diagnostics.globals": ["vim"],
|
|
||||||
"workspace.checkThirdParty": false,
|
|
||||||
"workspace.library": [ "$VIMRUNTIME" ],
|
|
||||||
"workspace.maxPreload": 100000,
|
|
||||||
"workspace.preloadFileSize": 10000,
|
|
||||||
"telemetry.enable": false
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"neodev": {
|
||||||
|
"library": {
|
||||||
|
"enabled": true,
|
||||||
|
"plugins": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"neoconf": {
|
||||||
|
"plugins": {
|
||||||
|
"lua_ls": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# 💤 LazyVim
|
||||||
|
|
||||||
|
A starter template for [LazyVim](https://github.com/LazyVim/LazyVim).
|
||||||
|
Refer to the [documentation](https://lazyvim.github.io/installation) to get started.
|
||||||
@@ -1 +1 @@
|
|||||||
require'config'
|
require("config.lazy")
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"LuaSnip": { "branch": "master", "commit": "458560534a73f7f8d7a11a146c801db00b081df0" },
|
|
||||||
"avante.nvim": { "branch": "main", "commit": "80f7079556c6acf3d3effa13c22f0e4fd00bcffd" },
|
|
||||||
"barbar.nvim": { "branch": "master", "commit": "53b5a2f34b68875898f0531032fbf090e3952ad7" },
|
|
||||||
"cloak.nvim": { "branch": "main", "commit": "648aca6d33ec011dc3166e7af3b38820d01a71e4" },
|
|
||||||
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
|
|
||||||
"cmp-cmdline": { "branch": "main", "commit": "d126061b624e0af6c3a556428712dd4d4194ec6d" },
|
|
||||||
"cmp-copilot": { "branch": "main", "commit": "1f3f31c54bd71e41ed157430702bc2837ea582ab" },
|
|
||||||
"cmp-dotenv": { "branch": "main", "commit": "4dd53aab60982f1f75848aec5e6214986263325e" },
|
|
||||||
"cmp-emoji": { "branch": "main", "commit": "e8398e2adf512a03bb4e1728ca017ffeac670a9f" },
|
|
||||||
"cmp-git": { "branch": "main", "commit": "b24309c386c9666c549a1abaedd4956541676d06" },
|
|
||||||
"cmp-luasnip-choice": { "branch": "master", "commit": "4f49232e51c9df379b9daf43f25f7ee6320450f0" },
|
|
||||||
"cmp-nerdfont": { "branch": "main", "commit": "e97482344ebed29093015a18c155057adf5c842b" },
|
|
||||||
"cmp-npm": { "branch": "main", "commit": "2337f109f51a09297596dd6b538b70ccba92b4e4" },
|
|
||||||
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
|
|
||||||
"cmp-nvim-lsp-document-symbol": { "branch": "main", "commit": "f94f7ba948e32cd302caba1c2ca3f7c697fb4fcf" },
|
|
||||||
"cmp-nvim-lsp-signature-help": { "branch": "main", "commit": "fd3e882e56956675c620898bf1ffcf4fcbe7ec84" },
|
|
||||||
"cmp-nvim-lua": { "branch": "main", "commit": "e3a22cb071eb9d6508a156306b102c45cd2d573d" },
|
|
||||||
"cmp-path": { "branch": "main", "commit": "c642487086dbd9a93160e1679a1327be111cbc25" },
|
|
||||||
"cmp-tw2css": { "branch": "main", "commit": "1abe0eebcb57fcbd5538d054f0db61f4e4a1302b" },
|
|
||||||
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" },
|
|
||||||
"conform.nvim": { "branch": "master", "commit": "9b8fa5e0b78168f68bee9bf886dc20f287c61e02" },
|
|
||||||
"copilot.vim": { "branch": "release", "commit": "f89e977c87180519ba3b942200e3d05b17b1e2fc" },
|
|
||||||
"dressing.nvim": { "branch": "master", "commit": "2d7c2db2507fa3c4956142ee607431ddb2828639" },
|
|
||||||
"fidget.nvim": { "branch": "main", "commit": "e32b672d8fd343f9d6a76944fedb8c61d7d8111a" },
|
|
||||||
"friendly-snippets": { "branch": "main", "commit": "572f5660cf05f8cd8834e096d7b4c921ba18e175" },
|
|
||||||
"gitsigns.nvim": { "branch": "main", "commit": "5813e4878748805f1518cee7abb50fd7205a3a48" },
|
|
||||||
"image.nvim": { "branch": "master", "commit": "446a8a5cc7a3eae3185ee0c697732c32a5547a0b" },
|
|
||||||
"img-clip.nvim": { "branch": "main", "commit": "93fcf50924108262caec66b900077987dfcb7c7f" },
|
|
||||||
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
|
|
||||||
"lspkind.nvim": { "branch": "master", "commit": "3ddd1b4edefa425fda5a9f95a4f25578727c0bb3" },
|
|
||||||
"lualine.nvim": { "branch": "master", "commit": "47f91c416daef12db467145e16bed5bbfe00add8" },
|
|
||||||
"mason-lspconfig.nvim": { "branch": "main", "commit": "3b3571b4dadbcb464804466e9872e7246c316af7" },
|
|
||||||
"mason.nvim": { "branch": "main", "commit": "57e5a8addb8c71fb063ee4acda466c7cf6ad2800" },
|
|
||||||
"mcphub.nvim": { "branch": "main", "commit": "5193329d510a68f1f5bf189960642c925c177a3a" },
|
|
||||||
"mini.icons": { "branch": "main", "commit": "ff2e4f1d29f659cc2bad0f9256f2f6195c6b2428" },
|
|
||||||
"neo-tree.nvim": { "branch": "v3.x", "commit": "f3df514fff2bdd4318127c40470984137f87b62e" },
|
|
||||||
"nerdcommenter": { "branch": "master", "commit": "a462bbda1e26f44fb3d3eb9d9d1c6a07aa98e665" },
|
|
||||||
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
|
|
||||||
"nvim-autopairs": { "branch": "master", "commit": "c2a0dd0d931d0fb07665e1fedb1ea688da3b80b4" },
|
|
||||||
"nvim-cmp": { "branch": "main", "commit": "d97d85e01339f01b842e6ec1502f639b080cb0fc" },
|
|
||||||
"nvim-lsp-file-operations": { "branch": "master", "commit": "9744b738183a5adca0f916527922078a965515ed" },
|
|
||||||
"nvim-lspconfig": { "branch": "master", "commit": "c4f67bf85b01a57e3c130352c0a0e453ab8cd5b9" },
|
|
||||||
"nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" },
|
|
||||||
"nvim-treesitter-context": { "branch": "master", "commit": "64dd4cf3f6fd0ab17622c5ce15c91fc539c3f24a" },
|
|
||||||
"nvim-ts-autotag": { "branch": "main", "commit": "c4ca798ab95b316a768d51eaaaee48f64a4a46bc" },
|
|
||||||
"nvim-web-devicons": { "branch": "master", "commit": "8dcb311b0c92d460fac00eac706abd43d94d68af" },
|
|
||||||
"nvim-window-picker": { "branch": "main", "commit": "6382540b2ae5de6c793d4aa2e3fe6dbb518505ec" },
|
|
||||||
"playground": { "branch": "master", "commit": "ba48c6a62a280eefb7c85725b0915e021a1a0749" },
|
|
||||||
"plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" },
|
|
||||||
"render-markdown.nvim": { "branch": "main", "commit": "07d088bf8bdadd159eb807b90eaee86a4778383f" },
|
|
||||||
"snacks.nvim": { "branch": "main", "commit": "fe7cfe9800a182274d0f868a74b7263b8c0c020b" },
|
|
||||||
"supermaven-nvim": { "branch": "main", "commit": "07d20fce48a5629686aefb0a7cd4b25e33947d50" },
|
|
||||||
"telescope.nvim": { "branch": "master", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" },
|
|
||||||
"tokyonight.nvim": { "branch": "main", "commit": "5da1b76e64daf4c5d410f06bcb6b9cb640da7dfd" },
|
|
||||||
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
|
|
||||||
"undotree": { "branch": "master", "commit": "0f1c9816975b5d7f87d5003a19c53c6fd2ff6f7f" }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extras": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"install_version": 8,
|
||||||
|
"news": {
|
||||||
|
"NEWS.md": "11866"
|
||||||
|
},
|
||||||
|
"version": 8
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
local augroup = vim.api.nvim_create_augroup
|
|
||||||
local gib_group = augroup('Gib', {})
|
|
||||||
local autocmd = vim.api.nvim_create_autocmd
|
|
||||||
local yank_group = augroup('HighlightYank', {})
|
|
||||||
|
|
||||||
function R(name)
|
|
||||||
require("plenary.reload").reload_module(name)
|
|
||||||
end
|
|
||||||
|
|
||||||
autocmd('TextYankPost', {
|
|
||||||
group = yank_group,
|
|
||||||
pattern = '*',
|
|
||||||
callback = function()
|
|
||||||
vim.highlight.on_yank({
|
|
||||||
higroup = 'IncSearch',
|
|
||||||
timeout = 40,
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
autocmd({"BufWritePre"}, {
|
|
||||||
group = gib_group,
|
|
||||||
pattern = "*",
|
|
||||||
command = [[%s/\s\+$//e]],
|
|
||||||
})
|
|
||||||
|
|
||||||
autocmd({'BufWritePost'}, {
|
|
||||||
pattern = 'init.lua',
|
|
||||||
command = 'source <afile>',
|
|
||||||
})
|
|
||||||
|
|
||||||
autocmd('LspAttach', {
|
|
||||||
group = gib_group,
|
|
||||||
callback = function(e)
|
|
||||||
local opts = { buffer = e.buf }
|
|
||||||
vim.keymap.set('n', '<leader>kf', function() vim.lsp.buf.format() end, opts)
|
|
||||||
vim.keymap.set("n", "<leader>gd", function() vim.lsp.buf.definition() end, opts)
|
|
||||||
vim.keymap.set("n", "<leader>kw", function() vim.lsp.buf.workspace_symbol() end, opts)
|
|
||||||
vim.keymap.set("n", "<leader>kd", function() vim.diagnostic.open_float() end, opts)
|
|
||||||
vim.keymap.set("n", "<leader>vca", function() vim.lsp.buf.code_action() end, opts)
|
|
||||||
vim.keymap.set("n", "<leader>re", function() vim.lsp.buf.references() end, opts)
|
|
||||||
vim.keymap.set("n", "<leader>rn", function() vim.lsp.buf.rename() end, opts)
|
|
||||||
vim.keymap.set("i", "<C-h>", function() vim.lsp.buf.signature_help() end, opts)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Autocmds are automatically loaded on the VeryLazy event
|
||||||
|
-- Default autocmds that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/autocmds.lua
|
||||||
|
|
||||||
|
-- Strip trailing whitespace on save
|
||||||
|
vim.api.nvim_create_autocmd("BufWritePre", {
|
||||||
|
pattern = "*",
|
||||||
|
callback = function()
|
||||||
|
local view = vim.fn.winsaveview()
|
||||||
|
vim.cmd([[keeppatterns %s/\s\+$//e]])
|
||||||
|
vim.fn.winrestview(view)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Follow the desktop's light/dark setting while running, not only at startup.
|
||||||
|
require("config.panama").watch()
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
require'config.options'
|
|
||||||
require'config.keymaps'
|
|
||||||
require'config.lazy'
|
|
||||||
require'config.autocmd'
|
|
||||||
@@ -1,41 +1,155 @@
|
|||||||
vim.g.mapleader = ' '
|
-- Keymaps are automatically loaded on the VeryLazy event
|
||||||
vim.g.maplocalleader = "\\"
|
-- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua
|
||||||
vim.keymap.set('n', '<leader><leader>s', ':update<CR> :source<CR>')
|
|
||||||
vim.keymap.set('n', '<leader><leader>x', ':!chmod +x %<CR>', { silent = true })
|
-- Remove LazyVim's C-h/j/k/l window navigation (using <leader>h/j/k/l instead)
|
||||||
vim.keymap.set({'n', 'v', 'x'}, '<leader>h', '<C-w>h')
|
pcall(vim.keymap.del, "n", "<C-h>")
|
||||||
vim.keymap.set({'n', 'v', 'x'}, '<leader>j', '<C-w>j')
|
pcall(vim.keymap.del, "n", "<C-j>")
|
||||||
vim.keymap.set({'n', 'v', 'x'}, '<leader>k', '<C-w>k')
|
pcall(vim.keymap.del, "n", "<C-k>")
|
||||||
vim.keymap.set({'n', 'v', 'x'}, '<leader>l', '<C-w>l')
|
pcall(vim.keymap.del, "n", "<C-l>")
|
||||||
vim.keymap.set('n', '<leader>w', ':write<CR>')
|
pcall(vim.keymap.del, "t", "<C-h>")
|
||||||
vim.keymap.set('n', '<leader>q', ':quit<CR>')
|
pcall(vim.keymap.del, "t", "<C-j>")
|
||||||
-- Move the selected lines up or down one line and reselect
|
pcall(vim.keymap.del, "t", "<C-k>")
|
||||||
vim.keymap.set('v', 'J', ':m \'>+1<CR>gv=gv')
|
pcall(vim.keymap.del, "t", "<C-l>")
|
||||||
vim.keymap.set('v', 'K', ':m \'<-2<CR>gv=gv')
|
-- Remove LazyVim's :Lazy shortcut — <leader>l is used for window navigation
|
||||||
-- Join the current line with the line below and reposition the cursor
|
pcall(vim.keymap.del, "n", "<leader>l")
|
||||||
vim.keymap.set('n', 'J', 'mzJ`z')
|
|
||||||
-- Scroll down or up and reposition the cursor
|
-- Window navigation
|
||||||
vim.keymap.set('n', '<C-d>', '<C-d>zz')
|
vim.keymap.set(
|
||||||
vim.keymap.set('n', '<C-u>', '<C-u>zz')
|
{ "n", "v", "x" },
|
||||||
-- Search forward and reposition the cursor
|
"<leader>h",
|
||||||
vim.keymap.set('n', 'n', 'nzzzv')
|
"<C-w>h",
|
||||||
-- Search backward and reposition the cursor
|
{ desc = "Go to left window", silent = true }
|
||||||
vim.keymap.set('n', 'N', 'Nzzzv')
|
)
|
||||||
-- Paste the selection from the system clipboard
|
vim.keymap.set(
|
||||||
vim.keymap.set({'n', 'v', 'x'}, '<leader>p', [["+p]])
|
{ "n", "v", "x" },
|
||||||
vim.keymap.set({'n', 'v', 'x'}, '<leader>P', [["+P]])
|
"<leader>j",
|
||||||
-- Move selected text to the black hole register & replace with copied text.
|
"<C-w>j",
|
||||||
vim.keymap.set('x', '<leader>v', [["_dP]])
|
{ desc = "Go to lower window", silent = true }
|
||||||
-- Yank (copy) the selection to the system clipboard
|
)
|
||||||
vim.keymap.set({'n', 'v'}, '<leader>y', [["+y]])
|
vim.keymap.set(
|
||||||
-- Yank (copy) the entire buffer to the system clipboard
|
{ "n", "v", "x" },
|
||||||
vim.keymap.set('n', '<leader>yy', [["+Y]])
|
"<leader>k",
|
||||||
-- Yank all the text in the file to the system clipboard
|
"<C-w>k",
|
||||||
vim.keymap.set('n', '<leader>YY', ':%y+<CR>')
|
{ desc = "Go to upper window", silent = true }
|
||||||
-- Delete the selection without yanking (copying) it
|
)
|
||||||
vim.keymap.set({'n', 'v'}, '<leader>d', [["_d]])
|
vim.keymap.set(
|
||||||
-- Delete the line without yanking (copying) it
|
{ "n", "v", "x" },
|
||||||
vim.keymap.set('n', '<leader>dd', [["_dd]])
|
"<leader>l",
|
||||||
-- Map Q in Normal mode to do nothing (nop)
|
"<C-w>l",
|
||||||
vim.keymap.set('n', 'Q', '<nop>')
|
{ desc = "Go to right window", silent = true }
|
||||||
-- Perform a search and replace operation using the word under the cursor
|
)
|
||||||
vim.keymap.set('n', '<leader>sr', [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/gI<Left><Left><Left>]])
|
|
||||||
|
-- Buffer tab navigation
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<C-h>",
|
||||||
|
"<cmd>BufferLineCyclePrev<cr>",
|
||||||
|
{ desc = "Previous buffer", silent = true }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<C-l>",
|
||||||
|
"<cmd>BufferLineCycleNext<cr>",
|
||||||
|
{ desc = "Next buffer", silent = true }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<C-j>",
|
||||||
|
"<cmd>BufferLineMovePrev<cr>",
|
||||||
|
{ desc = "Move buffer left", silent = true }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<C-k>",
|
||||||
|
"<cmd>BufferLineMoveNext<cr>",
|
||||||
|
{ desc = "Move buffer right", silent = true }
|
||||||
|
)
|
||||||
|
vim.keymap.set("n", "<C-q>", function()
|
||||||
|
Snacks.bufdelete()
|
||||||
|
end, { desc = "Close buffer", silent = true })
|
||||||
|
vim.keymap.set("n", "<C-a>", function()
|
||||||
|
Snacks.bufdelete.other()
|
||||||
|
end, { desc = "Close other buffers", silent = true })
|
||||||
|
|
||||||
|
-- Disable ex mode
|
||||||
|
vim.keymap.set("n", "Q", "<nop>")
|
||||||
|
|
||||||
|
-- Move selected lines up/down
|
||||||
|
vim.keymap.set(
|
||||||
|
"v",
|
||||||
|
"J",
|
||||||
|
":m '>+1<CR>gv=gv",
|
||||||
|
{ desc = "Move selection down", silent = true }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
"v",
|
||||||
|
"K",
|
||||||
|
":m '<-2<CR>gv=gv",
|
||||||
|
{ desc = "Move selection up", silent = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Join line without moving cursor
|
||||||
|
vim.keymap.set("n", "J", "mzJ`z", { desc = "Join line, keep cursor" })
|
||||||
|
|
||||||
|
-- Scroll and center
|
||||||
|
vim.keymap.set("n", "<C-d>", "<C-d>zz", { desc = "Scroll down and center" })
|
||||||
|
vim.keymap.set("n", "<C-u>", "<C-u>zz", { desc = "Scroll up and center" })
|
||||||
|
|
||||||
|
-- Search and center
|
||||||
|
vim.keymap.set("n", "n", "nzzzv", { desc = "Next search result" })
|
||||||
|
vim.keymap.set("n", "N", "Nzzzv", { desc = "Prev search result" })
|
||||||
|
|
||||||
|
-- Yank to system clipboard
|
||||||
|
vim.keymap.set({ "n", "v" }, "<leader>y", '"+y', { desc = "Yank to clipboard" })
|
||||||
|
vim.keymap.set("n", "<leader>Y", '"+Y', { desc = "Yank line to clipboard" })
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<leader>YY",
|
||||||
|
":%y+<CR>",
|
||||||
|
{ desc = "Yank buffer to clipboard" }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Delete to black hole register (no yank side effect)
|
||||||
|
vim.keymap.set(
|
||||||
|
{ "n", "v" },
|
||||||
|
"<leader>d",
|
||||||
|
'"_d',
|
||||||
|
{ desc = "Delete to black hole" }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<leader>dd",
|
||||||
|
'"_dd',
|
||||||
|
{ desc = "Delete line to black hole" }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Paste from system clipboard
|
||||||
|
vim.keymap.set(
|
||||||
|
{ "n", "v", "x" },
|
||||||
|
"<leader>p",
|
||||||
|
'"+p',
|
||||||
|
{ desc = "Paste from clipboard (after)" }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
{ "n", "v", "x" },
|
||||||
|
"<leader>P",
|
||||||
|
'"+P',
|
||||||
|
{ desc = "Paste from clipboard (before)" }
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Paste over selection without overwriting the yank register
|
||||||
|
vim.keymap.set("x", "<leader>v", '"_dP', { desc = "Paste over selection" })
|
||||||
|
|
||||||
|
-- Utility
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<leader>so",
|
||||||
|
":update<CR>:source<CR>",
|
||||||
|
{ desc = "Save and source file" }
|
||||||
|
)
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
"<leader><leader>x",
|
||||||
|
":!chmod +x %<CR>",
|
||||||
|
{ desc = "Make file executable" }
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||||
if not (vim.uv or vim.loop).fs_stat(lazypath) then
|
if not (vim.uv or vim.loop).fs_stat(lazypath) then
|
||||||
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
|
local lazyrepo = "https://github.com/folke/lazy.nvim.git"
|
||||||
local out = vim.fn.system({
|
local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
|
||||||
"git",
|
|
||||||
"clone",
|
|
||||||
"--filter=blob:none",
|
|
||||||
"--branch=stable",
|
|
||||||
lazyrepo,
|
|
||||||
lazypath
|
|
||||||
})
|
|
||||||
if vim.v.shell_error ~= 0 then
|
if vim.v.shell_error ~= 0 then
|
||||||
vim.api.nvim_echo({
|
vim.api.nvim_echo({
|
||||||
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
|
{ "Failed to clone lazy.nvim:\n", "ErrorMsg" },
|
||||||
@@ -21,8 +14,47 @@ if not (vim.uv or vim.loop).fs_stat(lazypath) then
|
|||||||
end
|
end
|
||||||
vim.opt.rtp:prepend(lazypath)
|
vim.opt.rtp:prepend(lazypath)
|
||||||
|
|
||||||
require'lazy'.setup({
|
require("lazy").setup({
|
||||||
spec = { { import = 'plugins' } },
|
spec = {
|
||||||
install = { colorscheme = { 'tokyonight-moon' } },
|
-- add LazyVim and import its plugins
|
||||||
checker = { enabled = false },
|
{ "LazyVim/LazyVim", import = "lazyvim.plugins" },
|
||||||
|
-- LazyVim extras (must come before user plugins)
|
||||||
|
{ import = "lazyvim.plugins.extras.lang.typescript" },
|
||||||
|
{ import = "lazyvim.plugins.extras.formatting.prettier" },
|
||||||
|
{ import = "lazyvim.plugins.extras.linting.eslint" },
|
||||||
|
{ import = "lazyvim.plugins.extras.editor.snacks_explorer" },
|
||||||
|
{ import = "lazyvim.plugins.extras.editor.snacks_picker" },
|
||||||
|
{ import = "lazyvim.plugins.extras.ai.supermaven" },
|
||||||
|
-- import/override with your plugins
|
||||||
|
{ import = "plugins" },
|
||||||
|
},
|
||||||
|
defaults = {
|
||||||
|
-- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup.
|
||||||
|
-- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default.
|
||||||
|
lazy = false,
|
||||||
|
-- It's recommended to leave version=false for now, since a lot the plugin that support versioning,
|
||||||
|
-- have outdated releases, which may break your Neovim install.
|
||||||
|
version = false, -- always use the latest git commit
|
||||||
|
-- version = "*", -- try installing the latest stable version for plugins that support semver
|
||||||
|
},
|
||||||
|
install = { colorscheme = { "tokyonight", "habamax" } },
|
||||||
|
checker = {
|
||||||
|
enabled = true, -- check for plugin updates periodically
|
||||||
|
notify = false, -- notify on update
|
||||||
|
}, -- automatically check for plugin updates
|
||||||
|
performance = {
|
||||||
|
rtp = {
|
||||||
|
-- disable some rtp plugins
|
||||||
|
disabled_plugins = {
|
||||||
|
"gzip",
|
||||||
|
-- "matchit",
|
||||||
|
-- "matchparen",
|
||||||
|
-- "netrwPlugin",
|
||||||
|
"tarPlugin",
|
||||||
|
"tohtml",
|
||||||
|
"tutor",
|
||||||
|
"zipPlugin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
vim.o.number = true
|
-- Options are automatically loaded before lazy.nvim startup
|
||||||
vim.o.relativenumber = true
|
-- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua
|
||||||
vim.o.winborder = 'rounded'
|
|
||||||
vim.o.tabstop = 2
|
-- LazyVim's default syncs clipboard only outside SSH:
|
||||||
vim.o.softtabstop = 2
|
-- vim.opt.clipboard = vim.env.SSH_CONNECTION and "" or "unnamedplus"
|
||||||
vim.o.shiftwidth = 2
|
-- Disabled entirely because wl-clipboard triggers GNOME window spawning on every yank.
|
||||||
vim.o.expandtab = true
|
-- Use <leader>y/<leader>p for explicit clipboard access.
|
||||||
vim.o.smartindent = true
|
vim.opt.clipboard = ""
|
||||||
vim.o.swapfile = false
|
vim.opt.winborder = "rounded"
|
||||||
vim.o.undodir = os.getenv('HOME') .. '/.vim/undodir'
|
vim.opt.updatetime = 50
|
||||||
vim.o.undofile = true
|
vim.opt.undodir = os.getenv("HOME") .. "/.vim/undodir"
|
||||||
vim.o.hlsearch = true
|
vim.opt.listchars = { trail = "⋅", nbsp = "⋅", tab = " " }
|
||||||
vim.o.incsearch = true
|
|
||||||
vim.o.termguicolors = true
|
vim.g.ai_cmp = true
|
||||||
vim.o.scrolloff = 4
|
vim.g.markdown_recommended_style = 0
|
||||||
vim.o.signcolumn = 'yes'
|
|
||||||
vim.o.updatetime = 50
|
|
||||||
vim.opt.isfname:append('@-@')
|
|
||||||
vim.cmd([[set list]])
|
|
||||||
vim.cmd([[set listchars=trail:⋅,nbsp:⋅,tab:\ \ ]])
|
|
||||||
vim.cmd(':hi statusline guibg=NONE')
|
|
||||||
|
|||||||
@@ -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,239 +0,0 @@
|
|||||||
return {
|
|
||||||
'yetone/avante.nvim',
|
|
||||||
-- if you want to build from source then do `make BUILD_FROM_SOURCE=true`
|
|
||||||
-- ⚠️ must add this setting! ! !
|
|
||||||
build = vim.fn.has('win32') ~= 0
|
|
||||||
and 'powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false'
|
|
||||||
or 'make',
|
|
||||||
event = 'VeryLazy',
|
|
||||||
version = false, -- Never set this value to '*'! Never!
|
|
||||||
---@module 'avante'
|
|
||||||
---@type avante.Config
|
|
||||||
opts = {
|
|
||||||
instructions_file = 'avante.md',
|
|
||||||
provider = 'openai',
|
|
||||||
auto_suggestions_provider = 'openai',
|
|
||||||
providers = {
|
|
||||||
claude = {
|
|
||||||
endpoint = 'https://api.anthropic.com',
|
|
||||||
model = 'claude-sonnet-4-20250514',
|
|
||||||
timeout = 30000,
|
|
||||||
extra_request_body = {
|
|
||||||
temperature = 0.75,
|
|
||||||
max_tokens = 4096,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
openai = {
|
|
||||||
endpoint = 'https://api.openai.com/v1',
|
|
||||||
model = 'gpt-5',
|
|
||||||
extra_request_body = {
|
|
||||||
temperature = 1,
|
|
||||||
max_completion_tokens = 8192,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rag_service = {
|
|
||||||
enabled = false,
|
|
||||||
host_path = '/home/gib/Documents/Code',
|
|
||||||
runner = 'docker',
|
|
||||||
llm = {
|
|
||||||
provider = 'openai',
|
|
||||||
endpoint = 'https://api.openai.com/v1',
|
|
||||||
api_key = 'OPENAI_API_KEY',
|
|
||||||
model = 'gpt-5',
|
|
||||||
extra = {
|
|
||||||
temperature = 1,
|
|
||||||
max_tokens = 512,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
embed = {
|
|
||||||
provider = 'openai',
|
|
||||||
endpoint = 'https://api.openai.com/v1',
|
|
||||||
api_key = 'OPENAI_API_KEY',
|
|
||||||
model = 'text-embedding-3-large',
|
|
||||||
extra = nil,
|
|
||||||
},
|
|
||||||
docker_extra_args = '',
|
|
||||||
},
|
|
||||||
web_search_engine = {
|
|
||||||
provider = 'searxng',
|
|
||||||
proxy = nil,
|
|
||||||
},
|
|
||||||
system_prompt = function()
|
|
||||||
local hub = require('mcphub').get_hub_instance()
|
|
||||||
return hub and hub:get_active_servers_prompt() or ''
|
|
||||||
end,
|
|
||||||
custom_tools = function()
|
|
||||||
return {
|
|
||||||
require('mcphub.extensions.avante').mcp_tool(),
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
behaviour = {
|
|
||||||
auto_suggestions = false,
|
|
||||||
auto_set_highlight_group = true,
|
|
||||||
auto_set_keymaps = true,
|
|
||||||
auto_apply_diff_after_generation = false,
|
|
||||||
support_paste_from_clipboard = false,
|
|
||||||
minimize_diff = true,
|
|
||||||
enable_token_counting = true,
|
|
||||||
auto_approve_tool_permissions = true,
|
|
||||||
},
|
|
||||||
mappings = {
|
|
||||||
--- @class AvanteConflictMappings
|
|
||||||
diff = {
|
|
||||||
ours = 'co',
|
|
||||||
theirs = 'ct',
|
|
||||||
all_theirs = 'ca',
|
|
||||||
both = 'cb',
|
|
||||||
cursor = 'cc',
|
|
||||||
next = ']x',
|
|
||||||
prev = '[x',
|
|
||||||
},
|
|
||||||
suggestion = {
|
|
||||||
accept = '<M-l>',
|
|
||||||
next = '<M-]>',
|
|
||||||
prev = '<M-[>',
|
|
||||||
dismiss = '<C-]>',
|
|
||||||
},
|
|
||||||
jump = {
|
|
||||||
next = ']]',
|
|
||||||
prev = '[[',
|
|
||||||
},
|
|
||||||
submit = {
|
|
||||||
normal = '<CR>',
|
|
||||||
insert = '<C-s>',
|
|
||||||
},
|
|
||||||
cancel = {
|
|
||||||
normal = { '<C-c>', '<Esc>', 'q' },
|
|
||||||
insert = { '<C-c>' },
|
|
||||||
},
|
|
||||||
sidebar = {
|
|
||||||
apply_all = 'A',
|
|
||||||
apply_cursor = 'a',
|
|
||||||
retry_user_request = 'r',
|
|
||||||
edit_user_request = 'e',
|
|
||||||
switch_windows = '<Tab>',
|
|
||||||
reverse_switch_windows = '<S-Tab>',
|
|
||||||
remove_file = 'd',
|
|
||||||
add_file = '@',
|
|
||||||
close = { '<Esc>', 'q' },
|
|
||||||
close_from_input = nil,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hints = { enabled = true },
|
|
||||||
windows = {
|
|
||||||
---@type 'right' | 'left' | 'top' | 'bottom'
|
|
||||||
position = 'right',
|
|
||||||
wrap = true,
|
|
||||||
width = 35,
|
|
||||||
sidebar_header = {
|
|
||||||
enabled = true,
|
|
||||||
align = 'center',
|
|
||||||
rounded = true,
|
|
||||||
},
|
|
||||||
input = {
|
|
||||||
prefix = '> ',
|
|
||||||
height = 8,
|
|
||||||
},
|
|
||||||
edit = {
|
|
||||||
border = 'rounded',
|
|
||||||
start_insert = true,
|
|
||||||
},
|
|
||||||
ask = {
|
|
||||||
floating = false,
|
|
||||||
start_insert = true,
|
|
||||||
border = 'rounded',
|
|
||||||
---@type 'ours' | 'theirs'
|
|
||||||
focus_on_apply = 'ours',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
highlights = {
|
|
||||||
diff = {
|
|
||||||
current = 'DiffText',
|
|
||||||
incoming = 'DiffAdd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
--- @class AvanteConflictUserConfig
|
|
||||||
diff = {
|
|
||||||
autojump = true,
|
|
||||||
---@type string | fun(): any
|
|
||||||
list_opener = 'copen',
|
|
||||||
override_timeoutlen = 500,
|
|
||||||
},
|
|
||||||
suggestion = {
|
|
||||||
debounce = 1000,
|
|
||||||
throttle = 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dependencies = {
|
|
||||||
{ 'nvim-lua/plenary.nvim' },
|
|
||||||
{ 'MunifTanjim/nui.nvim' },
|
|
||||||
{ 'hrsh7th/nvim-cmp' },
|
|
||||||
{ 'nvim-treesitter/nvim-treesitter' },
|
|
||||||
{ 'stevearc/dressing.nvim' },
|
|
||||||
{ 'nvim-tree/nvim-web-devicons' },
|
|
||||||
{ 'folke/snacks.nvim' },
|
|
||||||
{
|
|
||||||
'ravitemer/mcphub.nvim',
|
|
||||||
build = "npm install -g mcp-hub@latest",
|
|
||||||
config = function()
|
|
||||||
require'mcphub'.setup({
|
|
||||||
config = vim.fn.expand('/home/gib/.config/mcphub/servers.json'),
|
|
||||||
port = 37373,
|
|
||||||
shutdown_delay = 60 * 10 * 000,
|
|
||||||
use_bundled_binary = false,
|
|
||||||
mcp_request_timeout = 60000,
|
|
||||||
auto_approve = true,
|
|
||||||
auto_toggle_mcp_servers = true,
|
|
||||||
extensions = {
|
|
||||||
avante = {
|
|
||||||
make_slash_commands = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
native_servers = {},
|
|
||||||
ui = {
|
|
||||||
window = {
|
|
||||||
width = 0.8,
|
|
||||||
height = 0.8,
|
|
||||||
align = 'center',
|
|
||||||
relative = 'editor',
|
|
||||||
zindex = 50,
|
|
||||||
border = 'rounded',
|
|
||||||
},
|
|
||||||
wo = {
|
|
||||||
winhl = 'Normal:MCPHubNormal,FloatBorder:MCPHubBorder',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
log = {
|
|
||||||
level = vim.log.levels.WARN,
|
|
||||||
to_file = false,
|
|
||||||
file_path = nil,
|
|
||||||
prefix = 'MCPHub',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
},
|
|
||||||
{
|
|
||||||
-- support for image pasting
|
|
||||||
'HakonHarnes/img-clip.nvim',
|
|
||||||
event = 'VeryLazy',
|
|
||||||
opts = {
|
|
||||||
-- recommended settings
|
|
||||||
default = {
|
|
||||||
embed_image_as_base64 = false,
|
|
||||||
prompt_for_file_name = false,
|
|
||||||
drag_and_drop = {
|
|
||||||
insert_mode = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'MeanderingProgrammer/render-markdown.nvim',
|
|
||||||
opts = {
|
|
||||||
file_types = { 'markdown', 'Avante' },
|
|
||||||
},
|
|
||||||
ft = { 'markdown', 'Avante' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
return {
|
|
||||||
'romgrk/barbar.nvim',
|
|
||||||
dependencies = {
|
|
||||||
'lewis6991/gitsigns.nvim',
|
|
||||||
'nvim-tree/nvim-web-devicons',
|
|
||||||
},
|
|
||||||
init = function() vim.g.barbar_auto_setup = false end,
|
|
||||||
opts = {
|
|
||||||
animation = true,
|
|
||||||
insert_at_start = true,
|
|
||||||
},
|
|
||||||
keys = {
|
|
||||||
{ "<C-h>", ":BufferPrevious<CR>", mode = "n", silent = true, desc = "Go to previous tab" },
|
|
||||||
{ "<C-l>", ":BufferNext<CR>", mode = "n", silent = true, desc = "Go to next tab" },
|
|
||||||
{ "<C-j>", ":BufferMovePrevious<CR>", mode = "n", silent = true, desc = "Move tab to previous position" },
|
|
||||||
{ "<C-k>", ":BufferMoveNext<CR>", mode = "n", silent = true, desc = "Move tab to next position" },
|
|
||||||
{ "<C-q>", ":BufferClose<CR>", mode = "n", silent = true, desc = "Close current tab" },
|
|
||||||
{ "<C-a>", ":BufferCloseAllButCurrent<CR>", mode = "n", silent = true, desc = "Close all tabs except current" },
|
|
||||||
},
|
|
||||||
version = '^1.0.0', -- optional: only update when a new 1.x version is released
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
return {
|
|
||||||
'laytan/cloak.nvim',
|
|
||||||
config = function()
|
|
||||||
require'cloak'.setup({
|
|
||||||
enabled = false,
|
|
||||||
cloak_character = "•",
|
|
||||||
highlight_group = "Comment",
|
|
||||||
patterns = {
|
|
||||||
{
|
|
||||||
file_pattern = {
|
|
||||||
'.env*',
|
|
||||||
'wrangler.toml',
|
|
||||||
'.dev.vars',
|
|
||||||
},
|
|
||||||
cloak_pattern = "=.+"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
return {
|
|
||||||
'folke/tokyonight.nvim',
|
|
||||||
lazy = false,
|
|
||||||
priority = 1000,
|
|
||||||
opts = {
|
|
||||||
style = 'moon',
|
|
||||||
transparent = true,
|
|
||||||
on_colors = function(colors)
|
|
||||||
colors.comment = '#a0a7c5'
|
|
||||||
colors.fg_gutter = '#787f93'
|
|
||||||
colors.terminal_black = '#828bb8'
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
config = function(_,opts)
|
|
||||||
require'tokyonight'.setup(opts)
|
|
||||||
vim.cmd.colorscheme('tokyonight-moon')
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
-- 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 = 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)
|
||||||
|
-- Inline code in markdown is invisible at both lightnesses without
|
||||||
|
-- an explicit background, because the theme leaves it unset.
|
||||||
|
highlights["@markup.raw.markdown_inline"] = {
|
||||||
|
bg = light and colors.bg_highlight or colors.terminal_black,
|
||||||
|
fg = colors.fg,
|
||||||
|
}
|
||||||
|
highlights["RenderMarkdownCodeInline"] = {
|
||||||
|
bg = light and colors.bg_highlight or colors.terminal_black,
|
||||||
|
fg = colors.fg,
|
||||||
|
}
|
||||||
|
|
||||||
|
-- LspReference* readability: DiagnosticUnnecessary dims fg for unused
|
||||||
|
-- imports, making text nearly invisible against LspReferenceText's
|
||||||
|
-- background.
|
||||||
|
local reference = {
|
||||||
|
bg = light and colors.bg_visual or colors.fg_gutter,
|
||||||
|
fg = colors.fg,
|
||||||
|
}
|
||||||
|
highlights["LspReferenceText"] = reference
|
||||||
|
highlights["LspReferenceRead"] = reference
|
||||||
|
highlights["LspReferenceWrite"] = reference
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"LazyVim/LazyVim",
|
||||||
|
opts = {
|
||||||
|
colorscheme = "tokyonight",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
return {
|
|
||||||
'3rd/image.nvim',
|
|
||||||
dependencies = {
|
|
||||||
'nvim-treesitter/nvim-treesitter',
|
|
||||||
},
|
|
||||||
config = function()
|
|
||||||
require'image'.setup({
|
|
||||||
backend = "kitty",
|
|
||||||
integrations = {
|
|
||||||
markdown = {
|
|
||||||
enabled = true,
|
|
||||||
clear_in_insert_mode = false,
|
|
||||||
download_remote_images = true,
|
|
||||||
only_render_image_at_cursor = false,
|
|
||||||
filetypes = { "markdown", "vimwiki" },
|
|
||||||
},
|
|
||||||
neorg = {
|
|
||||||
enabled = true,
|
|
||||||
clear_in_insert_mode = false,
|
|
||||||
download_remote_images = true,
|
|
||||||
only_render_image_at_cursor = false,
|
|
||||||
filetypes = { "norg" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
max_width = nil,
|
|
||||||
max_height = nil,
|
|
||||||
max_width_window_percentage = nil,
|
|
||||||
max_height_window_percentage = 50,
|
|
||||||
kitty_method = "normal",
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
@@ -1,371 +1,36 @@
|
|||||||
return {
|
return {
|
||||||
'mason-org/mason-lspconfig.nvim',
|
|
||||||
dependencies = {
|
|
||||||
{ 'neovim/nvim-lspconfig', },
|
|
||||||
{
|
{
|
||||||
'mason-org/mason.nvim',
|
"neovim/nvim-lspconfig",
|
||||||
opts = {
|
opts = {
|
||||||
ui = {
|
servers = {
|
||||||
icons = {
|
-- CSS / HTML / Tailwind
|
||||||
package_installed = "✓",
|
cssls = {},
|
||||||
package_pending = "➜",
|
html = {},
|
||||||
package_uninstalled = "✗"
|
tailwindcss = {},
|
||||||
}
|
-- Shell / Config
|
||||||
}
|
bashls = {},
|
||||||
|
yamlls = {},
|
||||||
|
-- Docker
|
||||||
|
dockerls = {},
|
||||||
|
docker_compose_language_service = {},
|
||||||
|
-- Systems languages
|
||||||
|
gopls = {},
|
||||||
|
rust_analyzer = {},
|
||||||
|
-- Web frameworks
|
||||||
|
svelte = {},
|
||||||
|
graphql = {},
|
||||||
|
-- Database / ORM
|
||||||
|
prismals = {},
|
||||||
|
sqlls = {},
|
||||||
|
-- PHP
|
||||||
|
intelephense = {},
|
||||||
|
-- Python
|
||||||
|
pyright = {},
|
||||||
|
-- Disabled: ltex grammar/spell LSP runs a ~1GB JVM and auto-attaches to
|
||||||
|
-- markdown/text/html/gitcommit/etc. via mason-lspconfig automatic_enable.
|
||||||
|
ltex = { enabled = false },
|
||||||
|
ltex_plus = { enabled = false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'hrsh7th/nvim-cmp',
|
|
||||||
event = { 'InsertEnter', 'CmdlineEnter' },
|
|
||||||
config = function()
|
|
||||||
local cmp = require'cmp'
|
|
||||||
local lspkind = require'lspkind'
|
|
||||||
local cmp_select = { behavior = cmp.SelectBehavior.Select }
|
|
||||||
local luasnip = require'luasnip'
|
|
||||||
cmp.setup({
|
|
||||||
snippet = {
|
|
||||||
expand = function(args)
|
|
||||||
luasnip.lsp_expand(args.body)
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
window = {
|
|
||||||
completion = cmp.config.window.bordered(),
|
|
||||||
documentation = cmp.config.window.bordered(),
|
|
||||||
},
|
|
||||||
sources = cmp.config.sources({
|
|
||||||
{ name = 'nvim_lsp' },
|
|
||||||
{ name = 'luasnip' },
|
|
||||||
}, {
|
|
||||||
{ name = 'buffer' },
|
|
||||||
{ name = 'luasnip_choice' },
|
|
||||||
{ name = 'copilot' },
|
|
||||||
{ name = 'supermaven' },
|
|
||||||
{ name = 'nvim_lsp_document_symbol' },
|
|
||||||
{ name = 'nvim_lsp_signature_help' },
|
|
||||||
{ name = 'nvim_lua' },
|
|
||||||
{ name = 'nerdfont' },
|
|
||||||
{ name = 'emoji' },
|
|
||||||
{ name = 'npm' },
|
|
||||||
{ name = 'cmp-tw2css' },
|
|
||||||
{ name = 'dotenv' },
|
|
||||||
{ name = 'render-markdown' },
|
|
||||||
}),
|
|
||||||
formatting = {
|
|
||||||
format = lspkind.cmp_format({
|
|
||||||
mode = 'symbol',
|
|
||||||
maxwidth = {
|
|
||||||
menu = 50,
|
|
||||||
abbr = 50,
|
|
||||||
},
|
|
||||||
symbol_map = {
|
|
||||||
Supermaven = '',
|
|
||||||
},
|
|
||||||
ellipsis_char = '...',
|
|
||||||
show_labelDetails = true,
|
|
||||||
before = function(entry, vim_item)
|
|
||||||
return vim_item
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
mapping = cmp.mapping.preset.insert({
|
|
||||||
['<C-p>'] = cmp.mapping.select_prev_item(cmp_select),
|
|
||||||
['<C-n>'] = cmp.mapping.select_next_item(cmp_select),
|
|
||||||
['<C-y>'] = cmp.mapping.confirm({ select = true }),
|
|
||||||
['<C-b>'] = cmp.mapping.scroll_docs(-4),
|
|
||||||
['<C-f>'] = cmp.mapping.scroll_docs(4),
|
|
||||||
['<C-e>'] = cmp.mapping.abort(),
|
|
||||||
}),
|
|
||||||
--mapping = cmp.mapping.preset.insert({
|
|
||||||
--['<Tab>'] = cmp.mapping(function(fallback)
|
|
||||||
--if cmp.visible() then
|
|
||||||
--if cmp.get_selected_entry() then
|
|
||||||
--cmp.confirm({ select = false })
|
|
||||||
--else
|
|
||||||
--cmp.select_next_item()
|
|
||||||
--end
|
|
||||||
--else
|
|
||||||
--fallback()
|
|
||||||
--end
|
|
||||||
--end, { 'i', 's' }),
|
|
||||||
|
|
||||||
--['<C-y>'] = cmp.mapping.confirm({ select = true }),
|
|
||||||
--['<C-Space>'] = cmp.mapping.complete(),
|
|
||||||
--['<C-b>'] = cmp.mapping.scroll_docs(-4),
|
|
||||||
--['<C-f>'] = cmp.mapping.scroll_docs(4),
|
|
||||||
--['<C-e>'] = cmp.mapping.abort(),
|
|
||||||
--['<C-p>'] = cmp.mapping.select_prev_item(cmp_select),
|
|
||||||
--['<C-n>'] = cmp.mapping.select_next_item(cmp_select),
|
|
||||||
--['<C-d>'] = cmp.mapping.scroll_docs(4),
|
|
||||||
--['<C-u>'] = cmp.mapping.scroll_docs(-4),
|
|
||||||
--}),
|
|
||||||
preselect = cmp.PreselectMode.Item,
|
|
||||||
completion = {
|
|
||||||
completeopt = 'menu,menuone,noinsert',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
cmp.setup.filetype('gitcommit', {
|
|
||||||
sources = cmp.config.sources({
|
|
||||||
{ name = 'git' },
|
|
||||||
}, {
|
|
||||||
{ name = 'buffer' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
cmp.setup.cmdline({ '/', '?' }, {
|
|
||||||
mapping = cmp.mapping.preset.cmdline(),
|
|
||||||
sources = {
|
|
||||||
{ name = 'buffer' },
|
|
||||||
}
|
|
||||||
})
|
|
||||||
cmp.setup.cmdline(':', {
|
|
||||||
mapping = cmp.mapping.preset.cmdline(),
|
|
||||||
sources = cmp.config.sources({
|
|
||||||
{ name = 'path' },
|
|
||||||
}, {
|
|
||||||
{ name = 'cmdline' },
|
|
||||||
}),
|
|
||||||
matching = { disallow_symbol_nonprefix_matching = false }
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
{ 'hrsh7th/cmp-nvim-lsp' },
|
|
||||||
{ 'nvim-lua/plenary.nvim' },
|
|
||||||
{ 'hrsh7th/cmp-nvim-lsp-document-symbol' },
|
|
||||||
{ 'hrsh7th/cmp-nvim-lua' },
|
|
||||||
{ 'hrsh7th/cmp-nvim-lsp-signature-help' },
|
|
||||||
{ 'hrsh7th/cmp-path' },
|
|
||||||
{ 'hrsh7th/cmp-copilot' },
|
|
||||||
{ 'hrsh7th/cmp-buffer' },
|
|
||||||
{ 'hrsh7th/cmp-emoji' },
|
|
||||||
{ 'chrisgrieser/cmp-nerdfont' },
|
|
||||||
{ 'jcha0713/cmp-tw2css' },
|
|
||||||
{ 'SergioRibera/cmp-dotenv' },
|
|
||||||
{ 'saadparwaiz1/cmp_luasnip' },
|
|
||||||
{ 'onsails/lspkind.nvim' },
|
|
||||||
{ 'rafamadriz/friendly-snippets' },
|
|
||||||
{
|
|
||||||
'L3MON4D3/cmp-luasnip-choice',
|
|
||||||
config = function()
|
|
||||||
require'cmp_luasnip_choice'.setup()
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'petertriho/cmp-git',
|
|
||||||
config = function()
|
|
||||||
require'cmp_git'.setup({})
|
|
||||||
end
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'j-hui/fidget.nvim',
|
|
||||||
config = function()
|
|
||||||
require'fidget'.setup({})
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'David-Kunz/cmp-npm',
|
|
||||||
config = function()
|
|
||||||
require'cmp-npm'.setup({})
|
|
||||||
end
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'stevearc/conform.nvim',
|
|
||||||
config = function()
|
|
||||||
require'conform'.setup({
|
|
||||||
formatters_by_ft = {
|
|
||||||
cs = { 'csharpier', 'lsp' },
|
|
||||||
javascript = { 'prettierd', 'prettier' },
|
|
||||||
javascriptreact = { 'prettierd', 'prettier' },
|
|
||||||
typescript = { 'prettierd', 'prettier' },
|
|
||||||
typescriptreact = { 'prettierd', 'prettier' },
|
|
||||||
json = { 'prettierd', 'prettier' },
|
|
||||||
html = { 'prettierd', 'prettier' },
|
|
||||||
css = { 'prettierd', 'prettier' },
|
|
||||||
lua = { 'stylua' },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
vim.api.nvim_create_user_command(
|
|
||||||
'Format',
|
|
||||||
function(args)
|
|
||||||
local range
|
|
||||||
if args.count ~= -1 then
|
|
||||||
local start_line = vim.fn.line("'<")
|
|
||||||
local end_line = vim.fn.line("'>")
|
|
||||||
range = { start = { start_line, 0 }, ["end"] = { end_line, 0 } }
|
|
||||||
end
|
|
||||||
require'conform'.format({
|
|
||||||
async = true,
|
|
||||||
lsp_fallback = true,
|
|
||||||
range = range,
|
|
||||||
})
|
|
||||||
end, { range = true, desc = "Format current buffer or range" })
|
|
||||||
end,
|
|
||||||
keys = {
|
|
||||||
{
|
|
||||||
'<leader>fk',
|
|
||||||
':Format<CR>',
|
|
||||||
mode = {'n', 'v'},
|
|
||||||
silent = true,
|
|
||||||
desc = "Format with conform"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'L3MON4D3/LuaSnip',
|
|
||||||
version = 'v2.*',
|
|
||||||
build = 'make install_jsregexp'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'github/copilot.vim',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'supermaven-inc/supermaven-nvim',
|
|
||||||
config = function()
|
|
||||||
require'supermaven-nvim'.setup({
|
|
||||||
keymaps = {
|
|
||||||
accept_suggestion = '<Tab>',
|
|
||||||
clear_suggestion = '<C-]>',
|
|
||||||
accept_word = '<C-.>',
|
|
||||||
},
|
|
||||||
disable_inline_completion = true,
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'windwp/nvim-autopairs',
|
|
||||||
event = 'InsertEnter',
|
|
||||||
config = true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'windwp/nvim-ts-autotag',
|
|
||||||
config = function()
|
|
||||||
require'nvim-ts-autotag'.setup({})
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
--{
|
|
||||||
--"garyhurtz/cmp_kitty",
|
|
||||||
--dependencies = {
|
|
||||||
--{ "hrsh7th/nvim-cmp" },
|
|
||||||
--},
|
|
||||||
--init = function()
|
|
||||||
--require('cmp_kitty'):setup()
|
|
||||||
--end
|
|
||||||
--},
|
|
||||||
{ 'hrsh7th/cmp-cmdline' },
|
|
||||||
},
|
|
||||||
config = function()
|
|
||||||
vim.diagnostic.config({
|
|
||||||
float = {
|
|
||||||
focusable = false,
|
|
||||||
style = 'minimal',
|
|
||||||
border = 'rounded',
|
|
||||||
source = true,
|
|
||||||
header = '',
|
|
||||||
prefix = '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
require'mason'.setup({})
|
|
||||||
local cmp = require'cmp'
|
|
||||||
local cmp_lsp = require'cmp_nvim_lsp'
|
|
||||||
local lspconfig = require'lspconfig'
|
|
||||||
local capabilities = vim.tbl_deep_extend(
|
|
||||||
'force',
|
|
||||||
{},
|
|
||||||
vim.lsp.protocol.make_client_capabilities(),
|
|
||||||
cmp_lsp.default_capabilities()
|
|
||||||
)
|
|
||||||
local cmp_autopairs = require'nvim-autopairs.completion.cmp'
|
|
||||||
cmp.event:on('confirm_done', cmp_autopairs.on_confirm_done())
|
|
||||||
require 'mason-lspconfig'.setup({
|
|
||||||
ensure_installed = {
|
|
||||||
'angularls',
|
|
||||||
'bacon_ls',
|
|
||||||
'bashls',
|
|
||||||
'clangd',
|
|
||||||
'cssls',
|
|
||||||
'css_variables',
|
|
||||||
'cssmodules_ls',
|
|
||||||
'docker_compose_language_service',
|
|
||||||
'docker_language_server',
|
|
||||||
'dockerls',
|
|
||||||
'eslint',
|
|
||||||
'gopls',
|
|
||||||
'graphql',
|
|
||||||
'html',
|
|
||||||
'htmx',
|
|
||||||
'intelephense',
|
|
||||||
'jsonls',
|
|
||||||
'kotlin_lsp',
|
|
||||||
'ltex',
|
|
||||||
'ltex_plus',
|
|
||||||
'lua_ls',
|
|
||||||
'markdown_oxide',
|
|
||||||
'phpactor',
|
|
||||||
'postgres_lsp',
|
|
||||||
'prismals',
|
|
||||||
'pylyzer',
|
|
||||||
'pyright',
|
|
||||||
'remark_ls',
|
|
||||||
'rust_analyzer',
|
|
||||||
'sqlls',
|
|
||||||
'svelte',
|
|
||||||
'systemd_ls',
|
|
||||||
'tailwindcss',
|
|
||||||
'tinymist',
|
|
||||||
'ts_ls',
|
|
||||||
'yamlls',
|
|
||||||
},
|
|
||||||
handlers = {
|
|
||||||
function(server_name)
|
|
||||||
lspconfig[server_name].setup({
|
|
||||||
capabilities = capabilities
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
zls = function()
|
|
||||||
lspconfig.zls.setup({
|
|
||||||
root_dir = lspconfig.util.root_pattern(".git", "build.zig", "zls.json"),
|
|
||||||
settings = {
|
|
||||||
zls = {
|
|
||||||
enable_inlay_hints = true,
|
|
||||||
enable_snippets = true,
|
|
||||||
warn_style = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
vim.g.zig_fmt_parse_errors = 0
|
|
||||||
vim.g.zig_fmt_autosave = 0
|
|
||||||
end,
|
|
||||||
["lua_ls"] = function()
|
|
||||||
lspconfig.lua_ls.setup({
|
|
||||||
capabilities = capabilities,
|
|
||||||
settings = {
|
|
||||||
Lua = {
|
|
||||||
runtime = {
|
|
||||||
version = 'LuaJIT',
|
|
||||||
},
|
|
||||||
diagnostics = {
|
|
||||||
globals = { 'vim' },
|
|
||||||
},
|
|
||||||
workspace = {
|
|
||||||
library = vim.api.nvim_get_runtime_file("", true),
|
|
||||||
checkThirdParty = false,
|
|
||||||
-- These are the key settings to prevent the infinite processing
|
|
||||||
maxPreload = 100000,
|
|
||||||
preloadFileSize = 10000,
|
|
||||||
},
|
|
||||||
telemetry = {
|
|
||||||
enable = false,
|
|
||||||
},
|
|
||||||
format = {
|
|
||||||
enable = true,
|
|
||||||
defaultConfig = {
|
|
||||||
indent_style = "space",
|
|
||||||
indent_size = "2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user