Compare commits
29 Commits
996f11d333
...
6e26cc62aa
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e26cc62aa | |||
| 400d81ff73 | |||
| dc810ed2a6 | |||
| 88f2131bdd | |||
| 95d4bf612b | |||
| 961733e127 | |||
| 1e3adbc842 | |||
| 5305bb78c6 | |||
| 2ca47c02a4 | |||
| 28f6160b02 | |||
| 577c169bae | |||
| ee93bcb7e8 | |||
| 2fcfc62cdd | |||
| c3bcf19e97 | |||
| bc3333a82a | |||
| 467be569fd | |||
| d7ab4baf7e | |||
| 30a300014a | |||
| b34d4269af | |||
| 17272611ea | |||
| ede1399853 | |||
| 2cd28edf52 | |||
| 622b0ef8b8 | |||
| d5ad517673 | |||
| bfb30d0a4f | |||
| 33c11e328f | |||
| 9362a5ee1d | |||
| b57d134c2b | |||
| 101accc26b |
@@ -1,2 +1,3 @@
|
||||
nohup.out
|
||||
mako/config
|
||||
__pycache__/
|
||||
|
||||
+14
-5
@@ -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:
|
||||
|
||||
@@ -76,7 +76,7 @@ dotfiles/
|
||||
│ ├── starship/
|
||||
│ ├── wofi/
|
||||
│ ├── mako/
|
||||
│ ├── gtklock/
|
||||
│ ├── hyprlock/
|
||||
│ └── ...
|
||||
├── install.sh
|
||||
├── packages.txt
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[device]
|
||||
wifi.backend=iwd
|
||||
@@ -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 1–6; 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, 50–54)
|
||||
|
||||
- [ ] **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 32–42 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.
|
||||
@@ -1,3 +0,0 @@
|
||||
[main]
|
||||
gtk-theme=Materia-dark
|
||||
follow-focus=true
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Executable
+59
@@ -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[*]}"
|
||||
@@ -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);
|
||||
@@ -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()
|
||||
@@ -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
@@ -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": "",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user