Compare commits

...

29 Commits

Author SHA1 Message Date
funman300 6e26cc62aa niri: drop Steam Big Picture fullscreen rule
Big Picture reuses the existing client window (title change only), so the
open-fullscreen rule never fires on toggle; unused, removing it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 10:04:19 -07:00
funman300 400d81ff73 niri: fullscreen Steam Big Picture mode
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 09:59:27 -07:00
funman300 dc810ed2a6 niri: float and center Steam client and child windows
Add window rules for the Steam client (app-id "steam"): all client
windows float and center, the main library window opens large (75x85%),
and notification toasts sit unobtrusively in the bottom-right without
border or focus-ring. Launched games (steam_app_) are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 09:59:01 -07:00
funman300 88f2131bdd docs: document hypridle, correct stale swww->awww references
Adds the hypridle entry to ARCHITECTURE, and folds in the pending
swww->awww wallpaper-tool rename in ARCHITECTURE + README (the repo spawns
awww-daemon, not swww).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:04:23 -07:00
funman300 95d4bf612b feat(waybar): remove system tray module
Drops 'tray' from modules-right and its config block. Network and
bluetooth status remain as their own dedicated modules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:04:23 -07:00
funman300 961733e127 feat: replace swayidle with hypridle for idle management
hypridle pairs with hyprlock, honors idle inhibitors (video/calls no
longer blank or lock the screen), and is config-file driven. Same staging
as before: blank monitors @5m, lock @10m, suspend-then-hibernate @30m.
Locks via 'loginctl lock-session' so the before-sleep lock is deduplicated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:04:23 -07:00
funman300 1e3adbc842 feat: centralize theme palette + add waybar bluetooth module
Theme centralization:
- theme/colors.json is now the single source of truth for the palette.
- New theme/generate-theme.py renders, at install time, the three derived
  artifacts into ~/.config: colors.css (waybar/wofi @import), mako config,
  and hyprlock-colors.conf (hyprlang $tn_* variables).
- theme/colors.css removed from the repo (now generated); install.sh calls
  the generator instead of the old inline mako snippet.
- hyprlock.conf sources the generated color file and uses $tn_* variables
  instead of hardcoded rgba() values.
- Generator covered by theme/test_generate_theme.py.

Bluetooth:
- Added waybar built-in bluetooth module (config.jsonc + style.css), with
  bluez/bluez-utils in packages.txt.

Includes design spec and implementation plan under docs/superpowers/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:45:01 -07:00
funman300 5305bb78c6 scripts: also wire fingerprint into greetd login
Adds /etc/pam.d/greetd to the TARGETS array so re-running
enable-fingerprint.sh extends fprintd auth to the regreet login
screen. Cold-boot timing and silent-touch UX caveats documented
in the spec are accepted as-is — fingerprint failure falls back to
password, no risk of lockout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:45:05 -07:00
funman300 2ca47c02a4 hyprlock: enable fingerprint auth path
hyprlock doesn't poll fprintd by default — pam_authenticate() is
only invoked on password submission. Adding the auth.fingerprint
block tells hyprlock to launch fprintd polling in parallel with the
password input, so touching the reader unlocks immediately without
typing first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:40:32 -07:00
funman300 28f6160b02 hyprlock: pin label font + flesh out ARCHITECTURE description
Final-review caveats:
- ARCHITECTURE.md hyprlock section was a generic one-liner inherited
  from gtklock; now mentions fprintd integration + GPU + blur.
- hyprlock label blocks didn't set font_family; clock/date now
  pinned to JetBrains Mono Nerd Font (same as rest of desktop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:28:04 -07:00
funman300 577c169bae docs: flip lockscreen references to hyprlock
Replace all gtklock mentions in README, ARCHITECTURE, CLAUDE.md with
hyprlock, and drop the stale gtklock consumer note from theme/colors.css
header comment (hyprlock uses Hyprlang, not CSS imports).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:19:49 -07:00
funman300 ee93bcb7e8 gtklock: delete tracked config (replaced by hyprlock)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:18:29 -07:00
funman300 2fcfc62cdd scripts: enable-fingerprint targets hyprlock PAM file
Replace /etc/pam.d/gtklock with /etc/pam.d/hyprlock in the TARGETS
array now that hyprlock is the session locker. Update the header
comment to match.
2026-05-25 11:17:10 -07:00
funman300 c3bcf19e97 niri: switch lock + idle hooks to hyprlock
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:15:14 -07:00
funman300 bc3333a82a hyprlock: add config + swap install.sh away from gtklock
Adds hyprlock/hyprlock.conf with Tomorrow Night colors, replaces gtklock
with hyprlock in packages.txt, and updates install.sh to symlink
hyprlock.conf into ~/.config/hypr/ instead of the old gtklock symlinks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:12:49 -07:00
funman300 467be569fd docs: implementation plan for hyprlock migration
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:10:50 -07:00
funman300 d7ab4baf7e docs: spec for hyprlock migration (replaces gtklock)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:08:38 -07:00
funman300 30a300014a docs: document one-time fingerprint setup 2026-05-25 10:45:24 -07:00
funman300 b34d4269af scripts: add enable-fingerprint.sh (one-time PAM bootstrap)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:33:00 -07:00
funman300 17272611ea packages: add fprintd for fingerprint reader support 2026-05-25 10:31:02 -07:00
funman300 ede1399853 docs: implementation plan for fingerprint reader
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:30:08 -07:00
funman300 2cd28edf52 docs: spec for Framework 13 fingerprint reader setup
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:28:50 -07:00
funman300 622b0ef8b8 docs: document one-time NetworkManager activation 2026-05-13 14:53:12 -07:00
funman300 d5ad517673 niri: re-add nm-applet --indicator spawn-at-startup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:51:20 -07:00
funman300 bfb30d0a4f packages: also track networkmanager
Discovered while re-adding network-manager-applet that the
networkmanager daemon was already installed locally but never tracked
in packages.txt. Track it now so a fresh install gets both the daemon
and the applet, making the README's NetworkManager activation step
work end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:49:46 -07:00
funman300 33c11e328f packages: re-add network-manager-applet (now functional)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:49:03 -07:00
funman300 9362a5ee1d NetworkManager: track wifi-backend.conf and deploy via install.sh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:46:50 -07:00
funman300 b57d134c2b docs: implementation plan for NetworkManager activation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:44:57 -07:00
funman300 101accc26b docs: spec for NetworkManager activation with iwd backend
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:43:10 -07:00
26 changed files with 2563 additions and 127 deletions
+1
View File
@@ -1,2 +1,3 @@
nohup.out
mako/config
__pycache__/
+14 -5
View File
@@ -123,9 +123,18 @@ This system is a **Wayland-first desktop environment** built around the **Niri c
### Lock Screen
#### gtklock
#### hyprlock
* Simple Wayland-native lockscreen
* GPU-accelerated Wayland lockscreen
* Native fprintd polling — touch the reader to unlock without typing
* Blurred-screenshot background, Tomorrow Night palette
#### hypridle
* Idle daemon paired with hyprlock (replaces swayidle)
* Honors idle inhibitors, so video/calls no longer blank or lock the screen
* Stages: blank monitors @ 5 min → lock @ 10 min → suspend-then-hibernate @ 30 min
* Locks via `loginctl lock-session` so logind-driven locks (e.g. before sleep) stay deduplicated
---
@@ -133,7 +142,7 @@ This system is a **Wayland-first desktop environment** built around the **Niri c
| Function | Tool |
| ----------- | -------------------- |
| Wallpaper | swww |
| Wallpaper | awww |
| Screenshots | grim + slurp + satty |
| Clipboard | cliphist |
@@ -150,7 +159,7 @@ This system is a **Wayland-first desktop environment** built around the **Niri c
* waybar
* mako
* swww
* awww (awww-daemon; restores last wallpaper from cache)
* cliphist (if enabled)
---
@@ -190,7 +199,7 @@ System → Waybar module → Display
├── starship.toml
├── mako/
├── wofi/
└── gtklock/
└── hyprlock/
```
All configs are sourced from the repo via:
+1 -1
View File
@@ -76,7 +76,7 @@ dotfiles/
│ ├── starship/
│ ├── wofi/
│ ├── mako/
│ ├── gtklock/
│ ├── hyprlock/
│ └── ...
├── install.sh
├── packages.txt
+2
View File
@@ -0,0 +1,2 @@
[device]
wifi.backend=iwd
+26 -2
View File
@@ -10,9 +10,9 @@ A personal Wayland desktop configuration centered around the **Niri** tiling com
| Shell | fish + starship |
| Launcher | wofi |
| Notifications | mako |
| Lockscreen | gtklock |
| Lockscreen | hyprlock |
| Login/greeter | greetd + regreet |
| Wallpaper | swww |
| Wallpaper | awww |
| Screenshots | grim + slurp + satty |
| Clipboard | cliphist |
| Theme | Tomorrow Night (GTK: Materia-dark, icons: Papirus) |
@@ -36,3 +36,27 @@ sudo bash scripts/enable-hibernation.sh
```
After reboot, `systemctl hibernate` will work, and lid close / 30-min idle will suspend-then-hibernate per the tracked logind config.
### One-time NetworkManager activation
After `install.sh` deploys the NetworkManager config, swap from `systemd-networkd` to `NetworkManager` (one-time, persistent across reboots):
```bash
sudo systemctl disable --now systemd-networkd
sudo systemctl enable --now NetworkManager
```
Wifi will drop for a few seconds and reconnect via iwd's stored profiles. If reconnection fails, re-enter the wifi password via `nm-applet` (tray icon) or `networkmanager_dmenu` (waybar network widget click).
### One-time fingerprint setup
After `install.sh` installs `fprintd`, enroll a finger and wire it into the sudo + lockscreen auth stacks:
```bash
fprintd-enroll # interactive; touch the reader as prompted
sudo bash scripts/enable-fingerprint.sh # idempotent — safe to re-run
```
Test with `sudo true` (should prompt for fingerprint) or `Mod+Shift+E` to lock + touch the reader.
To enroll additional fingers: `fprintd-enroll -f left-thumb` (or `right-thumb`, `left-index`, etc.).
@@ -0,0 +1,293 @@
# NetworkManager Activation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Activate NetworkManager (with iwd as wifi backend) so the existing nm-applet, networkmanager_dmenu, and waybar `network` widget actually work.
**Architecture:** One tracked drop-in (`NetworkManager/conf.d/wifi-backend.conf`) deployed by `install.sh`. Re-add the package and the niri spawn-at-startup line that were removed when nm-applet was useless. Document the one-time `systemctl` swap in README. iwd keeps running as wifi backend; systemd-networkd is disabled by the user via the documented commands.
**Tech Stack:** NetworkManager 1.56, iwd, niri, systemd, install(1).
**Spec:** `docs/superpowers/specs/2026-05-13-networkmanager-activation-design.md`
**Verification model:** Bash syntax checks + `niri validate` for static correctness. Functional checks (NetworkManager active, wifi connected, tray icon present) require the live session — controller verifies after the README's manual systemctl commands run.
---
## File Structure
| File | Role |
|---|---|
| `NetworkManager/conf.d/wifi-backend.conf` | New tracked dotfile. One-line `[device]` block telling NetworkManager to use iwd as wifi backend. |
| `install.sh` | Modified: append one `sudo install -Dm644` line to deploy the drop-in. |
| `packages.txt` | Modified: re-add `network-manager-applet`. |
| `niri/config.kdl` | Modified: re-add `spawn-at-startup "nm-applet" "--indicator"`. |
| `README.md` | Modified: append `### One-time NetworkManager activation` subsection under `## Setup`. |
---
## Task 1: Tracked drop-in + install.sh deploy
After this task, the repo carries the wifi-backend drop-in and `install.sh` deploys it. The system file `/etc/NetworkManager/conf.d/wifi-backend.conf` lands when install.sh runs. NetworkManager isn't enabled yet — Task 4 (manual user step) does that.
**Files:**
- Create: `NetworkManager/conf.d/wifi-backend.conf`
- Modify: `install.sh` (append after the existing `sleep/hibernate-delay.conf` line at line 73)
- [ ] **Step 1: Create the directory and drop-in file**
```bash
mkdir -p NetworkManager/conf.d
```
Write `NetworkManager/conf.d/wifi-backend.conf` with this exact content:
```ini
[device]
wifi.backend=iwd
```
- [ ] **Step 2: Verify file contents**
```bash
cat NetworkManager/conf.d/wifi-backend.conf
```
Expected: exactly the two lines above.
- [ ] **Step 3: Add deploy line to `install.sh`**
Find the existing suspend/hibernate deploy block in `install.sh` (lines 71-73):
```bash
echo "==> Deploying suspend/hibernate config"
sudo install -Dm644 "$(pwd)/logind/lid.conf" /etc/systemd/logind.conf.d/lid.conf
sudo install -Dm644 "$(pwd)/sleep/hibernate-delay.conf" /etc/systemd/sleep.conf.d/hibernate-delay.conf
```
Immediately *after* the second `sudo install -Dm644` line (the sleep one), insert:
```bash
echo "==> Deploying NetworkManager config"
sudo install -Dm644 "$(pwd)/NetworkManager/conf.d/wifi-backend.conf" /etc/NetworkManager/conf.d/wifi-backend.conf
```
(Leading blank line for visual separation between the two deploy blocks.)
- [ ] **Step 4: Bash-syntax-check install.sh**
```bash
bash -n install.sh && echo "syntax ok"
```
Expected: `syntax ok` and exit 0.
- [ ] **Step 5: Confirm the new line is in place**
```bash
grep -n "wifi-backend" install.sh
```
Expected output:
```
sudo install -Dm644 "$(pwd)/NetworkManager/conf.d/wifi-backend.conf" /etc/NetworkManager/conf.d/wifi-backend.conf
```
- [ ] **Step 6: Commit**
```bash
git add NetworkManager/conf.d/wifi-backend.conf install.sh
git commit -m "NetworkManager: track wifi-backend.conf and deploy via install.sh"
```
---
## Task 2: Re-add `network-manager-applet` to packages.txt
After this task, `network-manager-applet` is back in the package manifest. Anyone setting up the system fresh installs the applet binary (currently still on this user's machine — uninstalling wasn't part of the earlier removal — but the manifest needs to match reality).
**Files:**
- Modify: `packages.txt` (re-add one line)
- [ ] **Step 1: Add `network-manager-applet` back to `packages.txt`**
Open `packages.txt`. The list isn't strictly alphabetical, but `networkmanager` is on line 17 (existing), so insert the applet on line 18 right after it:
Current lines 17-18:
```
networkmanager
xdg-desktop-portal-wlr
```
Becomes:
```
networkmanager
network-manager-applet
xdg-desktop-portal-wlr
```
- [ ] **Step 2: Verify the entry is present exactly once**
```bash
grep -c "^network-manager-applet$" packages.txt
```
Expected: `1`.
- [ ] **Step 3: Commit**
```bash
git add packages.txt
git commit -m "packages: re-add network-manager-applet (now functional)"
```
---
## Task 3: Re-add `nm-applet` spawn-at-startup in niri
After this task, `nm-applet --indicator` runs at niri start. Combined with the working SNI watcher (waybar's built-in), the network connection icon will appear in the tray once NetworkManager is enabled (Task 5, manual).
**Files:**
- Modify: `niri/config.kdl` (insert one spawn-at-startup line)
- [ ] **Step 1: Find the right insertion point**
The existing spawn-at-startup block is around lines 34-46 (after the env block, before `binds {`). It currently does NOT contain `nm-applet`. Insertion goes anywhere in that block; placing it logically with other applet-style spawns (after `awww-daemon` which is line 36) keeps it grouped with desktop-startup helpers.
- [ ] **Step 2: Insert the line**
Find lines 36-37:
```kdl
spawn-at-startup "awww-daemon"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
```
Insert between them:
```kdl
spawn-at-startup "nm-applet" "--indicator"
```
Result (lines 36-38):
```kdl
spawn-at-startup "awww-daemon"
spawn-at-startup "nm-applet" "--indicator"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
```
- [ ] **Step 3: Validate niri config**
```bash
niri validate 2>&1 | tail -2
```
Expected: a final line `INFO niri: config is valid`. Non-zero exit means a syntax issue.
- [ ] **Step 4: Confirm the line is in place**
```bash
grep -n 'nm-applet' niri/config.kdl
```
Expected: one match, `spawn-at-startup "nm-applet" "--indicator"`.
- [ ] **Step 5: Commit**
```bash
git add niri/config.kdl
git commit -m "niri: re-add nm-applet --indicator spawn-at-startup"
```
---
## Task 4: README — One-time NetworkManager activation subsection
After this task, the README documents the one-time `systemctl` swap so a fresh-install reader knows the manual step required after `install.sh` deploys the config.
**Files:**
- Modify: `README.md` (append a subsection after the existing "One-time hibernation enablement")
- [ ] **Step 1: Append the new subsection**
Open `README.md`. The last subsection is `### One-time hibernation enablement` (added previously). After its closing line, append:
````markdown
### One-time NetworkManager activation
After `install.sh` deploys the NetworkManager config, swap from `systemd-networkd` to `NetworkManager` (one-time, persistent across reboots):
```bash
sudo systemctl disable --now systemd-networkd
sudo systemctl enable --now NetworkManager
```
Wifi will drop for a few seconds and reconnect via iwd's stored profiles. If reconnection fails, re-enter the wifi password via `nm-applet` (tray icon) or `networkmanager_dmenu` (waybar network widget click).
````
(The outer fence above is four backticks because the block contains a triple-backtick code block — paste the inner content verbatim, no enclosing 4-backtick fence.)
- [ ] **Step 2: Verify the section was added**
```bash
grep -A 6 "One-time NetworkManager activation" README.md
```
Expected: the new subsection prints, including the `sudo systemctl enable --now NetworkManager` line.
- [ ] **Step 3: Commit**
```bash
git add README.md
git commit -m "docs: document one-time NetworkManager activation"
```
---
## Final verification (controller, post-merge)
These run on the live system, after merging all four task commits and running the README's manual systemctl commands.
- [ ] **Run install.sh to deploy the config**
```bash
bash install.sh
# Expects "==> Deploying NetworkManager config" to appear in output
ls -la /etc/NetworkManager/conf.d/wifi-backend.conf
```
Expected: the file exists, mode 644, owned by root, content matches the repo file.
- [ ] **Run the documented systemctl swap (one-time, requires sudo)**
```bash
sudo systemctl disable --now systemd-networkd
sudo systemctl enable --now NetworkManager
```
Expected: both succeed; wifi briefly drops then reconnects within ~5-10 s.
- [ ] **Verify NetworkManager is the active connection manager**
```bash
systemctl is-active NetworkManager # active
systemctl is-active systemd-networkd # inactive
nmcli general status # prints connectivity status
nmcli connection show --active # shows the wifi connection
```
- [ ] **Verify nm-applet appears in the tray**
After a niri restart (or `niri msg action spawn -- nm-applet --indicator` if niri's spawn IPC supports it), the nm-applet tray icon should appear in waybar:
```bash
busctl --user get-property org.kde.StatusNotifierWatcher \
/StatusNotifierWatcher org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems
```
Expected: includes `:.../org/ayatana/NotificationItem/nm_applet` (or similar nm_applet entry).
- [ ] **Verify the waybar network widget click works**
Click the network module in waybar — `networkmanager_dmenu` should pop up a wofi-style picker listing available networks. Selecting one should connect.
@@ -0,0 +1,302 @@
# Framework 13 Fingerprint Reader Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Enable the Framework 13's Goodix fingerprint reader for `sudo` and `gtklock` via `fprintd` + a one-time idempotent PAM-edit script.
**Architecture:** Three small repo changes plus one user-side runtime activity. Repo: add `fprintd` to `packages.txt`, ship a `scripts/enable-fingerprint.sh` bootstrap that adds `auth sufficient pam_fprintd.so` to two PAM files only if not already present, document the two manual user steps (enroll + run script) in README. Runtime: user runs `fprintd-enroll` once + `sudo bash scripts/enable-fingerprint.sh` once.
**Tech Stack:** bash, `fprintd`/`libfprint`, PAM, Arch Linux.
**Spec:** `docs/superpowers/specs/2026-05-25-fingerprint-reader-design.md`
**Verification model:** Bash syntax checks for the new script + idempotency dry-run (run script's grep-guards against a copy and confirm they short-circuit). End-to-end functional check (touch reader → unlock) is interactive and live-only — controller verifies after install + manual enroll.
---
## File Structure
| File | Role |
|---|---|
| `packages.txt` | Add `fprintd` line (libfprint pulled as dep). |
| `scripts/enable-fingerprint.sh` | New bash bootstrap. For each of `/etc/pam.d/sudo` and `/etc/pam.d/gtklock`: backup + insert `auth sufficient pam_fprintd.so` before first existing `auth ` line if not already present. Requires sudo. |
| `README.md` | Append `### One-time fingerprint setup` subsection under `## Setup`. |
No new niri config, no install.sh changes (the bootstrap is user-triggered like `enable-hibernation.sh`).
---
## Task 1: Add `fprintd` to `packages.txt`
After this task, fresh installs get `fprintd` (and `libfprint` as transitive dependency). The reader is unusable until enrollment + PAM wiring (later tasks).
**Files:**
- Modify: `packages.txt` (add one line)
- [ ] **Step 1: Add the package**
The list isn't strictly alphabetical. Insert `fprintd` between two logical neighbours — after `polkit-gnome` (line 16) is a sensible "auth/security" cluster slot:
Current lines 16-17:
```
polkit-gnome
networkmanager
```
Becomes:
```
polkit-gnome
fprintd
networkmanager
```
- [ ] **Step 2: Verify the entry is present exactly once**
```bash
grep -c "^fprintd$" packages.txt
```
Expected: `1`.
- [ ] **Step 3: Commit**
```bash
git add packages.txt
git commit -m "packages: add fprintd for fingerprint reader support"
```
---
## Task 2: Write the bootstrap script
After this task, `scripts/enable-fingerprint.sh` exists. Running it as root adds `auth sufficient pam_fprintd.so` to the two target PAM files. Re-runs are no-ops.
**Files:**
- Create: `scripts/enable-fingerprint.sh`
- [ ] **Step 1: Create the script**
Write `scripts/enable-fingerprint.sh` with this exact content:
```bash
#!/bin/bash
# enable-fingerprint.sh — wire pam_fprintd.so into sudo + gtklock auth stacks.
# Idempotent: re-runs are no-ops once the line is present. Run with sudo.
# Prerequisite: fprintd installed and at least one finger enrolled
# (run `fprintd-enroll` as your user first).
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "Run as root: sudo bash $0" >&2
exit 1
fi
if ! command -v fprintd-enroll >/dev/null 2>&1; then
echo "ERROR: fprintd is not installed. Run install.sh first." >&2
exit 1
fi
PAM_LINE="auth sufficient pam_fprintd.so"
TARGETS=(/etc/pam.d/sudo /etc/pam.d/gtklock)
for f in "${TARGETS[@]}"; do
if [ ! -f "$f" ]; then
echo "ERROR: $f does not exist. Is the corresponding package installed?" >&2
exit 1
fi
if grep -q "pam_fprintd" "$f"; then
echo "$f: pam_fprintd already present — skipping"
continue
fi
backup="$f.bak.$(date +%s)"
cp -a "$f" "$backup"
echo "$f: backed up to $backup"
# Insert PAM_LINE on a new line directly before the first `auth ` directive.
awk -v line="$PAM_LINE" '
!inserted && /^auth[[:space:]]/ { print line; inserted = 1 }
{ print }
' "$f" > "$f.new"
if ! grep -q "pam_fprintd" "$f.new"; then
echo "ERROR: failed to insert PAM line into $f (no auth directive found?). Restoring backup." >&2
rm -f "$f.new"
exit 1
fi
mv "$f.new" "$f"
chmod --reference="$backup" "$f"
echo "$f: inserted '$PAM_LINE'"
done
echo
echo "Done. Test with: sudo true (should prompt for fingerprint)"
echo "Or lock the screen (Mod+Shift+E) and touch the reader."
echo
echo "To revert: restore the .bak.<ts> files in /etc/pam.d/, or:"
echo " sudo sed -i '/pam_fprintd/d' ${TARGETS[*]}"
```
- [ ] **Step 2: Make it executable**
```bash
chmod +x scripts/enable-fingerprint.sh
ls -la scripts/enable-fingerprint.sh
```
Expected: `-rwxr-xr-x` mode.
- [ ] **Step 3: Syntax-check**
```bash
bash -n scripts/enable-fingerprint.sh && echo "syntax ok"
```
Expected: `syntax ok`, exit 0.
- [ ] **Step 4: Dry-run the EUID guard (no sudo, no side effects)**
```bash
bash scripts/enable-fingerprint.sh 2>&1
echo "exit=$?"
```
Expected:
```
Run as root: sudo bash scripts/enable-fingerprint.sh
exit=1
```
- [ ] **Step 5: Dry-run the idempotency / awk logic against a fixture (no sudo)**
Create a fake PAM file in /tmp with an `auth` line, run the insertion logic by hand, and verify the line lands in the right place:
```bash
cat > /tmp/fake-pam <<'EOF'
#%PAM-1.0
auth include system-auth
account include system-auth
password include system-auth
session include system-auth
EOF
# Pull the awk insertion block out and run it
awk -v line="auth sufficient pam_fprintd.so" '
!inserted && /^auth[[:space:]]/ { print line; inserted = 1 }
{ print }
' /tmp/fake-pam
# Expected output: the new auth line appears on line 2, original 4 lines below it.
```
Then verify the idempotency grep works — running on a fixture that already has `pam_fprintd` should not duplicate:
```bash
echo "auth sufficient pam_fprintd.so" >> /tmp/fake-pam
grep -q "pam_fprintd" /tmp/fake-pam && echo "idempotency guard triggers correctly"
```
Expected: `idempotency guard triggers correctly`.
Cleanup:
```bash
rm /tmp/fake-pam
```
- [ ] **Step 6: Commit**
```bash
git add scripts/enable-fingerprint.sh
git commit -m "scripts: add enable-fingerprint.sh (one-time PAM bootstrap)"
```
---
## Task 3: README — `### One-time fingerprint setup` subsection
After this task, the README documents all three user-side steps (install run is implicit via packages.txt; enrollment + bootstrap script are explicit).
**Files:**
- Modify: `README.md` (append a subsection after the existing "One-time NetworkManager activation")
- [ ] **Step 1: Append the new subsection**
Open `README.md`. The last subsection is `### One-time NetworkManager activation`. After its closing line, append:
````markdown
### One-time fingerprint setup
After `install.sh` installs `fprintd`, enroll a finger and wire it into the sudo + lockscreen auth stacks:
```bash
fprintd-enroll # interactive; touch the reader as prompted
sudo bash scripts/enable-fingerprint.sh # idempotent — safe to re-run
```
Test with `sudo true` (should prompt for fingerprint) or `Mod+Shift+E` to lock + touch the reader.
To enroll additional fingers: `fprintd-enroll -f left-thumb` (or `right-thumb`, `left-index`, etc.).
````
(The outer fence above is four backticks because the block contains a triple-backtick code block — paste the inner content verbatim, no enclosing 4-backtick fence.)
- [ ] **Step 2: Verify the section was added**
```bash
grep -A 8 "One-time fingerprint setup" README.md
```
Expected: the new subsection prints, including both the `fprintd-enroll` and `sudo bash scripts/enable-fingerprint.sh` lines.
- [ ] **Step 3: Commit**
```bash
git add README.md
git commit -m "docs: document one-time fingerprint setup"
```
---
## Final verification (controller, post-merge)
After all three task commits land:
- [ ] **Install fprintd**
```bash
bash install.sh
# OR direct: sudo pacman -S --needed fprintd
which fprintd-enroll fprintd-verify fprintd-list
```
- [ ] **Enroll a finger** (interactive, ~10 s):
```bash
fprintd-enroll
# Touch the reader as prompted (~5 swipes)
fprintd-list "$USER"
# Expected: lists enrolled fingers
```
- [ ] **Run the bootstrap script**
```bash
sudo bash scripts/enable-fingerprint.sh
```
Expected output mentions backing up `/etc/pam.d/sudo` and `/etc/pam.d/gtklock` and inserting the line. Re-running it should print "pam_fprintd already present — skipping" for both.
Confirm:
```bash
grep "pam_fprintd" /etc/pam.d/sudo /etc/pam.d/gtklock
```
Expected: both files show the line near the top.
- [ ] **Functional tests:**
1. `sudo true` — should prompt for fingerprint; touch reader → no password requested. (Password fallback still works if you ignore the prompt or press Enter.)
2. `Mod+Shift+E` → lock screen → touch reader → unlocks without typing password.
3. `fprintd-verify` — touch reader → "verify-match" exits 0.
@@ -0,0 +1,439 @@
# Replace gtklock with hyprlock Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Swap the lockscreen from `gtklock` to `hyprlock` so fingerprint unlock works without typing a character first.
**Architecture:** Replace one tracked config tree (`gtklock/`) with another (`hyprlock/`). Update `packages.txt`, `install.sh`, `niri/config.kdl`, `scripts/enable-fingerprint.sh`, and docs. The user re-runs `install.sh` + `enable-fingerprint.sh` after merge.
**Tech Stack:** Hyprlang config, niri, bash, PAM.
**Spec:** `docs/superpowers/specs/2026-05-25-hyprlock-replace-gtklock-design.md`
**Verification model:** `niri validate` for niri changes, `bash -n` for shell, parse-check for the Hyprlang config (`hyprlock --help` won't validate config without `-c`; spec-correctness is via inspection). Functional end-to-end (touch reader → unlock) is interactive — controller verifies after the live install + bootstrap.
---
## File Structure
| File | Role |
|---|---|
| `hyprlock/hyprlock.conf` | New — single Hyprlang file, deployed to `~/.config/hypr/hyprlock.conf` |
| `packages.txt` | swap `gtklock``hyprlock`, drop `gtklock-userinfo-module` if present |
| `install.sh` | drop gtklock symlinks + the `gtklock` mkdir entry, add hyprlock symlink |
| `niri/config.kdl` | swap 3 gtklock invocations (Mod+Shift+E + 2 swayidle) |
| `scripts/enable-fingerprint.sh` | swap target in `TARGETS` array |
| `gtklock/config.ini`, `gtklock/style.css` | deleted |
| `README.md`, `ARCHITECTURE.md`, `CLAUDE.md` | swap lockscreen references |
5 tasks, each atomic.
---
## Task 1: Add `hyprlock` config + swap `install.sh` + `packages.txt`
After this task: `hyprlock` is in `packages.txt`, `hyprlock/hyprlock.conf` exists, `install.sh` symlinks it into `~/.config/hypr/` (mkdir-ing the dir if needed), and the gtklock symlinks are gone from `install.sh`. The repo still has `gtklock/` files and niri still calls gtklock — Tasks 2-4 finish the swap.
**Files:**
- Create: `hyprlock/hyprlock.conf`
- Modify: `packages.txt`
- Modify: `install.sh` (lines 19, 43-44)
- [ ] **Step 1: Create `hyprlock/hyprlock.conf`**
```bash
mkdir -p hyprlock
```
Write `hyprlock/hyprlock.conf` with this exact content:
```hyprlang
general {
grace = 0
hide_cursor = true
}
background {
monitor =
path = screenshot
blur_passes = 3
blur_size = 8
}
label {
monitor =
text = cmd[update:1000] date +"%-I:%M %p"
font_size = 96
color = rgba(c5c8c6ff)
position = 0, 150
halign = center
valign = center
}
label {
monitor =
text = cmd[update:60000] date +"%A, %B %-d"
font_size = 24
color = rgba(969896ff)
position = 0, 40
halign = center
valign = center
}
input-field {
monitor =
size = 300, 50
outline_thickness = 2
dots_size = 0.25
dots_spacing = 0.4
inner_color = rgba(282a2eff)
outer_color = rgba(81a2beff)
font_color = rgba(c5c8c6ff)
fail_color = rgba(cc6666ff)
check_color = rgba(b5bd68ff)
placeholder_text = <i>Touch fingerprint or type password</i>
fail_text = <i>$FAIL</i>
fade_on_empty = false
position = 0, -50
halign = center
valign = center
}
```
- [ ] **Step 2: Update `packages.txt`**
Currently `gtklock` is on line 13 (between `pamixer` and `swayidle`):
```
gtklock
swayidle
```
Replace `gtklock` with `hyprlock`:
```
hyprlock
swayidle
```
If `gtklock-userinfo-module` appears anywhere in the file, also delete that line. Verify with:
```bash
grep -E "gtklock" packages.txt
```
Expected: zero matches after edit.
```bash
grep -c "^hyprlock$" packages.txt
```
Expected: `1`.
- [ ] **Step 3: Update `install.sh` — remove gtklock symlinks and entry, add hyprlock symlink**
Find lines 43-44:
```bash
ln -sf "$(pwd)/gtklock/config.ini" ~/.config/gtklock/config
ln -sf "$(pwd)/gtklock/style.css" ~/.config/gtklock/style.css
```
Replace both lines with:
```bash
mkdir -p ~/.config/hypr
ln -sf "$(pwd)/hyprlock/hyprlock.conf" ~/.config/hypr/hyprlock.conf
```
Also find line 19 (the brace-expansion mkdir):
```bash
mkdir -p ~/.config/{niri,waybar,wofi,mako,alacritty,gtk-3.0,gtk-4.0,gtklock,fish} ~/.config/environment.d ~/.config/xdg-desktop-portal
```
Remove `gtklock,` from the brace list (no need to mkdir gtklock anymore):
```bash
mkdir -p ~/.config/{niri,waybar,wofi,mako,alacritty,gtk-3.0,gtk-4.0,fish} ~/.config/environment.d ~/.config/xdg-desktop-portal
```
- [ ] **Step 4: Bash-syntax-check install.sh**
```bash
bash -n install.sh && echo "syntax ok"
```
Expected: `syntax ok`, exit 0.
- [ ] **Step 5: Confirm changes landed**
```bash
grep -nE "gtklock|hyprlock" install.sh
```
Expected: 2 hits — both for hyprlock (`mkdir -p ~/.config/hypr` and the `ln -sf ... hyprlock.conf` line). No `gtklock` references remain.
```bash
ls hyprlock/hyprlock.conf && wc -l hyprlock/hyprlock.conf
```
Expected: file exists, ~52 lines.
- [ ] **Step 6: Commit**
```bash
git add hyprlock/hyprlock.conf packages.txt install.sh
git commit -m "hyprlock: add config + swap install.sh away from gtklock"
```
---
## Task 2: Swap niri keybind + swayidle hooks from gtklock to hyprlock
After this task, niri's lock keybind and swayidle's lock-on-idle/before-sleep hooks all call `hyprlock` instead of `gtklock -d`. The repo still has the `gtklock/` config files and the old PAM target in `enable-fingerprint.sh` — Tasks 3-4 finish those.
**Files:**
- Modify: `niri/config.kdl`
- [ ] **Step 1: Find the three gtklock invocations**
```bash
grep -n "gtklock" niri/config.kdl
```
Expected: 3 matches — one in the swayidle `spawn-at-startup` line (with two `"gtklock" "-d"` substrings — counts as the same line, may show twice if grep prints once per line), and one keybind line `Mod+Shift+E { spawn "gtklock" "-d"; }`.
- [ ] **Step 2: Replace the keybind**
Find `Mod+Shift+E { spawn "gtklock" "-d"; }` and replace with:
```kdl
Mod+Shift+E { spawn "hyprlock"; }
```
Indentation: 4 spaces, matching the surrounding `binds` block entries.
- [ ] **Step 3: Replace the swayidle line's two `"gtklock" "-d"` occurrences**
The swayidle spawn-at-startup line looks like:
```kdl
spawn-at-startup "swayidle" "-w" "timeout" "300" "niri msg action power-off-monitors" "timeout" "600" "gtklock" "-d" "timeout" "1800" "systemctl suspend-then-hibernate" "before-sleep" "gtklock" "-d"
```
Replace both `"gtklock" "-d"` substrings with `"hyprlock"`. The full line becomes:
```kdl
spawn-at-startup "swayidle" "-w" "timeout" "300" "niri msg action power-off-monitors" "timeout" "600" "hyprlock" "timeout" "1800" "systemctl suspend-then-hibernate" "before-sleep" "hyprlock"
```
Note: each `"gtklock" "-d"` (two quoted strings, two niri spawn args) becomes a single `"hyprlock"` (one quoted string).
- [ ] **Step 4: Validate niri config**
```bash
niri validate 2>&1 | tail -2
```
Expected: a final `INFO niri: config is valid`. Exit 0.
- [ ] **Step 5: Confirm gtklock is gone, hyprlock present**
```bash
grep -c "gtklock" niri/config.kdl
grep -c "hyprlock" niri/config.kdl
```
Expected: `0` for gtklock, `3` for hyprlock (one keybind + two swayidle action positions).
- [ ] **Step 6: Commit**
```bash
git add niri/config.kdl
git commit -m "niri: switch lock + idle hooks to hyprlock"
```
---
## Task 3: Update `scripts/enable-fingerprint.sh` PAM target
After this task, re-running the script targets `/etc/pam.d/hyprlock` instead of `/etc/pam.d/gtklock`. The existing line in `/etc/pam.d/gtklock` is left in place on the live system (harmless dead config — see Out of Scope in the spec).
**Files:**
- Modify: `scripts/enable-fingerprint.sh` (one line in the `TARGETS` array)
- [ ] **Step 1: Find the TARGETS line**
```bash
grep -n "TARGETS" scripts/enable-fingerprint.sh
```
Expected: one line like:
```
TARGETS=(/etc/pam.d/sudo /etc/pam.d/gtklock)
```
- [ ] **Step 2: Replace `gtklock` with `hyprlock`**
Change the line to:
```bash
TARGETS=(/etc/pam.d/sudo /etc/pam.d/hyprlock)
```
- [ ] **Step 3: Syntax-check**
```bash
bash -n scripts/enable-fingerprint.sh && echo "syntax ok"
```
Expected: `syntax ok`, exit 0.
- [ ] **Step 4: Confirm the swap**
```bash
grep -n "gtklock\|hyprlock" scripts/enable-fingerprint.sh
```
Expected: one match — the new `hyprlock` line. Zero `gtklock` references.
- [ ] **Step 5: Commit**
```bash
git add scripts/enable-fingerprint.sh
git commit -m "scripts: enable-fingerprint targets hyprlock PAM file"
```
---
## Task 4: Delete the tracked `gtklock/` directory
After this task, the repo no longer carries `gtklock/config.ini` or `gtklock/style.css`. The system's `/etc/pam.d/gtklock` (with the harmless dead pam_fprintd line) is untouched. The user's `~/.config/gtklock/` symlinks become broken but harmless since gtklock is no longer launched.
**Files:**
- Delete: `gtklock/config.ini`
- Delete: `gtklock/style.css`
- [ ] **Step 1: Remove the tracked files**
```bash
git rm gtklock/config.ini gtklock/style.css
```
Expected output:
```
rm 'gtklock/config.ini'
rm 'gtklock/style.css'
```
The empty `gtklock/` directory may remain — git ignores empty directories. If it bothers you cosmetically, `rmdir gtklock` to clean up.
- [ ] **Step 2: Confirm no references remain in the repo (outside docs)**
```bash
grep -rn "gtklock" --exclude-dir=.git --exclude-dir=docs --exclude-dir=.claude /home/alex/Documents/dotfiles
```
Expected: only matches in README.md, ARCHITECTURE.md, CLAUDE.md (those get updated in Task 5). No matches in install.sh, niri/, scripts/, packages.txt, or the deleted `gtklock/` directory.
- [ ] **Step 3: Commit**
```bash
git commit -m "gtklock: delete tracked config (replaced by hyprlock)"
```
(`git rm` already staged the deletions; no further `git add` needed.)
---
## Task 5: Update README, ARCHITECTURE, CLAUDE.md
After this task, all documentation references `hyprlock` instead of `gtklock`.
**Files:**
- Modify: `README.md`
- Modify: `ARCHITECTURE.md`
- Modify: `CLAUDE.md`
- [ ] **Step 1: Find every gtklock reference in the three docs**
```bash
grep -n "gtklock" README.md ARCHITECTURE.md CLAUDE.md
```
Expected: a small handful of matches. Most are in tooling tables (`| Lockscreen | gtklock |`) and short narrative mentions.
- [ ] **Step 2: README.md — replace each gtklock occurrence with hyprlock**
For each line `grep` returned in README.md, change `gtklock``hyprlock`. Most likely:
- A tooling table row: `| Lockscreen | gtklock |``| Lockscreen | hyprlock |`
- The `Mod+Shift+E` mention in the fingerprint setup section, if it referenced gtklock by name (verify against the actual line — it may already be `Mod+Shift+E to lock` without naming the lockscreen)
For each match, use `sed -i 's/gtklock/hyprlock/g' README.md` if every occurrence should change verbatim, or hand-edit if context requires more thought (rare for this kind of swap).
Verify after:
```bash
grep -n "gtklock" README.md
```
Expected: zero matches.
```bash
grep -c "hyprlock" README.md
```
Expected: matches the count from Step 1 (1-for-1 replacement).
- [ ] **Step 3: ARCHITECTURE.md — same swap**
```bash
sed -i 's/gtklock/hyprlock/g' ARCHITECTURE.md
grep -n "gtklock" ARCHITECTURE.md
```
Expected: zero matches.
- [ ] **Step 4: CLAUDE.md — same swap**
```bash
sed -i 's/gtklock/hyprlock/g' CLAUDE.md
grep -n "gtklock" CLAUDE.md
```
Expected: zero matches.
- [ ] **Step 5: Final repo-wide sanity check**
```bash
grep -rn "gtklock" --exclude-dir=.git --exclude-dir=docs --exclude-dir=.claude /home/alex/Documents/dotfiles
```
Expected: zero matches anywhere except inside `docs/` (the historical spec/plan files for the original gtklock setup) and `.claude/` (cached permission entries).
- [ ] **Step 6: Commit**
```bash
git add README.md ARCHITECTURE.md CLAUDE.md
git commit -m "docs: flip lockscreen references to hyprlock"
```
---
## Final verification (controller, post-merge)
- [ ] **Install hyprlock**:
```bash
bash install.sh
which hyprlock
```
Expected: `/usr/bin/hyprlock`.
- [ ] **Verify config deployed**:
```bash
ls -la ~/.config/hypr/hyprlock.conf
```
Expected: symlink to repo's `hyprlock/hyprlock.conf`.
- [ ] **Re-run PAM bootstrap**:
```bash
sudo bash scripts/enable-fingerprint.sh
```
Expected output mentions `/etc/pam.d/hyprlock` got the new line; `/etc/pam.d/sudo` "already present — skipping".
- [ ] **Verify PAM line is in hyprlock**:
```bash
grep pam_fprintd /etc/pam.d/hyprlock
```
Expected: one match.
- [ ] **Functional tests**:
1. `Mod+Shift+E` — locks the screen with clock + date + input field.
2. Touch the fingerprint reader (without typing anything) — unlocks immediately.
3. Re-lock; type the password instead — also unlocks (fallback).
4. Wait 10 min idle — swayidle triggers `hyprlock` automatically.
@@ -0,0 +1,549 @@
# Theme Centralization & Bluetooth Module Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `theme/colors.json` the single source of truth for the desktop palette, generating all derived configs (waybar/wofi CSS, mako, hyprlock colors) from it at install time, and add a bluetooth indicator to waybar.
**Architecture:** A pure-function Python generator (`theme/generate-theme.py`) reads `colors.json` and renders three artifacts into `~/.config` at install time (matching how mako is already generated). `hyprlock.conf` switches to a sourced hyprlang color file using `$tn_*` variables. The hand-maintained `theme/colors.css` is deleted and becomes a generated artifact. Bluetooth uses waybar's built-in module.
**Tech Stack:** Python 3 (stdlib only: `json`, `os`), waybar (JSONC + GTK CSS), hyprlang (hyprlock config), bash (install.sh).
---
## File Structure
- `theme/colors.json`**source of truth** (unchanged; 12 base colors).
- `theme/generate-theme.py`**new** generator. Pure render functions + a `main()` that writes outputs. One responsibility: turn the palette into config artifacts.
- `theme/test_generate_theme.py`**new** test for the generator's pure functions.
- `theme/colors.css`**deleted from repo** (now generated into `~/.config/theme/colors.css`).
- `install.sh`**modified**: call the generator, drop the inline mako snippet and the colors.css symlink.
- `hyprlock/hyprlock.conf`**modified**: `source=` the generated color file, replace hardcoded `rgba(...)` with `$tn_*` vars.
- `waybar/config.jsonc`**modified**: add `bluetooth` module.
- `waybar/style.css`**modified**: add `#bluetooth` style rules.
- `packages.txt`**modified**: ensure `bluez`/`bluez-utils` present.
Two independent features (theme: Tasks 16; bluetooth: Task 7). Implement and verify separately.
---
## Task 1: Generator — palette loading and hex conversion
**Files:**
- Create: `theme/generate-theme.py`
- Test: `theme/test_generate_theme.py`
- [ ] **Step 1: Write the failing test**
```python
# theme/test_generate_theme.py
import importlib.util, os
_spec = importlib.util.spec_from_file_location(
"gen", os.path.join(os.path.dirname(__file__), "generate-theme.py"))
gen = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(gen)
def test_load_palette_reads_colors_json():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
assert pal["background"] == "#1d1f21"
assert pal["blue"] == "#81a2be"
def test_hex_to_hyprlock_rgba():
assert gen.hex_to_hyprlock_rgba("#81a2be") == "rgba(81a2beff)"
assert gen.hex_to_hyprlock_rgba("#1d1f21") == "rgba(1d1f21ff)"
def test_hex_to_rgb_tuple():
assert gen.hex_to_rgb("#1d1f21") == (29, 31, 33)
if __name__ == "__main__":
test_load_palette_reads_colors_json()
test_hex_to_hyprlock_rgba()
test_hex_to_rgb_tuple()
print("OK")
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: FAIL — `FileNotFoundError` / `AttributeError: module 'gen' has no attribute 'load_palette'` (generate-theme.py does not exist yet).
- [ ] **Step 3: Write minimal implementation**
```python
# theme/generate-theme.py
"""Generate desktop config artifacts from theme/colors.json (single source of truth)."""
import json
import os
def load_palette(path):
return json.load(open(path))
def hex_to_rgb(h):
h = h.lstrip("#")
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def hex_to_hyprlock_rgba(h):
return f"rgba({h.lstrip('#')}ff)"
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: `OK`
- [ ] **Step 5: Commit**
```bash
git add theme/generate-theme.py theme/test_generate_theme.py
git commit -m "feat(theme): generator palette loading + hex conversion"
```
---
## Task 2: Generator — render colors.css (with alpha variants)
**Files:**
- Modify: `theme/generate-theme.py`
- Test: `theme/test_generate_theme.py`
- [ ] **Step 1: Write the failing test**
Add to `theme/test_generate_theme.py` (and add the call in `__main__`):
```python
def test_render_colors_css():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
css = gen.render_colors_css(pal)
# base colors
assert "@define-color tn-bg #1d1f21;" in css
assert "@define-color tn-bg-alt #282a2e;" in css
assert "@define-color tn-bg-high #373b41;" in css
assert "@define-color tn-fg #c5c8c6;" in css
assert "@define-color tn-fg-dim #969896;" in css
assert "@define-color tn-fg-muted #707880;" in css
assert "@define-color tn-blue #81a2be;" in css
assert "@define-color tn-green #b5bd68;" in css
assert "@define-color tn-yellow #f0c674;" in css
assert "@define-color tn-red #cc6666;" in css
assert "@define-color tn-magenta #b294bb;" in css
assert "@define-color tn-cyan #8abeb7;" in css
# alpha variants computed from background (29,31,33)
assert "@define-color tn-bg-a90 rgba(29, 31, 33, 0.90);" in css
assert "@define-color tn-bg-a95 rgba(29, 31, 33, 0.95);" in css
assert "@define-color tn-bg-a96 rgba(29, 31, 33, 0.96);" in css
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: FAIL — `AttributeError: module 'gen' has no attribute 'render_colors_css'`
- [ ] **Step 3: Write minimal implementation**
Add to `theme/generate-theme.py`:
```python
# (key in colors.json) -> (css variable suffix)
_CSS_MAP = [
("background", "bg"), ("background_alt", "bg-alt"), ("background_high", "bg-high"),
("foreground", "fg"), ("foreground_dim", "fg-dim"), ("foreground_muted", "fg-muted"),
("blue", "blue"), ("green", "green"), ("yellow", "yellow"),
("red", "red"), ("magenta", "magenta"), ("cyan", "cyan"),
]
_ALPHA_VARIANTS = [("a90", "0.90"), ("a95", "0.95"), ("a96", "0.96")]
def render_colors_css(pal):
lines = ["/* Tomorrow Night - GENERATED from colors.json by generate-theme.py. DO NOT EDIT. */", ""]
for key, suffix in _CSS_MAP:
lines.append(f"@define-color tn-{suffix} {pal[key]};")
lines.append("")
r, g, b = hex_to_rgb(pal["background"])
for name, alpha in _ALPHA_VARIANTS:
lines.append(f"@define-color tn-bg-{name} rgba({r}, {g}, {b}, {alpha});")
return "\n".join(lines) + "\n"
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: `OK`
- [ ] **Step 5: Commit**
```bash
git add theme/generate-theme.py theme/test_generate_theme.py
git commit -m "feat(theme): render colors.css from palette"
```
---
## Task 3: Generator — render mako config
**Files:**
- Modify: `theme/generate-theme.py`
- Test: `theme/test_generate_theme.py`
- [ ] **Step 1: Write the failing test**
Add to `theme/test_generate_theme.py` (and call in `__main__`):
```python
def test_render_mako_config():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
cfg = gen.render_mako_config(pal)
assert "background-color=#1d1f21" in cfg
assert "text-color=#c5c8c6" in cfg
assert "border-size=2" in cfg
assert "border-color=#81a2be" in cfg
assert "default-timeout=4000" in cfg
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: FAIL — `AttributeError: module 'gen' has no attribute 'render_mako_config'`
- [ ] **Step 3: Write minimal implementation**
Add to `theme/generate-theme.py` (preserves the exact output of the current inline install.sh snippet):
```python
def render_mako_config(pal):
return (
f"background-color={pal['background']}\n"
f"text-color={pal['foreground']}\n"
"border-size=2\n"
f"border-color={pal['blue']}\n"
"default-timeout=4000\n"
)
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: `OK`
- [ ] **Step 5: Commit**
```bash
git add theme/generate-theme.py theme/test_generate_theme.py
git commit -m "feat(theme): render mako config from palette"
```
---
## Task 4: Generator — render hyprlock colors + `main()` writer
**Files:**
- Modify: `theme/generate-theme.py`
- Test: `theme/test_generate_theme.py`
- [ ] **Step 1: Write the failing test**
Add to `theme/test_generate_theme.py` (and call in `__main__`):
```python
def test_render_hyprlock_colors():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
out = gen.render_hyprlock_colors(pal)
assert "$tn_fg = rgba(c5c8c6ff)" in out
assert "$tn_fg_dim = rgba(969896ff)" in out
assert "$tn_bg_alt = rgba(282a2eff)" in out
assert "$tn_blue = rgba(81a2beff)" in out
assert "$tn_red = rgba(cc6666ff)" in out
assert "$tn_green = rgba(b5bd68ff)" in out
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: FAIL — `AttributeError: module 'gen' has no attribute 'render_hyprlock_colors'`
- [ ] **Step 3: Write minimal implementation**
Add to `theme/generate-theme.py`. The `_HYPRLOCK_MAP` covers exactly the colors hyprlock.conf needs:
```python
# (colors.json key) -> (hyprlock variable name)
_HYPRLOCK_MAP = [
("foreground", "tn_fg"), ("foreground_dim", "tn_fg_dim"),
("background_alt", "tn_bg_alt"), ("blue", "tn_blue"),
("red", "tn_red"), ("green", "tn_green"),
]
def render_hyprlock_colors(pal):
lines = ["# GENERATED from colors.json by generate-theme.py. DO NOT EDIT."]
for key, var in _HYPRLOCK_MAP:
lines.append(f"${var} = {hex_to_hyprlock_rgba(pal[key])}")
return "\n".join(lines) + "\n"
def main():
here = os.path.dirname(os.path.abspath(__file__))
pal = load_palette(os.path.join(here, "colors.json"))
cfgdir = os.path.expanduser("~/.config")
os.makedirs(os.path.join(cfgdir, "theme"), exist_ok=True)
os.makedirs(os.path.join(cfgdir, "mako"), exist_ok=True)
writes = {
os.path.join(cfgdir, "theme", "colors.css"): render_colors_css(pal),
os.path.join(cfgdir, "mako", "config"): render_mako_config(pal),
os.path.join(cfgdir, "theme", "hyprlock-colors.conf"): render_hyprlock_colors(pal),
}
for path, content in writes.items():
with open(path, "w") as f:
f.write(content)
print(f" generated {path}")
if __name__ == "__main__":
main()
```
- [ ] **Step 4: Run test to verify it passes**
Run: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py`
Expected: `OK`
- [ ] **Step 5: Verify the writer produces a colors.css matching the current one**
Run:
```bash
cd ~/Documents/dotfiles && python3 theme/generate-theme.py
diff <(grep '@define-color' theme/colors.css) <(grep '@define-color' ~/.config/theme/colors.css)
```
Expected: no differences (the generated palette matches the committed hand-written one). If `~/.config/theme/colors.css` was a symlink to the repo file, `diff` shows nothing because they are the same file — that is fine; the symlink is removed in Task 6.
- [ ] **Step 6: Commit**
```bash
git add theme/generate-theme.py theme/test_generate_theme.py
git commit -m "feat(theme): render hyprlock colors + main() writer"
```
---
## Task 5: Convert hyprlock.conf to the shared palette
**Files:**
- Modify: `hyprlock/hyprlock.conf` (add `source=` line; replace 7 `rgba(...)` at lines 27, 38, 5054)
- [ ] **Step 1: Add the source line at the top of `hyprlock/hyprlock.conf`**
Insert as the very first line (before `general {`):
```
source = ~/.config/theme/hyprlock-colors.conf
```
- [ ] **Step 2: Replace the hardcoded colors with variables**
Apply these exact replacements in `hyprlock/hyprlock.conf`:
| Line context | From | To |
|---|---|---|
| label (clock) | `color = rgba(c5c8c6ff)` | `color = $tn_fg` |
| label (date) | `color = rgba(969896ff)` | `color = $tn_fg_dim` |
| input-field | `inner_color = rgba(282a2eff)` | `inner_color = $tn_bg_alt` |
| input-field | `outer_color = rgba(81a2beff)` | `outer_color = $tn_blue` |
| input-field | `font_color = rgba(c5c8c6ff)` | `font_color = $tn_fg` |
| input-field | `fail_color = rgba(cc6666ff)` | `fail_color = $tn_red` |
| input-field | `check_color = rgba(b5bd68ff)` | `check_color = $tn_green` |
- [ ] **Step 3: Generate the hyprlock color file and parse-test hyprlock**
Run:
```bash
cd ~/Documents/dotfiles && python3 theme/generate-theme.py
cat ~/.config/theme/hyprlock-colors.conf
# Parse test: launch with no grace, watch for parse errors, kill after 2s before interacting.
timeout 2 hyprlock --verbose 2>&1 | grep -iE 'error|invalid|fail to parse|unknown' || echo "NO PARSE ERRORS"
```
Expected: the color file lists the 6 `$tn_*` variables; the parse test prints `NO PARSE ERRORS`. (hyprlock will lock the screen for ~2s — unlock with your password/fingerprint after `timeout` kills it. Do this when you can attend the screen.)
- [ ] **Step 4: FALLBACK (only if Step 3 shows parse errors for `source=` or `$variables`)**
If hyprlock v0.9.5 rejects `source=`/variables, abandon the source approach for hyprlock:
1. Revert `hyprlock/hyprlock.conf` changes: `git checkout hyprlock/hyprlock.conf`
2. Rename it to a template: `git mv hyprlock/hyprlock.conf hyprlock/hyprlock.conf.tmpl`
3. In `hyprlock.conf.tmpl`, replace the 7 `rgba(...)` values with `__TN_FG__`, `__TN_FG_DIM__`, `__TN_BG_ALT__`, `__TN_BLUE__`, `__TN_RED__`, `__TN_GREEN__` placeholders.
4. Add a `render_hyprlock_conf(pal, template_str)` function to the generator that `.replace()`s each placeholder with `hex_to_hyprlock_rgba(pal[key])`, plus a test asserting no `__` placeholders remain and `rgba(c5c8c6ff)` is present.
5. In `main()`, read the template and write the rendered file to `~/.config/hypr/hyprlock.conf`.
6. In `install.sh` (Task 6), remove the `ln -sf .../hyprlock/hyprlock.conf` symlink (now generated).
Document in the commit message which path was taken.
- [ ] **Step 5: Commit**
```bash
git add hyprlock/hyprlock.conf
git commit -m "feat(theme): hyprlock uses shared palette via sourced variables"
```
---
## Task 6: Wire generator into install.sh; remove colors.css from repo
**Files:**
- Modify: `install.sh` (replace lines 3242 inline python; remove line 29 colors.css symlink)
- Delete: `theme/colors.css`
- [ ] **Step 1: Remove the inline mako python block and the colors.css symlink**
In `install.sh`, delete the colors.css symlink line:
```bash
ln -sf "$(pwd)/theme/colors.css" ~/.config/theme/colors.css
```
and delete the entire inline block:
```bash
python3 - <<'PYEOF'
import json, os
c = json.load(open("theme/colors.json"))
config = f"""background-color={c['background']}
text-color={c['foreground']}
border-size=2
border-color={c['blue']}
default-timeout=4000
"""
open(os.path.expanduser("~/.config/mako/config"), "w").write(config)
PYEOF
```
- [ ] **Step 2: Add the generator call**
In `install.sh`, after the `mkdir -p ~/.config/theme` line (and after the wofi/waybar symlinks), add:
```bash
echo "==> Generating theme artifacts from colors.json"
python3 "$(pwd)/theme/generate-theme.py"
```
- [ ] **Step 3: Delete the now-generated colors.css from the repo**
```bash
cd ~/Documents/dotfiles && git rm theme/colors.css
# Remove any stale symlink so generation writes a real file:
rm -f ~/.config/theme/colors.css
```
- [ ] **Step 4: Run install.sh's theming path and verify all artifacts regenerate**
Run:
```bash
cd ~/Documents/dotfiles && python3 theme/generate-theme.py
test -f ~/.config/theme/colors.css && echo "colors.css OK"
test -f ~/.config/mako/config && echo "mako OK"
test -f ~/.config/theme/hyprlock-colors.conf && echo "hyprlock-colors OK"
grep -q '@define-color tn-blue #81a2be;' ~/.config/theme/colors.css && echo "palette OK"
waybar-restart
```
Expected: all four `OK` lines print, and waybar restarts with colors intact (no GTK CSS parse errors in its output).
- [ ] **Step 5: Commit**
```bash
git add install.sh
git rm --cached theme/colors.css 2>/dev/null || true
git commit -m "build(theme): generate all theme artifacts from colors.json in install.sh"
```
---
## Task 7: Bluetooth waybar module
**Files:**
- Modify: `waybar/config.jsonc` (add `bluetooth` to `modules-right`; add module block)
- Modify: `waybar/style.css` (add `#bluetooth` rules)
- Modify: `packages.txt` (ensure `bluez`, `bluez-utils`)
- [ ] **Step 1: Ensure bluez packages are present**
In `packages.txt`, confirm/add (alongside the existing `blueman`):
```
bluez
bluez-utils
```
- [ ] **Step 2: Ensure the bluetooth service is running (one-time, host setup)**
Run:
```bash
systemctl is-enabled bluetooth.service 2>/dev/null || sudo systemctl enable --now bluetooth.service
systemctl is-active bluetooth.service
```
Expected: `active`. (The waybar module reads adapter state from the bluez daemon.)
- [ ] **Step 3: Add the bluetooth module to waybar config**
In `waybar/config.jsonc`, add `"bluetooth"` to `modules-right` immediately after `"network"`:
```jsonc
"network",
"bluetooth",
"battery",
```
And add this module block (e.g. after the `network` block):
```jsonc
"bluetooth": {
"format": " ",
"format-disabled": "",
"format-off": "",
"format-connected": " {num_connections}",
"tooltip-format": "{controller_alias}\n{status}",
"tooltip-format-connected": "{controller_alias}\n{num_connections} connected\n{device_enumerate}",
"tooltip-format-enumerate-connected": "{device_alias}",
"on-click": "blueman-manager",
},
```
- [ ] **Step 4: Add bluetooth styling**
In `waybar/style.css`, add `#bluetooth` to the shared padding selector list and add state colors:
```css
#bluetooth {
padding: 0 12px;
}
#bluetooth.connected {
color: @tn-blue;
}
#bluetooth.disabled, #bluetooth.off {
color: @tn-fg-muted;
}
```
- [ ] **Step 5: Verify the module renders**
Run:
```bash
waybar-restart
```
Expected: a bluetooth glyph appears between network and battery; clicking it opens `blueman-manager`; toggling bluetooth changes the glyph/color. No waybar errors in output.
- [ ] **Step 6: Commit**
```bash
git add waybar/config.jsonc waybar/style.css packages.txt
git commit -m "feat(waybar): add bluetooth module"
```
---
## Final Verification
- [ ] Run the generator test suite: `cd ~/Documents/dotfiles && python3 theme/test_generate_theme.py``OK`
- [ ] Re-run generation and confirm idempotency: `python3 theme/generate-theme.py` twice → identical files, no errors.
- [ ] `waybar-restart` → bar renders with correct colors + bluetooth module.
- [ ] Open wofi (Mod+D) → styling intact.
- [ ] Trigger a notification → mako uses palette colors.
- [ ] Lock screen (Mod+Shift+E) → hyprlock colors correct; unlock works.
- [ ] `grep -rn 'rgba(' hyprlock/hyprlock.conf` → no hardcoded colors remain (only `$tn_*` vars), unless the Task 5 fallback template path was taken.
- [ ] Confirm `theme/colors.css` no longer exists in the repo: `git ls-files theme/colors.css` → empty.
@@ -0,0 +1,133 @@
# NetworkManager Activation Design
**Date:** 2026-05-13
**Status:** Approved
## Summary
Activate NetworkManager (already installed but disabled) and configure it to use `iwd` as its wifi backend. Disables `systemd-networkd` (the current connection manager) to avoid conflicts. Re-enables `nm-applet` and `networkmanager_dmenu` paths that were dead because NM wasn't running.
## Current Behaviour
- `iwd` is running — manages wifi at the protocol level
- `systemd-networkd` is running — handles DHCP and link config
- `NetworkManager.service` is installed but **disabled** (inactive/dead)
- `nm-applet` runs at no-op (no dbus peer to connect to)
- `networkmanager_dmenu` (waybar `network` module's on-click) errors with "NetworkManager is not running"
End result: connectivity works, but the entire NM-based UI ecosystem (applet, dmenu picker, nmcli) is non-functional.
## Target Behaviour
- `iwd` continues running, now as NM's wifi backend
- `NetworkManager` runs as the connection manager
- `systemd-networkd` stopped and disabled
- `nm-applet` shows connection state in the waybar tray (works because the SNI watcher chain works)
- `networkmanager_dmenu` opens a wofi-style picker that lists/connects to known networks
- Existing wifi profiles preserved (NM reads iwd's known-networks via the backend interface)
## Implementation
### Tracked dotfile
**`NetworkManager/conf.d/wifi-backend.conf`** (new file in repo, deployed to `/etc/NetworkManager/conf.d/`):
```ini
[device]
wifi.backend=iwd
```
This is the only file NM needs. It tells NM to delegate wifi to iwd (which is already running) instead of spawning its own `wpa_supplicant`. Modern, well-supported configuration on Arch.
### `install.sh`
Append one block in the existing system-deploy section (after the hibernate `sleep.conf.d` line):
```bash
sudo install -Dm644 "$(pwd)/NetworkManager/conf.d/wifi-backend.conf" \
/etc/NetworkManager/conf.d/wifi-backend.conf
```
`install -Dm644` creates `/etc/NetworkManager/conf.d/` if it doesn't exist (it usually does, since the NM package ships an empty stub) and sets mode 644.
### `packages.txt`
Re-add `network-manager-applet` (removed in commit `4303562` when nm-applet was non-functional). Insertion can go anywhere — alphabetical-ish near the existing `networkmanager`-related lines.
### `niri/config.kdl`
Re-add the spawn-at-startup line (removed in same earlier commit):
```kdl
spawn-at-startup "nm-applet" "--indicator"
```
Position: with the other applet/daemon spawns, alphabetical order doesn't matter.
### One-time service swap (manual, with sudo)
Documented in README, not scripted. Two commands run once:
```bash
sudo systemctl disable --now systemd-networkd
sudo systemctl enable --now NetworkManager
```
Why not script it: systemd unit state is persistent. Once flipped, it stays. Doesn't need to be in `install.sh` (which runs frequently). A one-time README instruction matches the existing pattern for `enable-hibernation.sh`.
### `README.md`
Append a subsection under `## Setup`, alongside the existing "One-time hibernation enablement":
```markdown
### One-time NetworkManager activation
After `install.sh` deploys the NM config, swap from systemd-networkd to NetworkManager (one-time, persistent across reboots):
\`\`\`bash
sudo systemctl disable --now systemd-networkd
sudo systemctl enable --now NetworkManager
\`\`\`
Wifi will drop for a few seconds and reconnect via iwd's stored profiles. If reconnection fails, re-enter the wifi password via `nm-applet` or `networkmanager_dmenu`.
```
## Files Touched
| File | Action |
|---|---|
| `NetworkManager/conf.d/wifi-backend.conf` | created |
| `install.sh` | append one `sudo install -Dm644` line |
| `packages.txt` | re-add `network-manager-applet` |
| `niri/config.kdl` | re-add `spawn-at-startup "nm-applet" "--indicator"` |
| `README.md` | append "One-time NetworkManager activation" subsection |
## Verification
After `install.sh` + the two systemctl commands:
```bash
systemctl is-active NetworkManager # active
systemctl is-active systemd-networkd # inactive
nmcli general status # prints connectivity status
nmcli -f WIFI-PROPERTIES.WPA general status # confirms iwd backend is alive
nmcli connection show --active # shows the wifi connection
busctl --user get-property org.kde.StatusNotifierWatcher \
/StatusNotifierWatcher org.kde.StatusNotifierWatcher RegisteredStatusNotifierItems
# should include nm-applet's tray entry
```
Behavioural checks:
1. Wifi stays connected (after a brief drop during the swap).
2. Click waybar's network widget — `networkmanager_dmenu` opens with available networks.
3. The nm-applet tray icon shows current connection state.
## Out of Scope
- **Migrating iwd profiles to NM-format files** — not needed; NM reads iwd's known-networks via the backend interface.
- **Removing `iwd`** — kept as the wifi backend.
- **Removing the `networkmanager` package** — already installed; we just enable the service.
- **DNS / dispatcher / split-tunnel / VPN config** — beyond scope; default NM behaviour is fine.
- **Wired-only fallback** — out of scope; NM handles ethernet automatically when present.
- **Deleting the obsolete `nm-applet` removal commit from history** — keep history honest.
- **Auto-running the systemctl commands from `install.sh`** — out of scope; service-state changes are one-time and persistent.
@@ -0,0 +1,105 @@
# Framework 13 Fingerprint Reader Design
**Date:** 2026-05-25
**Status:** Approved
## Summary
Enable the Framework 13 (AMD 7040) Goodix fingerprint reader for `sudo` and `gtklock` (lockscreen). Three-phase setup: package install via the existing `packages.txt` manifest, interactive enrollment (one-time, user-driven), and a small idempotent bootstrap script that adds `pam_fprintd.so` to the two PAM stacks if not already present.
## Current State
- **Hardware:** Goodix `27c6:609c` USB fingerprint reader (well-supported by libfprint's `goodix.MOC` driver)
- **Software:** `fprintd` and `libfprint` are NOT installed
- **PAM:** No `pam_fprintd.so` line in any `/etc/pam.d/` file
- **Enrollments:** none
## Target State
After install + enrollment + bootstrap script:
| Service | Behaviour |
|---|---|
| `sudo` | Prompt offers fingerprint first; falls back to password if it fails / times out |
| `gtklock` (lockscreen) | Same — touch the reader to unlock; type password if it fails |
| `login`, `system-local-login`, `polkit-1` | Untouched — password only (boot-time fingerprint is finicky; polkit-1 has no existing PAM file) |
## Implementation
### Phase 1: Package install
Add to `packages.txt`:
```
fprintd
```
`libfprint` is pulled in as a dependency. `fprintd.service` auto-starts via dbus activation — no `systemctl enable` needed.
### Phase 2: Enrollment (interactive, user-driven)
After install, the user runs:
```
fprintd-enroll
```
Default enrolls right-index. Additional fingers via `fprintd-enroll -f left-thumb` (or `right-thumb`, `left-index`, etc.). Verify with `fprintd-list $USER`.
This step is interactive and not scriptable — documented in the README.
### Phase 3: PAM wiring via bootstrap script
**`scripts/enable-fingerprint.sh`** — new, idempotent, requires sudo, run once.
For each of `/etc/pam.d/sudo` and `/etc/pam.d/gtklock`, inserts this exact line at the top of the auth stack (before the first existing `auth` directive), if not already present:
```
auth sufficient pam_fprintd.so
```
`sufficient` means: fingerprint success → skip password; fingerprint missing/failure → fall through to the existing password auth. Never blocks login on fingerprint failure.
The script:
1. Checks each target file for `pam_fprintd` (already present → skip with message)
2. Backs up the file to `<file>.bak.<unix-ts>` before editing
3. Inserts the new line on a fresh line directly before the first `auth ` line
4. Prints what changed
Why a script vs tracking the PAM files in the repo: `/etc/pam.d/sudo` is owned by the `sudo` pacman package and `/etc/pam.d/gtklock` by `gtklock`. Tracking modified copies and `sudo install`-ing them via `install.sh` would clobber pacman updates of those packages. An idempotent one-time edit is the cleanest path.
### Phase 4: README documentation
Append a `### One-time fingerprint setup` subsection under `## Setup`, alongside the existing hibernation + NetworkManager subsections. Three steps documented:
1. Run `install.sh` (gets `fprintd` installed via packages.txt)
2. Enroll a finger: `fprintd-enroll`
3. Wire up PAM: `sudo bash scripts/enable-fingerprint.sh`
## Files Touched
| File | Action |
|---|---|
| `packages.txt` | add `fprintd` |
| `scripts/enable-fingerprint.sh` | created |
| `README.md` | append `### One-time fingerprint setup` subsection |
## Verification
After all three phases:
```
fprintd-list $USER # lists enrolled fingers
fprintd-verify # touch reader → "verify-match"
grep pam_fprintd /etc/pam.d/sudo /etc/pam.d/gtklock # both have the line
```
Behavioural checks:
1. Run `sudo true` — prompts for fingerprint first; if you touch the reader → no password requested.
2. Lock screen (`Mod+Shift+E`) → touch reader → unlocks without typing password.
3. Disconnect the fingerprint reader (theoretically; you can't physically with a built-in one — instead `pkill fprintd; sudo systemctl mask fprintd` temporarily): both services should still accept the password, proving the fallback works. Restore with `sudo systemctl unmask fprintd`.
## Out of Scope
- **Tracking PAM files in the repo** — would clobber pacman-owned files. Bootstrap script edits in place instead.
- **Adding fingerprint to login/system-local-login/polkit-1** — boot-time fingerprint is finicky (USB device may not be ready); polkit-1 has no existing PAM file. Easy follow-up if desired.
- **Automatic enrollment** — `fprintd-enroll` is inherently interactive.
- **Custom enrollment of multiple fingers** — user runs `fprintd-enroll -f <finger>` as needed.
- **Removing fingerprint from PAM** — manual `sudo sed -i '/pam_fprintd/d' /etc/pam.d/sudo /etc/pam.d/gtklock` reverses it; not scripted.
- **Backwards-compatible PAM stack** that survives package updates of `sudo`/`gtklock`. Acceptable risk: pacman package updates of these are rare; if it happens, the script is idempotent — run it again to re-add the line.
@@ -0,0 +1,175 @@
# Replace gtklock with hyprlock Design
**Date:** 2026-05-25
**Status:** Approved
## Summary
Migrate the lockscreen from `gtklock` to `hyprlock`. Hyprlock has native fprintd polling: touching the reader on the lockscreen unlocks immediately, without the user having to first type / press Enter to trigger PAM (the limitation that motivated this change).
## Current Behaviour
- `gtklock` is the lockscreen, bound to `Mod+Shift+E` and to swayidle's 10-minute lock + before-sleep hooks.
- `gtklock/style.css` (72 lines) + `gtklock/config.ini` are tracked and symlinked to `~/.config/gtklock/`.
- `/etc/pam.d/gtklock` has `auth sufficient pam_fprintd.so` (from `scripts/enable-fingerprint.sh`).
- Pain point: gtklock doesn't poll fprintd until PAM is invoked, which only happens once the user submits something in the password field. Touching the reader on a freshly-locked screen does nothing.
## Target Behaviour
- `hyprlock` is the lockscreen, bound to the same keybind, called by the same swayidle hooks.
- Touch the reader on the locked screen → unlocks immediately.
- Type the password and press Enter → also unlocks (fallback).
- Visual: centred 96 px clock, smaller date below, password input field with placeholder text, blurred-screenshot background. Colors map the Tomorrow Night palette.
## Implementation
### `hyprlock/hyprlock.conf` (new)
Single Hyprlang file, deployed to `~/.config/hypr/hyprlock.conf`:
```hyprlang
general {
grace = 0
hide_cursor = true
}
background {
monitor =
path = screenshot
blur_passes = 3
blur_size = 8
}
label {
monitor =
text = cmd[update:1000] date +"%-I:%M %p"
font_size = 96
color = rgba(c5c8c6ff)
position = 0, 150
halign = center
valign = center
}
label {
monitor =
text = cmd[update:60000] date +"%A, %B %-d"
font_size = 24
color = rgba(969896ff)
position = 0, 40
halign = center
valign = center
}
input-field {
monitor =
size = 300, 50
outline_thickness = 2
dots_size = 0.25
dots_spacing = 0.4
inner_color = rgba(282a2eff)
outer_color = rgba(81a2beff)
font_color = rgba(c5c8c6ff)
fail_color = rgba(cc6666ff)
check_color = rgba(b5bd68ff)
placeholder_text = <i>Touch fingerprint or type password</i>
fail_text = <i>$FAIL</i>
fade_on_empty = false
position = 0, -50
halign = center
valign = center
}
```
Color mapping:
- `c5c8c6` = `@tn-fg` (clock text, input font)
- `969896` = `@tn-fg-dim` (date label)
- `282a2e` = `@tn-bg-alt` (input fill)
- `81a2be` = `@tn-blue` (focus ring)
- `cc6666` = `@tn-red` (auth failure)
- `b5bd68` = `@tn-green` (auth success)
### `packages.txt`
Swap `gtklock``hyprlock`. Drop the AUR `gtklock-userinfo-module` entry too (not used in the new config). `hyprlock` is in the `extra` repo — no AUR helper needed for it.
### `install.sh`
Remove the two gtklock symlink lines:
```bash
ln -sf "$(pwd)/gtklock/config.ini" ~/.config/gtklock/config
ln -sf "$(pwd)/gtklock/style.css" ~/.config/gtklock/style.css
```
Replace with:
```bash
mkdir -p ~/.config/hypr
ln -sf "$(pwd)/hyprlock/hyprlock.conf" ~/.config/hypr/hyprlock.conf
```
Also drop the `gtklock` entry from the brace-expansion `mkdir -p` near the top of install.sh.
### `niri/config.kdl`
Three changes:
1. The keybind `Mod+Shift+E { spawn "gtklock" "-d"; }``Mod+Shift+E { spawn "hyprlock"; }`
2. Swayidle's 600 s timer action `"gtklock" "-d"``"hyprlock"`
3. Swayidle's `before-sleep` action `"gtklock" "-d"``"hyprlock"`
`hyprlock` takes no `-d` flag — it daemonises by default.
### `scripts/enable-fingerprint.sh`
Change `TARGETS=(/etc/pam.d/sudo /etc/pam.d/gtklock)``TARGETS=(/etc/pam.d/sudo /etc/pam.d/hyprlock)`. The script remains idempotent for the new target. Existing `/etc/pam.d/gtklock` pam_fprintd line stays untouched (harmless dead config; the user can `sed -i '/pam_fprintd/d' /etc/pam.d/gtklock` if they want).
### `gtklock/` (deletion)
Delete the tracked directory: `gtklock/config.ini` + `gtklock/style.css`. The packages get uninstalled (or remain installed, user's call); the tracked config files are gone.
### Documentation updates
- `README.md`: change Tooling table row for Lockscreen from `gtklock` to `hyprlock`. Update the `### One-time fingerprint setup` subsection's reference to gtklock's lockscreen (test instruction mentions `Mod+Shift+E`, still valid).
- `ARCHITECTURE.md`: same table row change.
- `CLAUDE.md`: replace "gtklock" mentions with "hyprlock" in the Theming + Tooling sections.
### Theme
The repo's `theme/colors.css` is the source of truth for GTK CSS variables. Hyprlock doesn't use CSS — colors are hand-mapped in `hyprlock.conf`. Not a regression in DRY (the values map 1:1 to entries in `theme/colors.json`); no new color source created.
## Files Touched
| File | Action |
|---|---|
| `hyprlock/hyprlock.conf` | created |
| `packages.txt` | swap `gtklock``hyprlock`; drop `gtklock-userinfo-module` if present |
| `install.sh` | swap symlinks (and mkdir entry) |
| `niri/config.kdl` | swap 3 gtklock invocations to hyprlock |
| `scripts/enable-fingerprint.sh` | swap target in `TARGETS` array |
| `gtklock/config.ini` | deleted |
| `gtklock/style.css` | deleted |
| `README.md` | update lockscreen reference |
| `ARCHITECTURE.md` | update lockscreen reference |
| `CLAUDE.md` | update lockscreen reference |
## Manual user steps after merge
1. `bash install.sh` — installs hyprlock, deploys config, replaces symlinks.
2. `sudo bash scripts/enable-fingerprint.sh` — re-runs idempotently; adds `pam_fprintd.so` to the new `/etc/pam.d/hyprlock`.
3. Test: `Mod+Shift+E` to lock → touch reader → unlocks immediately.
4. Optional cleanup: `sudo pacman -Rns gtklock gtklock-userinfo-module` if you want the old package gone.
## Verification
- `which hyprlock` returns `/usr/bin/hyprlock`
- `grep pam_fprintd /etc/pam.d/hyprlock` shows the line
- `hyprlock` (run from terminal) launches the lockscreen with clock, date, input field; Ctrl-C in the terminal won't kill it (it's its own GPU surface); use the password to unlock.
- Touching the reader unlocks without typing anything first.
## Out of Scope
- **Porting gtklock's CSS layout / theming verbatim.** Hyprlock doesn't use CSS. The new config achieves a comparable look via the palette mapping; further visual polish (avatars, music controls, last-login time) is a future incremental edit to `hyprlock.conf`.
- **Migrating swayidle's behaviour beyond changing the lock command.** Timers stay (300/600/1800 s).
- **Cleaning up `/etc/pam.d/gtklock`** — the existing pam_fprintd line in that file becomes harmless dead config since gtklock won't be launched. Manual cleanup if desired.
- **Uninstalling the `gtklock` system package** — out of scope; user decides.
- **Hyprlock styling beyond colors + clock + input field** — keep it minimal.
- **Solid-color background fallback** if the blurred screenshot renders poorly — easy follow-up edit, not part of this spec.
- **Custom font selection** — uses system default (likely JetBrainsMono Nerd Font since it's installed).
@@ -0,0 +1,141 @@
# Theme Centralization & Bluetooth Module — Design
Date: 2026-06-07
Status: Approved (pending spec review)
## Summary
Two independent enhancements to the niri dotfiles:
1. **Theme centralization** — collapse the duplicated color palette into a single
source of truth (`theme/colors.json`) and generate all derived configs from it
at install time. Bring `hyprlock` under the shared palette. Leave `alacritty`
untouched (out of scope).
2. **Bluetooth module** — add a waybar bluetooth indicator using the
already-installed `blueman`/`bluez` stack.
## Motivation
`CLAUDE.md` mandates a single source of truth and warns against duplicating
configs / hardcoding colors in multiple places. Current state violates this:
- `theme/colors.css` (`@define-color` palette) and `theme/colors.json` (same
palette as JSON) **both** hand-maintain the same 12 colors. Changing a color
requires editing both files.
- `hyprlock/hyprlock.conf` hardcodes 7 `rgba(...)` colors, every one of which
already exists in the palette.
Already-correct (not changed by this work):
- waybar (`waybar/style.css`) and wofi (`wofi/style.css`) `@import` `colors.css`.
- mako config is generated from `colors.json` by `install.sh`.
## Scope
**In scope:** unify the two palette files into one source; generate `colors.css`,
the mako config, and a hyprlock color file from it; convert `hyprlock.conf` to
reference the shared palette; add a waybar bluetooth module.
**Out of scope:** alacritty terminal colors (needs a full 16-color normal+bright
set the palette does not currently model — deferred); any change to the
Tomorrow Night palette values themselves; GTK theming (handled by Materia).
## Approach
Chosen approach **A**: `theme/colors.json` is the single source of truth;
everything else is generated at install time. This extends the existing Python
generation block already in `install.sh` (which generates the mako config).
Rejected: parsing `colors.css` as the source (fragile regex), and introducing a
new neutral source format (YAGNI for this scope, discards working JSON+Python).
## Detailed Design
### 1. Single source of truth
`theme/colors.json` remains the only hand-edited palette file. It holds the 12
base colors (already present). The hand-maintained `theme/colors.css` is
**deleted from the repo**; it becomes a generated artifact.
### 2. Generator: `theme/generate-theme.py`
A new standalone script, invoked by `install.sh`, replacing the current inline
Python mako snippet. Reads `theme/colors.json` and writes three artifacts
directly into `~/.config` (written, not symlinked — matching how mako is handled
today):
- **`~/.config/theme/colors.css`** — `@define-color` block for all 12 base
colors, plus the three alpha variants (`tn-bg-a90`, `tn-bg-a95`, `tn-bg-a96`)
computed from the `background` color at opacities 0.90 / 0.95 / 0.96. This is
what waybar and wofi already `@import`; their files do **not** change.
- **`~/.config/mako/config`** — same output as the current inline snippet.
- **`~/.config/theme/hyprlock-colors.conf`** — hyprlang variable declarations,
one per needed color, e.g. `$tn_fg = rgba(c5c8c6ff)`. Hex from `colors.json` is
converted to hyprlock's `rgba(RRGGBBAA)` form with `AA = ff`.
The generator must be idempotent and safe to re-run (overwrites its outputs).
### 3. hyprlock.conf
- Add `source = ~/.config/theme/hyprlock-colors.conf` near the top.
- Replace the 7 hardcoded `rgba(...)` values with the matching `$tn_*`
variables. Color → variable mapping (from current values):
- `rgba(c5c8c6ff)``$tn_fg` (foreground)
- `rgba(969896ff)``$tn_fg_dim` (foreground_dim)
- `rgba(282a2eff)``$tn_bg_alt` (background_alt)
- `rgba(81a2beff)``$tn_blue` (blue)
- `rgba(cc6666ff)``$tn_red` (red)
- `rgba(b5bd68ff)``$tn_green` (green)
- `hyprlock.conf` stays a hand-edited, symlinked file; only its colors come from
the generated source file.
**Risk + fallback:** hyprlock v0.9.5 uses the hyprlang parser, which documents
support for `$variables` and `source=`. This is verified by documentation, not
empirically (no dry-run flag; launching hyprlock grabs the screen). The
implementation plan MUST include an explicit parse test (lock once and confirm
colors render, or launch with `--grace 0` and watch `--verbose` for parse
errors). **Fallback if `source=`/variables misbehave:** generate the entire
`hyprlock.conf` from a `hyprlock/hyprlock.conf.tmpl` template with color
placeholders, instead of using `source=`. Same single-source outcome, more
generation.
### 4. install.sh changes
- Remove the inline `python3 - <<'PYEOF' ... PYEOF` mako block.
- Add a call to `python3 theme/generate-theme.py` after configs are linked.
- Remove the `ln -sf .../theme/colors.css ~/.config/theme/colors.css` line
(colors.css is now generated into that path, not symlinked).
- Ensure `~/.config/theme` exists before generation (already created).
### 5. Bluetooth waybar module
- `waybar/config.jsonc`: add `"bluetooth"` to `modules-right` (placed next to
`network`), with a `bluetooth` module block:
- `format`, `format-connected`, `format-disabled` using Nerd Font glyphs.
- `on-click`: `blueman-manager`.
- `waybar/style.css`: add a `#bluetooth` padding rule (consistent with the other
right-side modules) plus a connected-state accent color using a palette
variable (e.g. `@tn-blue`).
- Dependencies: `blueman` + `bluez`/`bluez-utils` (blueman pulls bluez). Confirm
`packages.txt` lists what's needed; the waybar bluetooth module requires the
`bluez` daemon running.
## Testing / Verification
- **Generator:** run `python3 theme/generate-theme.py`; diff generated
`~/.config/theme/colors.css` against the previous hand-written file to confirm
identical palette + alpha variants. Confirm mako config output unchanged.
- **waybar/wofi:** restart waybar (`waybar-restart`), confirm no CSS errors and
colors render. Open wofi, confirm styling intact.
- **hyprlock:** parse test per the fallback note above; confirm lock screen
colors match before/after.
- **Bluetooth:** confirm module appears, reflects adapter state, and
`blueman-manager` opens on click.
- **Idempotency:** re-run `install.sh`; confirm no errors and stable output.
## Rollout Notes
- Generated files (`~/.config/theme/colors.css`, `~/.config/theme/hyprlock-colors.conf`)
are not committed and not symlinked — consistent with the existing mako
generation. The committed `theme/colors.css` is removed in this change.
- The two features (theme, bluetooth) are independent and can be implemented and
verified separately.
-3
View File
@@ -1,3 +0,0 @@
[main]
gtk-theme=Materia-dark
follow-focus=true
-72
View File
@@ -1,72 +0,0 @@
@import url("../theme/colors.css");
/* gtklock style - Tomorrow Night theme */
window {
background-color: @tn-bg;
}
#body {
background-color: transparent;
}
#clock-label {
color: @tn-fg;
font-family: "JetBrainsMono Nerd Font";
font-size: 48px;
font-weight: bold;
margin-bottom: 8px;
}
#date-label {
color: @tn-fg-dim;
font-family: "JetBrainsMono Nerd Font";
font-size: 16px;
margin-bottom: 24px;
}
#username-label {
color: @tn-fg;
font-family: "JetBrainsMono Nerd Font";
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
}
#input-box {
background-color: @tn-bg-alt;
border: 2px solid @tn-bg-high;
border-radius: 6px;
color: @tn-fg;
font-family: "JetBrainsMono Nerd Font";
font-size: 14px;
padding: 8px 12px;
min-width: 280px;
}
#input-box:focus {
border-color: @tn-blue;
}
#error-label {
color: @tn-red;
font-family: "JetBrainsMono Nerd Font";
font-size: 13px;
margin-top: 8px;
}
#unlock-button {
background-color: @tn-bg-high;
border: none;
border-radius: 6px;
color: @tn-fg;
font-family: "JetBrainsMono Nerd Font";
font-size: 14px;
padding: 8px 20px;
margin-top: 12px;
}
#unlock-button:hover {
background-color: @tn-blue;
color: @tn-bg;
}
+30
View File
@@ -0,0 +1,30 @@
general {
# Dedup: never stack a second hyprlock on top of an existing one
lock_cmd = pidof hyprlock || hyprlock
# Lock before the machine sleeps, restore monitors on wake
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = niri msg action power-on-monitors
# Honor idle inhibitors (video, meetings, presentations) instead of
# blanking/locking through them — this is the swayidle behavior we lost.
ignore_dbus_inhibit = false
ignore_systemd_inhibit = false
}
# 5 min: blank the monitors
listener {
timeout = 300
on-timeout = niri msg action power-off-monitors
on-resume = niri msg action power-on-monitors
}
# 10 min: lock (drives lock_cmd via logind)
listener {
timeout = 600
on-timeout = loginctl lock-session
}
# 30 min: suspend-then-hibernate
listener {
timeout = 1800
on-timeout = systemctl suspend-then-hibernate
}
+63
View File
@@ -0,0 +1,63 @@
source = ~/.config/theme/hyprlock-colors.conf
general {
grace = 0
hide_cursor = true
}
auth {
fingerprint {
enabled = true
ready_message = Touch reader to unlock
present_message = Scanning…
retry_delay = 250
}
}
background {
monitor =
path = screenshot
blur_passes = 3
blur_size = 8
}
label {
monitor =
text = cmd[update:1000] date +"%-I:%M %p"
font_family = JetBrains Mono Nerd Font
font_size = 96
color = $tn_fg
position = 0, 150
halign = center
valign = center
}
label {
monitor =
text = cmd[update:60000] date +"%A, %B %-d"
font_family = JetBrains Mono Nerd Font
font_size = 24
color = $tn_fg_dim
position = 0, 40
halign = center
valign = center
}
input-field {
monitor =
size = 300, 50
outline_thickness = 2
dots_size = 0.25
dots_spacing = 0.4
inner_color = $tn_bg_alt
outer_color = $tn_blue
font_color = $tn_fg
fail_color = $tn_red
check_color = $tn_green
placeholder_text = <i>Touch fingerprint or type password</i>
fail_text = <i>$FAIL</i>
fade_on_empty = false
position = 0, -50
halign = center
valign = center
}
+9 -15
View File
@@ -16,7 +16,7 @@ echo "==> Installing packages (using $AUR_HELPER)"
$AUR_HELPER -S --needed - < packages.txt
echo "==> Creating config directories"
mkdir -p ~/.config/{niri,waybar,wofi,mako,alacritty,gtk-3.0,gtk-4.0,gtklock,fish} ~/.config/environment.d ~/.config/xdg-desktop-portal
mkdir -p ~/.config/{niri,waybar,wofi,mako,alacritty,gtk-3.0,gtk-4.0,fish} ~/.config/environment.d ~/.config/xdg-desktop-portal
echo "==> Linking configs"
@@ -26,22 +26,13 @@ ln -sf "$(pwd)/niri/config.kdl" ~/.config/niri/config.kdl
ln -sf "$(pwd)/waybar/config.jsonc" ~/.config/waybar/config.jsonc
ln -sf "$(pwd)/waybar/style.css" ~/.config/waybar/style.css
mkdir -p ~/.config/theme
ln -sf "$(pwd)/theme/colors.css" ~/.config/theme/colors.css
echo "==> Generating theme artifacts from colors.json"
python3 "$(pwd)/theme/generate-theme.py"
ln -sf "$(pwd)/wofi/config" ~/.config/wofi/config
ln -sf "$(pwd)/wofi/style.css" ~/.config/wofi/style.css
python3 - <<'PYEOF'
import json, os
c = json.load(open("theme/colors.json"))
config = f"""background-color={c['background']}
text-color={c['foreground']}
border-size=2
border-color={c['blue']}
default-timeout=4000
"""
open(os.path.expanduser("~/.config/mako/config"), "w").write(config)
PYEOF
ln -sf "$(pwd)/gtklock/config.ini" ~/.config/gtklock/config
ln -sf "$(pwd)/gtklock/style.css" ~/.config/gtklock/style.css
mkdir -p ~/.config/hypr
ln -sf "$(pwd)/hyprlock/hyprlock.conf" ~/.config/hypr/hyprlock.conf
ln -sf "$(pwd)/hypridle/hypridle.conf" ~/.config/hypr/hypridle.conf
ln -sf "$(pwd)/fish/config.fish" ~/.config/fish/config.fish
ln -sf "$(pwd)/starship/starship.toml" ~/.config/starship.toml
ln -sf "$(pwd)/alacritty/alacritty.toml" ~/.config/alacritty/alacritty.toml
@@ -72,4 +63,7 @@ echo "==> Deploying suspend/hibernate config"
sudo install -Dm644 "$(pwd)/logind/lid.conf" /etc/systemd/logind.conf.d/lid.conf
sudo install -Dm644 "$(pwd)/sleep/hibernate-delay.conf" /etc/systemd/sleep.conf.d/hibernate-delay.conf
echo "==> Deploying NetworkManager config"
sudo install -Dm644 "$(pwd)/NetworkManager/conf.d/wifi-backend.conf" /etc/NetworkManager/conf.d/wifi-backend.conf
echo "==> Done. Start Niri with: niri-session"
+30 -2
View File
@@ -39,8 +39,9 @@ spawn-at-startup "sh" "-c" "systemctl --user import-environment XDG_CURRENT_DESK
spawn-at-startup "waybar"
spawn-at-startup "mako"
spawn-at-startup "awww-daemon"
spawn-at-startup "nm-applet" "--indicator"
spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1"
spawn-at-startup "swayidle" "-w" "timeout" "300" "niri msg action power-off-monitors" "timeout" "600" "gtklock" "-d" "timeout" "1800" "systemctl suspend-then-hibernate" "before-sleep" "gtklock" "-d"
spawn-at-startup "hypridle"
spawn-at-startup "wl-paste" "--watch" "cliphist" "store"
spawn-at-startup "wlsunset" "-l" "49.2" "-L" "-123.1"
spawn-at-startup "xwayland-satellite"
@@ -91,7 +92,7 @@ binds {
Mod+M { move-window-to-workspace "minimized"; }
Mod+Shift+M { focus-workspace "minimized"; }
Mod+Shift+E { spawn "gtklock" "-d"; }
Mod+Shift+E { spawn "hyprlock"; }
Mod+Shift+P { spawn "powermenu"; }
Mod+Shift+C { spawn "clipboard-picker"; }
Mod+Shift+X { quit; }
@@ -156,3 +157,30 @@ window-rule {
match app-id="^thunar$" title="^File Operation Progress$"
open-floating true
}
// Steam client + child windows: float and center (friends list, settings,
// game properties, dialogs). Games (steam_app_) are handled above, unaffected.
window-rule {
match app-id="^steam$"
open-floating true
}
// Steam main library window: float large (centered by niri)
window-rule {
match app-id="^steam$" title="^Steam$"
default-column-width { proportion 0.75; }
default-window-height { proportion 0.85; }
}
// Steam notification toasts: unobtrusive, bottom-right, no border/focus-ring
window-rule {
match app-id="^steam$" title="^notificationtoasts"
open-floating true
default-floating-position x=16 y=16 relative-to="bottom-right"
border {
off
}
focus-ring {
off
}
}
+7 -2
View File
@@ -10,10 +10,13 @@ swappy
wl-clipboard
brightnessctl
pamixer
gtklock
swayidle
hyprlock
hypridle
ttf-jetbrains-mono-nerd
polkit-gnome
fprintd
networkmanager
network-manager-applet
xdg-desktop-portal-wlr
thunar
greetd
@@ -21,6 +24,8 @@ greetd-regreet
cage
cliphist
blueman
bluez
bluez-utils
wlsunset
pavucontrol
nwg-look
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# enable-fingerprint.sh — wire pam_fprintd.so into sudo + hyprlock auth stacks.
# Idempotent: re-runs are no-ops once the line is present. Run with sudo.
# Prerequisite: fprintd installed and at least one finger enrolled
# (run `fprintd-enroll` as your user first).
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "Run as root: sudo bash $0" >&2
exit 1
fi
if ! command -v fprintd-enroll >/dev/null 2>&1; then
echo "ERROR: fprintd is not installed. Run install.sh first." >&2
exit 1
fi
PAM_LINE="auth sufficient pam_fprintd.so"
TARGETS=(/etc/pam.d/sudo /etc/pam.d/hyprlock /etc/pam.d/greetd)
for f in "${TARGETS[@]}"; do
if [ ! -f "$f" ]; then
echo "ERROR: $f does not exist. Is the corresponding package installed?" >&2
exit 1
fi
if grep -q "pam_fprintd" "$f"; then
echo "$f: pam_fprintd already present — skipping"
continue
fi
backup="$f.bak.$(date +%s)"
cp -a "$f" "$backup"
echo "$f: backed up to $backup"
# Insert PAM_LINE on a new line directly before the first `auth ` directive.
awk -v line="$PAM_LINE" '
!inserted && /^auth[[:space:]]/ { print line; inserted = 1 }
{ print }
' "$f" > "$f.new"
if ! grep -q "pam_fprintd" "$f.new"; then
echo "ERROR: failed to insert PAM line into $f (no auth directive found?). Restoring backup." >&2
rm -f "$f.new"
exit 1
fi
mv "$f.new" "$f"
chmod --reference="$backup" "$f"
echo "$f: inserted '$PAM_LINE'"
done
echo
echo "Done. Test with: sudo true (should prompt for fingerprint)"
echo "Or lock the screen (Mod+Shift+E) and touch the reader."
echo
echo "To revert: restore the .bak.<ts> files in /etc/pam.d/, or:"
echo " sudo sed -i '/pam_fprintd/d' ${TARGETS[*]}"
-20
View File
@@ -1,20 +0,0 @@
/* Tomorrow Night - shared color palette */
/* @import this file from waybar, wofi, gtklock CSS */
@define-color tn-bg #1d1f21;
@define-color tn-bg-alt #282a2e;
@define-color tn-bg-high #373b41;
@define-color tn-fg #c5c8c6;
@define-color tn-fg-dim #969896;
@define-color tn-fg-muted #707880;
@define-color tn-blue #81a2be;
@define-color tn-green #b5bd68;
@define-color tn-yellow #f0c674;
@define-color tn-red #cc6666;
@define-color tn-magenta #b294bb;
@define-color tn-cyan #8abeb7;
/* Alpha variants */
@define-color tn-bg-a90 rgba(29, 31, 33, 0.90);
@define-color tn-bg-a95 rgba(29, 31, 33, 0.95);
@define-color tn-bg-a96 rgba(29, 31, 33, 0.96);
+85
View File
@@ -0,0 +1,85 @@
# theme/generate-theme.py
"""Generate desktop config artifacts from theme/colors.json (single source of truth)."""
import json
import os
def load_palette(path):
with open(path) as f:
return json.load(f)
def hex_to_rgb(h):
h = h.lstrip("#")
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
def hex_to_hyprlock_rgba(h):
return f"rgba({h.lstrip('#')}ff)"
# (key in colors.json) -> (css variable suffix)
_CSS_MAP = [
("background", "bg"), ("background_alt", "bg-alt"), ("background_high", "bg-high"),
("foreground", "fg"), ("foreground_dim", "fg-dim"), ("foreground_muted", "fg-muted"),
("blue", "blue"), ("green", "green"), ("yellow", "yellow"),
("red", "red"), ("magenta", "magenta"), ("cyan", "cyan"),
]
_ALPHA_VARIANTS = [("a90", "0.90"), ("a95", "0.95"), ("a96", "0.96")]
def render_mako_config(pal):
return (
f"background-color={pal['background']}\n"
f"text-color={pal['foreground']}\n"
"border-size=2\n"
f"border-color={pal['blue']}\n"
"default-timeout=4000\n"
)
def render_colors_css(pal):
lines = ["/* Tomorrow Night - GENERATED from colors.json by generate-theme.py. DO NOT EDIT. */", ""]
for key, suffix in _CSS_MAP:
lines.append(f"@define-color tn-{suffix} {pal[key]};")
lines.append("")
r, g, b = hex_to_rgb(pal["background"])
for name, alpha in _ALPHA_VARIANTS:
lines.append(f"@define-color tn-bg-{name} rgba({r}, {g}, {b}, {alpha});")
return "\n".join(lines) + "\n"
# (colors.json key) -> (hyprlock variable name)
_HYPRLOCK_MAP = [
("foreground", "tn_fg"), ("foreground_dim", "tn_fg_dim"),
("background_alt", "tn_bg_alt"), ("blue", "tn_blue"),
("red", "tn_red"), ("green", "tn_green"),
]
def render_hyprlock_colors(pal):
lines = ["# GENERATED from colors.json by generate-theme.py. DO NOT EDIT."]
for key, var in _HYPRLOCK_MAP:
lines.append(f"${var} = {hex_to_hyprlock_rgba(pal[key])}")
return "\n".join(lines) + "\n"
def main():
here = os.path.dirname(os.path.abspath(__file__))
pal = load_palette(os.path.join(here, "colors.json"))
cfgdir = os.path.expanduser("~/.config")
os.makedirs(os.path.join(cfgdir, "theme"), exist_ok=True)
os.makedirs(os.path.join(cfgdir, "mako"), exist_ok=True)
writes = {
os.path.join(cfgdir, "theme", "colors.css"): render_colors_css(pal),
os.path.join(cfgdir, "mako", "config"): render_mako_config(pal),
os.path.join(cfgdir, "theme", "hyprlock-colors.conf"): render_hyprlock_colors(pal),
}
for path, content in writes.items():
with open(path, "w") as f:
f.write(content)
print(f" generated {path}")
if __name__ == "__main__":
main()
+75
View File
@@ -0,0 +1,75 @@
# theme/test_generate_theme.py
import importlib.util, os
_spec = importlib.util.spec_from_file_location(
"gen", os.path.join(os.path.dirname(__file__), "generate-theme.py"))
gen = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(gen)
def test_load_palette_reads_colors_json():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
assert pal["background"] == "#1d1f21"
assert pal["blue"] == "#81a2be"
def test_hex_to_hyprlock_rgba():
assert gen.hex_to_hyprlock_rgba("#81a2be") == "rgba(81a2beff)"
assert gen.hex_to_hyprlock_rgba("#1d1f21") == "rgba(1d1f21ff)"
def test_hex_to_rgb_tuple():
assert gen.hex_to_rgb("#1d1f21") == (29, 31, 33)
def test_render_colors_css():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
css = gen.render_colors_css(pal)
# base colors
assert "@define-color tn-bg #1d1f21;" in css
assert "@define-color tn-bg-alt #282a2e;" in css
assert "@define-color tn-bg-high #373b41;" in css
assert "@define-color tn-fg #c5c8c6;" in css
assert "@define-color tn-fg-dim #969896;" in css
assert "@define-color tn-fg-muted #707880;" in css
assert "@define-color tn-blue #81a2be;" in css
assert "@define-color tn-green #b5bd68;" in css
assert "@define-color tn-yellow #f0c674;" in css
assert "@define-color tn-red #cc6666;" in css
assert "@define-color tn-magenta #b294bb;" in css
assert "@define-color tn-cyan #8abeb7;" in css
# alpha variants computed from background (29,31,33)
assert "@define-color tn-bg-a90 rgba(29, 31, 33, 0.90);" in css
assert "@define-color tn-bg-a95 rgba(29, 31, 33, 0.95);" in css
assert "@define-color tn-bg-a96 rgba(29, 31, 33, 0.96);" in css
def test_render_mako_config():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
cfg = gen.render_mako_config(pal)
assert "background-color=#1d1f21" in cfg
assert "text-color=#c5c8c6" in cfg
assert "border-size=2" in cfg
assert "border-color=#81a2be" in cfg
assert "default-timeout=4000" in cfg
def test_render_hyprlock_colors():
pal = gen.load_palette(os.path.join(os.path.dirname(__file__), "colors.json"))
out = gen.render_hyprlock_colors(pal)
assert "$tn_fg = rgba(c5c8c6ff)" in out
assert "$tn_fg_dim = rgba(969896ff)" in out
assert "$tn_bg_alt = rgba(282a2eff)" in out
assert "$tn_blue = rgba(81a2beff)" in out
assert "$tn_red = rgba(cc6666ff)" in out
assert "$tn_green = rgba(b5bd68ff)" in out
if __name__ == "__main__":
test_load_palette_reads_colors_json()
test_hex_to_hyprlock_rgba()
test_hex_to_rgb_tuple()
test_render_colors_css()
test_render_mako_config()
test_render_hyprlock_colors()
print("OK")
+12 -5
View File
@@ -12,8 +12,8 @@
"custom/fan-profile",
"pulseaudio",
"network",
"bluetooth",
"battery",
"tray",
],
"cpu": {
"format": " {usage}%",
@@ -71,10 +71,6 @@
},
},
"tray": {
"spacing": 8,
},
"network": {
"format-wifi": " {essid}",
"format-ethernet": "󰈀",
@@ -82,6 +78,17 @@
"on-click": "networkmanager_dmenu",
},
"bluetooth": {
"format": " ",
"format-disabled": "",
"format-off": "",
"format-connected": " {num_connections}",
"tooltip-format": "{controller_alias}\n{status}",
"tooltip-format-connected": "{controller_alias}\n{num_connections} connected\n{device_enumerate}",
"tooltip-format-enumerate-connected": "{device_alias}",
"on-click": "blueman-manager",
},
"pulseaudio": {
"format": " {volume}%",
"format-muted": "",
+12
View File
@@ -56,3 +56,15 @@ window#waybar {
#temperature.critical {
color: @tn-red;
}
#bluetooth {
padding: 0 12px;
}
#bluetooth.connected {
color: @tn-blue;
}
#bluetooth.disabled, #bluetooth.off {
color: @tn-fg-muted;
}