[Sync] Lua Hyprland chain, licence cleanup

This commit is contained in:
Coja committed 2026-11-09 18:25:35 +01:00
1 parent 93b4f695ce
commit 47166d877f
183 files changed
+5614 -5835

No files matched your search

+8
View File
@@ -36,3 +36,11 @@ __pycache__/
# HyDE generates waybar's include list per machine, with ABSOLUTE $HOME paths — the committed copy
# carried fl's /home/anon into every gui host, where it resolved to nothing. Let each box write it.
**/waybar/includes/includes.json
# dconf binary db — if stow ever folds ~/.config/dconf into a dir symlink on a fresh
# host, the dconf daemon writes the user db next to the tracked profile, into the repo.
gui/.config/dconf/hyde_hyprland
# llama.cpp router bench ledgers — generated next to bench.py, per-machine (fl/lw/wm)
**/llamacpp/bench-results.md
**/llamacpp/bench-history.md
+62 -33
View File
@@ -13,7 +13,7 @@ common/ # shared everywhere (mirrors $HOME): fish, nvim, bat, btop, tmux, pi,
gui/ # GUI configs shared by graphical hosts: hypr, waybar, kitty, alacritty, mpv, zellij
wm/ # desktop: 4K monitors, NVIDIA, cloud LLM (duskadiy), work aliases
lw/ # laptop: no NVIDIA, roaming VPNs, network/LLM tools
fl/ # KDE Plasma box: a few Hyprland overrides + llamacpp, evremap
fl/ # KDE Plasma box (no Hyprland session): llamacpp, whisper, evremap
bin/ # tooling (not a stow package): install.sh companions
install.sh
```
@@ -22,6 +22,8 @@ A **headless** (terminal-only) machine has no overlay package — it runs `commo
headless with `touch ~/.config/dots-headless` (a machine-local, hostname-independent marker); it then
skips `gui` too, so the same terminal-only setup replicates across any number of machines. In other
words `common` *is* the headless baseline; `gui` and the host packages layer graphical machines on top.
Run it as a bare `./install.sh` — never `./install.sh common` (`common`/`gui` are layers, not hosts,
and install.sh refuses them).
### How it deploys
@@ -68,9 +70,8 @@ cd ~/.dots
`./install.sh -h` lists all options: `-n` dry-run, `-a` adopt (first run), `-D` unstow (revert),
`-h` help. GUI hosts also need **HyDE installed** first — see *HyDE dependency* below.
Once per machine afterwards: `bat cache --build` (registers the repo's custom bat themes —
without it every `bat` run warns `Unknown theme 'onedark'`), and `:Lazy restore` inside nvim
(`restore` obeys the shared `lazy-lock.json` pins; `sync`/`update` would rewrite them).
Once per machine afterwards: `:Lazy restore` inside nvim (`restore` obeys the shared
`lazy-lock.json` pins; `sync`/`update` would rewrite them).
nvim's treesitter (`main` branch) builds parsers via the `tree-sitter` CLI — `pacman -S
tree-sitter-cli` first, or `:TSInstallAll` fails with `ENOENT ... 'tree-sitter'`.
@@ -125,32 +126,40 @@ Because configs are symlinks into the repo, **you almost never run `install.sh`
| Difference | Mechanism |
|---|---|
| Monitors | `<h>/.config/hypr/monitors.conf` (sourced by the shared `hyprland.conf`) |
| NVIDIA | `wm/.config/hypr/nvidia.conf`, pulled in by wm's `host.conf` (fl/lw have no NVIDIA) |
| Monitors | `<h>/.config/hypr/host.lua` (`hl.monitor` + workspace pins; loaded by the shared `hyprland.lua`) |
| NVIDIA | the NVIDIA block in `wm/.config/hypr/host.lua` (lw has no NVIDIA) |
| Shell aliases | shared `config.fish` sources `~/.config/fish/host.fish` (per host) |
| Bash extras | shared `.bashrc` sources `~/.bashrc.host` (optional, per host) |
| fl's Hyprland | overrides ~9 hypr files (`hyprland.conf`, keybinds, windowrules, theme colors, monitors); inherits the rest from `gui` |
| Unique apps | in the owning host overlay (fl: llamacpp/evremap; lw: xremap) |
`aichat`, `pi`, and `opencode` share one config in `common` (LAN-first, with the cloud provider also
listed so any host can switch). `lw` carries a host overlay of all three that adds its local CPU
llama.cpp models — mirror edits between the common and `lw` copies. The opencode tree is documented
listed so any host can switch). `lw` and `wm` each carry a host overlay of all three that adds their
own llama.cpp models (lw: CPU; wm: the GTX 1080 Ti router, plus the duskadiy default) — mirror
edits between the common copy and both overlays. The opencode tree is documented
in `common/.config/opencode/README.md`.
## HyDE dependency (important for GUI hosts)
The Hyprland config is [HyDE](https://github.com/HyDE-Project/HyDE)-based. `gui/.config/hypr/*` is
your customization layer, but it **sources HyDE's framework**:
`source = ~/.local/share/hyde/hyprland.conf` — which defines `$mainMod` and other variables. So a GUI
host must have **HyDE installed**, or Hyprland fails to start (`source globbing error` +
`invalid mod $mainMod`).
The Hyprland config is [HyDE](https://github.com/HyDE-Project/HyDE)-based, since 2026-08-21 through
HyDE 26.x's **Lua chain** (live on wm since then and on lw since 2026-11; merged into `main`
2026-11-09 — see `LUA-PORT.md`). The repo pins `HYPRLAND_CONFIG` at
`~/.local/share/hypr/hyde.lua` in two files — `gui/.config/fish/conf.d/00-hypr-config.fish`
(SDDM/fish session path) and `gui/.config/uwsm/env-hyprland.d/10-dots.sh` (uwsm path) — and hyde.lua
loads HyDE's `lua/` modules, then the repo's override `gui/.config/hypr/hyprland.lua`, which pulls in
`dots_rules.lua` + `dots_binds.lua` (split out of it) and the per-host `host.lua`. So a GUI host must have **HyDE installed** — it ships `hyde.lua` and the
modules, both overwritten on HyDE updates; the dots layer lives only in `~/.config/hypr/*.lua`, which
survives them. `LUA-PORT.md` has the full map, flip recipe and rollback.
That file used to be generated-and-untracked, but **HyDE 26.x deploys neither it nor `hyde.conf`**,
so since the 2026-08-14 audit both are **tracked pins** in `gui/.local/share/hyde/` — the one
deliberate exception to the generated-files-are-never-tracked rule below, interim until the Lua port.
After pulling that audit, a machine must run **`./bin/hyde-materialize.sh` before `./install.sh`**:
the newly-untracked files leave dangling stow symlinks otherwise, and hypr/hyprlock treat a missing
`source =` as a hard parse error.
The legacy hyprlang chain (`gui/.config/hypr/*.conf`, plus the two `~/.local/share/hyde/
{hyprland,hyde}.conf` framework copies HyDE 26.x stopped shipping) was **retired 2026-09-10**
(`LUA-PORT.md` step 8) once the Lua soak closed — nothing reads a `.conf` in Lua mode. One line
survives: `gui/.local/share/hyde/hyde.conf` keeps `$default.FONT`, the fallback HyDE's rofi/menu
scripts resolve FONT through (`get_hyprConf`) when the theme's `hypr.theme` names no font. Rolling
back to hyprlang now means restoring those files from history (`git restore --source=bddbbf3 -- gui/.config/hypr wm/.config/hypr lw/.config/hypr gui/.local/share/hyde`), `./install.sh <host>`,
repointing the two pins and relogging; d851213 is the last hyprlang `main` (the port merged
2026-11-09). After pulling the 2026-08-14 audit, a machine must run
**`./bin/hyde-materialize.sh` before `./install.sh`**: the newly-untracked files leave dangling stow
symlinks otherwise, and hypr/hyprlock treat a missing `source =` as a hard parse error.
- Install/update HyDE with its own installer (`~/HyDE/Scripts/install.sh`); it also pulls the
required apps (`xdg-desktop-portal-hyprland`, `xdg-desktop-portal-gtk`, `hyprpolkitagent`).
@@ -172,8 +181,8 @@ track neither. HyDE-managed customization goes via:
**The custom cava rig is NOT HyDE-managed**: `layouts/custom.jsonc` + `layouts/custom.css` form a
self-contained two-bar config run by `wbar` (`gui/.local/bin/wbar`) as the supervised systemd
user unit `dots-waybar.service` (`Restart=always` — waybar 0.15.0 dies on every DP-2 link
drop/return, and as a bare process nothing relaunched it). The `exec-once` in `userprefs.conf`,
the Ctrl+Alt+W / Super+Shift+R binds and the `wbar` command all go through it; **never `killall
drop/return, and as a bare process nothing relaunched it). `hc.start.bar` in `hyprland.lua`, the
Ctrl+Alt+W / Super+Shift+R binds and the `wbar` command all go through it; **never `killall
waybar`** — systemd brings it back in seconds and a raw relaunch beside it means two bars
(`wbar toggle|restart|reload|stop`). **Never route it through `hyde-shell waybar --set`** — waybar.py would clobber the repo's files as above. HyDE only reads
from `layouts/`, which is what makes them safe to track. On a rig host, HyDE's own bar must be
@@ -181,10 +190,10 @@ disabled once, or **two bars appear at every login** — and the launcher differ
- current HyDE: systemd user unit `hyde-<XDG_SESSION_DESKTOP>-bar.service` (auto-restarting) —
`systemctl --user stop <unit>; systemctl --user mask <unit>`.
- older (pre-unit) HyDE, e.g. wm's: `exec-once = $start.BAR` in HyDE's generated `hyprland.conf`
— neutralized by `$start.BAR=` in `gui/.config/hypr/hyde.conf` (empty = unset; hyde.conf is
sourced before that exec-once). Current HyDE's `variables.conf` re-sets `$start.BAR`, so that
line stops working after an update — see HYDE-UPDATE.md step 4.
- older (pre-unit) HyDE: `exec-once = $start.BAR` in HyDE's generated `hyprland.conf` — the
hyprlang-era dots blanked it with `$start.BAR=` in `gui/.config/hypr/hyde.conf`; that file went
with the chain (LUA-PORT.md step 8), and under Lua the lever is `hc.start.bar` in
`hyprland.lua`, which launches the rig itself — see HYDE-UPDATE.md step 4.
Note: the Arch `waybar` build has **no native `cava`** module — the rig uses the script module
`custom/cava` running `cava-waybar.sh` (self-contained, no HyDE dependency).
@@ -225,11 +234,31 @@ The repo is GPL-3.0 (`LICENSE`). What that does and doesn't cover:
[HyDE](https://github.com/HyDE-Project/HyDE)** (and, further back, legacy Hyprdots). HyDE is
itself GPL-3.0, so the derivation and this repo's licence agree — no conflict, and the
Hyprdots-era ASCII banners in those files are left in place as attribution.
- **`common/.config/aichat/dark.tmTheme`** is *One Half Dark* by Son A. Pham, **MIT**. The
licence header at the top of that file is the attribution — don't strip it.
- **Colour themes**: the repo ships no third-party theme files, so it needs no notices file.
Each app gets its theme from somewhere else:
- **kitty**: HyDE renders `theme.conf` from the current HyDE theme. `kitty.conf` adds only
this repo's own background, foreground and tab bar.
- **aichat**: *One Half Dark* (MIT, by Son A. Pham) is downloaded from upstream at install
time by `bin/dots-themes-fetch.sh`, pinned by commit and checksum.
- **cava**: it writes its own shaders and themes.
- **zellij and bat**: their gruvbox-dark and OneHalfDark themes are built in.
The pi, opencode and alacritty themes are the repo's own files. They hold colour values from
the Catppuccin Mocha and SynthWave '84 palettes (both MIT, by Catppuccin and Robb Owen),
mapped onto each app's keys. Credit, gladly given: a set of colour values isn't a copy of
either licensed theme. Keep it that way: a theme file copied in brings its licence notice
with it.
- **`gui/.local/share/icons/dots-icons/`** ships only its `index.theme`. Its tray/status icons
are symlinks into each host's own *Tela-circle-dracula* (by vinceliuice, installed by HyDE),
made at install time by `bin/dots-icons-link.sh`. Tela-circle publishes no licence, so the
repo doesn't redistribute its SVGs.
- **`common/.config/fastfetch/pngs/`** holds project and distro marks (Arch, the Open Source
Initiative and Open Source Hardware logos, …). Trademarks aren't relicensable, so GPL-3 does
not extend to them; they are bundled as nominative use, the way most dotfiles repos do.
- **`gui/.config/hypr/hyprlock/assets/`** — the lock screen background and logo. EXIF is
stripped, so the files carry no provenance; confirm these are yours to redistribute before
publishing, or swap them for something you know the origin of.
Initiative and Open Source Hardware logos). Trademarks aren't relicensable, so GPL-3 does
not extend to them; they are bundled as nominative use, the way most dotfiles repos do. The
dmz.rs rabbit (`dmz-rabbit*.png`) and the decentrala.org network logo (`network*.png`) are
the community's own marks, used with permission. `neonom.png` is the repo owner's own render
of the Om glyph, offered as **CC0**.
- **`gui/.config/hypr/hyprlock/assets/`** — the lock screen background and logo are AI-generated
by the repo owner (February 2026). They sit outside the GPL-3 grant and are offered as **CC0**.
The keyhole in `background.jpg` is an AI-altered rendition of the Open Source Initiative mark,
used purely decoratively; no affiliation with or endorsement by the OSI is implied.
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# dots-icons-link.sh — fill the dots-icons overlay theme with symlinks into the host's own
# Tela-circle-dracula install (HyDE ships that icon theme on every GUI host).
#
# The overlay (gui/.local/share/icons/dots-icons/index.theme, searched before Tela) exists
# because Tela ships its flat tray/status icons only at fixed 16/22/24px. On a fractionally
# scaled monitor waybar asks for size×2 (44px), which falls through to Tela's coloured
# scalable/devices circles (the blue nm-applet blob). Serving Tela's 22/panel SVGs as
# Scalable keeps them flat at every size.
#
# Links, not copies: Tela-circle publishes no licence (2026-11-09), so the repo can't
# redistribute its SVGs. Each host links its own installed files instead.
#
# Usage: bin/dots-icons-link.sh [-n] (-n = dry run). Idempotent; ./install.sh runs it on
# GUI hosts. Exit 1 if Tela is missing or any icon could not be linked — the overlay then
# degrades gracefully (Inherits=Tela-circle-dracula), so callers treat that as a warning.
set -euo pipefail
DRY=0
[ "${1:-}" = "-n" ] && DRY=1
data="${XDG_DATA_HOME:-$HOME/.local/share}"
src="$data/icons/Tela-circle-dracula/22/panel"
dst="$data/icons/dots-icons/scalable/status"
icons=(
blueman-active blueman-disabled blueman-tray-active blueman-tray-disabled blueman-tray
nm-adhoc nm-device-wired-autoip nm-device-wired-secureoffline nm-device-wired-secure
nm-device-wired nm-device-wireless nm-device-wwan nm-mb-roam nm-nm-signal-00
nm-nm-signal-100 nm-nm-signal-25 nm-nm-signal-50 nm-nm-signal-75 nm-no-connection
nm-secure-lock nm-signal-00-secure nm-signal-00 nm-signal-0-secure nm-signal-0
nm-signal-100-secure nm-signal-100 nm-signal-25-secure nm-signal-25 nm-signal-50-secure
nm-signal-50 nm-signal-75-secure nm-signal-75 nm-tech-3g nm-tech-4g nm-tech-cdma-1x
nm-tech-edge nm-tech-evdo nm-tech-gprs nm-tech-hspa nm-tech-lte nm-tech-umts
nm-vpn-active-lock nm-vpn-lock nm-vpn-standalone-lock nm-wwan-tower
)
if [ ! -d "$src" ]; then
echo " dots-icons: $src missing — Tela-circle-dracula not installed; overlay left empty" >&2
exit 1
fi
[ "$DRY" = 1 ] || mkdir -p "$dst"
linked=0 ok=0 bad=0
for n in "${icons[@]}"; do
s="$src/$n.svg" d="$dst/$n.svg"
if [ ! -e "$s" ]; then
echo " dots-icons: $n.svg not in Tela-circle-dracula/22/panel (renamed upstream?)" >&2
bad=$((bad + 1))
elif [ -L "$d" ] && [ "$(readlink "$d")" = "$s" ]; then
ok=$((ok + 1))
elif [ -e "$d" ] && [ ! -L "$d" ]; then
echo " dots-icons: $d is a real file — left alone" >&2
bad=$((bad + 1))
else
[ "$DRY" = 1 ] || ln -sfn "$s" "$d"
linked=$((linked + 1))
fi
done
verb="linked"
[ "$DRY" = 1 ] && verb="would link"
echo " dots-icons: $verb $linked, $ok already linked, $bad problem(s)"
[ "$bad" -eq 0 ]
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# dots-themes-fetch.sh — download the third-party theme files this repo's configs use but the
# repo doesn't ship. Each host fetches its own copy from upstream, so the repo redistributes
# nothing and needs no licence notice. (Same idea as bin/dots-icons-link.sh, which links Tela's
# icons from the host instead of shipping them.)
#
# Every entry is pinned to an upstream commit plus a sha256, so a fetch is exactly the file
# that was reviewed. To update one: bump the commit in the URL, check the diff, then replace
# the checksum with `curl -fsSL <url> | sha256sum`.
#
# Usage: bin/dots-themes-fetch.sh [-n] (-n = dry run). Idempotent; ./install.sh runs it on
# every host. A destination that's already a real file is left alone, even if its content
# differs (a local edit wins). A symlink there (e.g. the stow link left dangling after the
# repo dropped the file) is replaced: mv swaps the link itself, never writes through it.
# Exit 1 if a download or checksum failed; the app then falls back to its built-in theme, so
# install.sh only warns.
set -euo pipefail
DRY=0
[ "${1:-}" = "-n" ] && DRY=1
cfg="${XDG_CONFIG_HOME:-$HOME/.config}"
# dest|sha256|url
themes=(
# aichat: One Half Dark by Son A. Pham (MIT), loaded as <config>/dark.tmTheme when present
"$cfg/aichat/dark.tmTheme|6205e2311e433841f0aa1b15862b46cb892b003650f31df508a4ebc39a27bdbb|https://raw.githubusercontent.com/sonph/onehalf/d13f47b0c20923eb13335f7effc5e8ec4431a5cd/sublimetext/OneHalfDark.tmTheme"
)
fetched=0 ok=0 kept=0 bad=0
for t in "${themes[@]}"; do
IFS='|' read -r dst sum url <<<"$t"
name="$dst"
case "$dst" in "$HOME"/*) name="~${dst#"$HOME"}" ;; esac
if [ ! -d "$(dirname "$dst")" ]; then
echo " themes: $name skipped (app not configured on this host)"
continue
fi
if [ -f "$dst" ] && [ ! -L "$dst" ]; then
if [ "$(sha256sum <"$dst" | cut -d' ' -f1)" = "$sum" ]; then
ok=$((ok + 1))
else
echo " themes: $name is a real file with other content — left alone"
kept=$((kept + 1))
fi
continue
fi
if [ "$DRY" = 1 ]; then
echo " themes: would fetch $name"
fetched=$((fetched + 1))
continue
fi
tmp="$(mktemp "$dst.XXXXXX")"
if curl -fsSL --connect-timeout 5 --max-time 30 "$url" -o "$tmp" \
&& [ "$(sha256sum <"$tmp" | cut -d' ' -f1)" = "$sum" ]; then
chmod 644 "$tmp"
mv -f "$tmp" "$dst"
fetched=$((fetched + 1))
else
rm -f "$tmp"
echo " themes: could not fetch $name (offline, or the checksum changed) — app uses its built-in theme" >&2
bad=$((bad + 1))
fi
done
verb="fetched"
[ "$DRY" = 1 ] && verb="would fetch"
echo " themes: $verb $fetched, $ok already present, $kept kept as local, $bad problem(s)"
[ "$bad" -eq 0 ]
+1
View File
@@ -115,6 +115,7 @@ __prompt() {
PROMPT_COMMAND=__prompt
set -o vi
eval "$(fzf --bash)"
# eval "$(zoxide init --cmd cd bash)"
# colorized --help
+1 -1
View File
@@ -51,7 +51,7 @@ clients:
- name: GLM-4.7-Flash-UD-Q3_K_XL
max_input_tokens: 22000 # ctx 24576 · ~28 t/s — fast coder (3-bit)
- name: Ornith-1.5-35B-A3B-IQ3_XXS
max_input_tokens: 22000 # ctx 24576 · ~32 t/s — agentic coder, thinking
max_input_tokens: 30000 # ctx 32768 · ~32 t/s — agentic coder, thinking
supports_vision: true
- name: gpt-oss-20b
max_input_tokens: 62000 # ctx 65536 · ~38 t/s — fast reasoning + tools
-663
View File
@@ -1,663 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Name: One Half Dark
Author: Son A. Pham <sp@sonpham.me>
Url: https://github.com/sonph/onehalf
License: The MIT License (MIT)
A dark Sublime Text color scheme based on Atom's One. See
github.com/sonph/onehalf for installation instructions, a light color
scheme, and versions for other editors/terminals such as Vim or iTerm.
Red: #e06c75
Green: #98c379
Yellow: #e5c07b
Blue: #61afef
Purple: #c678dd
Cyan: #56b6c2
White: #dcdfe4
Black: #282c34
Fg: #dcdfe4
Bg: #282c34
Comment: #5c6370
Gutter foreground: #919baa
Gutter background: #282c34
Selection: #474e5d
-->
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>name</key>
<string>OneHalfLight</string>
<key>semanticClass</key>
<string>theme.dark.one_half_dark</string>
<key>uuid</key>
<string></string>
<key>colorSpaceName</key>
<string>sRGB</string>
<key>author</key>
<string>Son A. Pham &lt;sp@sonpham.me&gt;</string>
<key>comment</key>
<string>A dark iTerm color scheme based on Atom&apos;s One. See github.com&#x2f;sonph&#x2f;onehalf for installation instructions, a light color scheme, and versions for other editors&#x2f;terminals such as (Neo)Vim and Sublime Text.</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#dcdfe4</string>
<key>background</key>
<string>#282c34</string>
<key>bracketsOptions</key>
<string>underline</string>
<key>caret</key>
<string>#a3b3cc</string>
<key>gutter</key>
<string>#282c34</string>
<key>gutterForeground</key>
<string>#919baa</string>
<key>invisibles</key>
<string>#5c6370</string>
<key>lineHighlight</key>
<string>#313640</string>
<key>selection</key>
<string>#474e5d</string>
<key>selectionBorder</key>
<string>#474e5d</string>
<key>tagsForeground</key>
<string></string>
<key>tagsOptions</key>
<string>stippled_underline</string>
<key>bracketContentsOptions</key>
<string>underline</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comments</string>
<key>scope</key>
<string>comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#5c6370</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Text</string>
<key>scope</key>
<string>variable.parameter.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Delimiters</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Operators</string>
<key>scope</key>
<string>keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keywords</string>
<key>scope</key>
<string>keyword</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variables</string>
<key>scope</key>
<string>variable</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Functions</string>
<key>scope</key>
<string>entity.name.function, meta.require, support.function.any-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>support.class, entity.name.class, entity.name.type.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Classes</string>
<key>scope</key>
<string>meta.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Methods</string>
<key>scope</key>
<string>keyword.other.special-method</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage</string>
<key>scope</key>
<string>storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Support</string>
<key>scope</key>
<string>support.function</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Strings, Inherited Class</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Integers</string>
<key>scope</key>
<string>constant.numeric</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Floats</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Boolean</string>
<key>scope</key>
<string>none</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Constants</string>
<key>scope</key>
<string>constant</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>HTML: Tags</string>
<key>scope</key>
<string>entity.name.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>HTML: Tag attributes</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute IDs</string>
<key>scope</key>
<string>entity.other.attribute-name.id, punctuation.definition.entity</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Selector</string>
<key>scope</key>
<string>meta.selector</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Headings</string>
<key>scope</key>
<string>markup.heading punctuation.definition.heading, entity.name.section</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string></string>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Bold</string>
<key>scope</key>
<string>markup.bold, punctuation.definition.bold</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Italic</string>
<key>scope</key>
<string>markup.italic, punctuation.definition.italic</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c678dd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Code</string>
<key>scope</key>
<string>markup.raw.inline</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Link Text</string>
<key>scope</key>
<string>string.other.link, punctuation.definition.string.end.markdown</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Link Url</string>
<key>scope</key>
<string>meta.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Lists</string>
<key>scope</key>
<string>markup.list</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markdown: Quotes</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Java Source</string>
<key>scope</key>
<string>source.java meta.class.java meta.method.java</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Java Class Body</string>
<key>scope</key>
<string>source.java meta.class.java meta.class.body.java</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Function Arguments</string>
<key>scope</key>
<string>source.js meta.function.js variable.parameter.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: New Variables</string>
<key>scope</key>
<string>source.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Variables</string>
<key>scope</key>
<string>source.js variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Variables in Function Calls</string>
<key>scope</key>
<string>source.js meta.function-call.method.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: New Block Variables</string>
<key>scope</key>
<string>source.js meta.block.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Block Variables</string>
<key>scope</key>
<string>source.js meta.block.js variable.other.object.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Block Variables in Function Calls</string>
<key>scope</key>
<string>source.js meta.block.js meta.function-call.method.js variable.other.readwrite.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Function Calls</string>
<key>scope</key>
<string>source.js meta.function-call.method.js variable.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Properties</string>
<key>scope</key>
<string>source.js meta.property.object.js entity.name.function.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Javascript: Prototypes</string>
<key>scope</key>
<string>source.js support.constant.prototype.js</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Separator</string>
<key>scope</key>
<string>meta.separator</string>
<key>settings</key>
<dict>
<key>background</key>
<string></string>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Changed</string>
<key>scope</key>
<string>markup.changed</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e5c07b</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Regular Expressions</string>
<key>scope</key>
<string>string.regexp</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Escape Characters</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#56b6c2</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Embedded</string>
<key>scope</key>
<string>punctuation.section.embedded, variable.interpolation</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string></string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Illegal</string>
<key>scope</key>
<string>invalid.illegal</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#e06c75</string>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Broken</string>
<key>scope</key>
<string>invalid.broken</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#e5c07b</string>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Deprecated</string>
<key>scope</key>
<string>invalid.deprecated</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#e5c07b</string>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Unimplemented</string>
<key>scope</key>
<string>invalid.unimplemented</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#c678dd</string>
<key>foreground</key>
<string>#dcdfe4</string>
</dict>
</dict>
</array>
</dict>
</plist>
+1 -1
View File
@@ -4,7 +4,7 @@
# Specify desired highlighting theme (e.g. "TwoDark"). Run `bat --list-themes`
# for a list of all available themes
# Custom theme lives in ./themes/ — run `bat cache --build` after changing it
# OneHalfDark is built into bat — no custom themes are tracked, so no `bat cache --build` step
--theme="OneHalfDark"
--style="numbers,changes,header,snip"
-260
View File
@@ -1,260 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>name</key>
<string>One Dark (NvChad base46)</string>
<key>uuid</key>
<string>3c1e2b84-9d5f-4a7c-8b2e-6f4a1d9c7e5b</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#1e222a</string>
<key>foreground</key>
<string>#abb2bf</string>
<key>caret</key>
<string>#abb2bf</string>
<key>selection</key>
<string>#3e4451</string>
<key>lineHighlight</key>
<string>#252931</string>
<key>gutterForeground</key>
<string>#545862</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment, punctuation.definition.comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#6f737b</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Number, constant</string>
<key>scope</key>
<string>constant.numeric, constant.language, constant.other</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d19a66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Escape character</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#a3b8ef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword, storage</string>
<key>scope</key>
<string>keyword, storage</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#c882e7</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Operator</string>
<key>scope</key>
<string>keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#abb2bf</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function</string>
<key>scope</key>
<string>entity.name.function, support.function, meta.function-call.generic</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Class, type</string>
<key>scope</key>
<string>entity.name.class, entity.name.type, entity.name.struct, entity.name.enum, support.type, support.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e7c787</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable, variable.other</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function parameter</string>
<key>scope</key>
<string>variable.parameter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#d19a66</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tag</string>
<key>scope</key>
<string>entity.name.tag, meta.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup heading</string>
<key>scope</key>
<string>markup.heading, entity.name.section</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup bold</string>
<key>scope</key>
<string>markup.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup italic</string>
<key>scope</key>
<string>markup.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup link</string>
<key>scope</key>
<string>markup.underline.link, string.other.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#61afef</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup quote</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#6f737b</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#98c379</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#e06c75</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid, invalid.illegal</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ffffff</string>
<key>background</key>
<string>#be5046</string>
</dict>
</dict>
</array>
</dict>
</plist>
@@ -1,262 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>name</key>
<string>Synthwave 84</string>
<key>uuid</key>
<string>7b2c3a10-4b6e-4d8f-9c1a-2e5f8d7a9b3c</string>
<key>settings</key>
<array>
<dict>
<key>settings</key>
<dict>
<key>background</key>
<string>#262335</string>
<key>foreground</key>
<string>#ffffff</string>
<key>caret</key>
<string>#f97e72</string>
<key>selection</key>
<string>#463465</string>
<key>lineHighlight</key>
<string>#2a2139</string>
<key>gutterForeground</key>
<string>#848bbd</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Comment</string>
<key>scope</key>
<string>comment, punctuation.definition.comment</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#848bbd</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>String</string>
<key>scope</key>
<string>string</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ff8b39</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Number, constant</string>
<key>scope</key>
<string>constant.numeric, constant.language, constant.other</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#f97e72</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Escape character</string>
<key>scope</key>
<string>constant.character.escape</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#36f9f6</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Keyword, storage</string>
<key>scope</key>
<string>keyword, storage, keyword.operator</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fede5d</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Storage type</string>
<key>scope</key>
<string>storage.type, storage.modifier</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fede5d</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function</string>
<key>scope</key>
<string>entity.name.function, support.function, meta.function-call.generic</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#36f9f6</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Class, type</string>
<key>scope</key>
<string>entity.name.class, entity.name.type, entity.name.struct, entity.name.enum, support.type, support.class</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fe4450</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Variable</string>
<key>scope</key>
<string>variable, variable.other</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ff7edb</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Function parameter</string>
<key>scope</key>
<string>variable.parameter</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ff7edb</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Attribute</string>
<key>scope</key>
<string>entity.other.attribute-name</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fede5d</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Tag</string>
<key>scope</key>
<string>entity.name.tag, meta.tag</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#72f1b8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup heading</string>
<key>scope</key>
<string>markup.heading, entity.name.section</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fe4450</string>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup bold</string>
<key>scope</key>
<string>markup.bold</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>bold</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup italic</string>
<key>scope</key>
<string>markup.italic</string>
<key>settings</key>
<dict>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup link</string>
<key>scope</key>
<string>markup.underline.link, string.other.link</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#36f9f6</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Markup quote</string>
<key>scope</key>
<string>markup.quote</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#848bbd</string>
<key>fontStyle</key>
<string>italic</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff inserted</string>
<key>scope</key>
<string>markup.inserted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#72f1b8</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Diff deleted</string>
<key>scope</key>
<string>markup.deleted</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#fe4450</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>Invalid</string>
<key>scope</key>
<string>invalid, invalid.illegal</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#ffffff</string>
<key>background</key>
<string>#fe4450</string>
</dict>
</dict>
</array>
</dict>
</plist>
+4 -294
View File
@@ -1,241 +1,13 @@
## Configuration file for CAVA.
# Remove the ; to change parameters.
## cava — only this repo's own settings. Every key left out falls back to cava's built-in
## default; cava documents them all in the config it writes to an empty ~/.config/cava/config,
## and it regenerates its own shaders/ and themes/ there on first run (so the repo ships none
## of cava's files).
[general]
# Smoothing mode. Can be 'normal', 'scientific' or 'waves'. DEPRECATED as of 0.6.0
# mode = scientific # deprecated since cava 0.6.0, ignored
# Accepts only non-negative values.
framerate = 60
# 'autosens' will attempt to decrease sensitivity if the bars peak. 1 = on, 0 = off
# new as of 0.6.0 autosens of low values (dynamic range)
# 'overshoot' allows bars to overshoot (in % of terminal height) without initiating autosens. DEPRECATED as of 0.6.0
autosens = 1
; overshoot = 20
# Manual sensitivity in %. If autosens is enabled, this will only be the initial value.
# 200 means double height. Accepts only non-negative values.
; sensitivity = 100
# The number of bars (0-512). 0 sets it to auto (fill up console).
# Bars' width and space between bars in number of characters.
; bars = 0
; bar_width = 2
; bar_spacing = 1
# bar_height is only used for output in "noritake" format
; bar_height = 32
# For SDL width and space between bars is in pixels, defaults are:
; bar_width = 20
; bar_spacing = 5
# sdl_glsl have these default values, they are only used to calculate max number of bars.
; bar_width = 1
; bar_spacing = 0
# ceter bars in terminal, if there is space.
; center_align = 1
# max height of bars in terminal, in percent of terminal height.
; max_height = 100
# Lower and higher cutoff frequencies for lowest and highest bars
# the bandwidth of the visualizer.
# Note: there is a minimum total bandwidth of 43Mhz x number of bars.
# Cava will automatically increase the higher cutoff if a too low band is specified.
; lower_cutoff_freq = 50
; higher_cutoff_freq = 10000
# Seconds with no input before cava goes to sleep mode. Cava will not perform FFT or drawing and
# only check for input once per second. Cava will wake up once input is detected. 0 = disable.
; sleep_timer = 0
[input]
# Audio capturing method. Possible methods are: 'fifo', 'portaudio', 'pipewire', 'alsa', 'pulse', 'sndio', 'oss', 'jack' or 'shmem'
# Defaults to 'oss', 'pipewire', 'sndio', 'jack', 'pulse', 'alsa', 'portaudio' or 'fifo', in that order, dependent on what support cava was built with.
# On Mac it defaults to 'portaudio' or 'fifo'
# On windows this is automatic and no input settings are needed.
#
# All input methods uses the same config variable 'source'
# to define where it should get the audio.
#
# For pulseaudio and pipewire 'source' will be the source. Default: 'auto', which uses the monitor source of the default sink
# (all pulseaudio sinks(outputs) have 'monitor' sources(inputs) associated with them).
#
# For pipewire 'source' will be the object name or object.serial of the device to capture from.
# Both input and output devices are supported. To capture the monitor source of a sink node, append '.monitor' to the sink's object name.
#
# For alsa 'source' will be the capture device.
# For fifo 'source' will be the path to fifo-file.
# For shmem 'source' will be /squeezelite-AA:BB:CC:DD:EE:FF where 'AA:BB:CC:DD:EE:FF' will be squeezelite's MAC address
#
# For sndio 'source' will be a raw recording audio descriptor or a monitoring sub-device, e.g. 'rsnd/2' or 'snd/1'. Default: 'default'.
# README.md contains further information on how to setup CAVA for sndio.
#
# For oss 'source' will be the path to a audio device, e.g. '/dev/dsp2'. Default: '/dev/dsp', i.e. the default audio device.
# README.md contains further information on how to setup CAVA for OSS on FreeBSD.
#
# For jack 'source' will be the name of the JACK server to connect to, e.g. 'foobar'. Default: 'default'.
# README.md contains further information on how to setup CAVA for JACK.
#
; method = pulse
; source = auto
; method = pipewire
; source = auto
; method = alsa
; source = hw:Loopback,1
; method = fifo
; source = /tmp/mpd.fifo
; method = shmem
; source = /squeezelite-AA:BB:CC:DD:EE:FF
; method = portaudio
; source = auto
; method = sndio
; source = default
; method = oss
; source = /dev/dsp
; method = jack
; source = default
# The options 'sample_rate', 'sample_bits', 'channels' and 'autoconnect' can be configured for some input methods:
# sample_rate: fifo, pipewire, sndio, oss
# sample_bits: fifo, pipewire, sndio, oss
# channels: sndio, oss, jack
# autoconnect: jack
# Other methods ignore these settings.
#
# For 'sndio' and 'oss' they are only preferred values, i.e. if the values are not supported
# by the chosen audio device, the device will use other supported values instead.
# Example: 48000, 32 and 2, but the device only supports 44100, 16 and 1, then it
# will use 44100, 16 and 1.
#
; sample_rate = 44100
; sample_bits = 16
; channels = 2
; autoconnect = 2
[output]
# Output method. Can be 'ncurses', 'noncurses', 'raw', 'noritake', 'sdl'
# or 'sdl_glsl'.
# 'noncurses' (default) uses a buffer and cursor movements to only print
# changes from frame to frame in the terminal. Uses less resources and is less
# prone to tearing (vsync issues) than 'ncurses'.
#
# 'raw' is an 8 or 16 bit (configurable via the 'bit_format' option) data
# stream of the bar heights that can be used to send to other applications.
# 'raw' defaults to 1024 bars stereo (512 bars mono), which can be adjusted in the 'bars' option above.
#
# 'noritake' outputs a bitmap in the format expected by a Noritake VFD display
# in graphic mode. It only support the 3000 series graphical VFDs for now.
#
# 'sdl' uses the Simple DirectMedia Layer to render in a graphical context.
# 'sdl_glsl' uses SDL to create an OpenGL context. Write your own shaders or
# use one of the predefined ones.
; method = noncurses
# Orientation of the visualization. Can be 'bottom', 'top', 'left', 'right' or
# 'horizontal'. Default is 'bottom'. 'left and 'right' are only supported on sdl
# and ncruses output. 'horizontal' (bars go up and down from center) is only supported
# on noncurses output.
# Note: many fonts have weird or missing glyphs for characters used in orientations
# other than 'bottom', which can make output not look right.
; orientation = bottom
# Visual channels. Can be 'stereo' or 'mono'.
# 'stereo' mirrors both channels with low frequencies in center.
# 'mono' outputs left to right lowest to highest frequencies.
# 'mono_option' set mono to either take input from 'left', 'right' or 'average'.
# set 'reverse' to 1 to display frequencies the other way around.
; channels = stereo
; mono_option = average
; reverse = 0
# Raw output target.
# On Linux, a fifo will be created if target does not exist.
# On Windows, a named pipe will be created if target does not exist.
; raw_target = /dev/stdout
# Raw data format. Can be 'binary' or 'ascii'.
; data_format = binary
# Binary bit format, can be '8bit' (0-255) or '16bit' (0-65530).
; bit_format = 16bit
# Ascii max value. In 'ascii' mode range will run from 0 to value specified here
; ascii_max_range = 1000
# Ascii delimiters. In ascii format each bar and frame is separated by a delimiters.
# Use decimal value in ascii table (i.e. 59 = ';' and 10 = '\n' (line feed)).
; bar_delimiter = 59
; frame_delimiter = 10
# sdl window size and position. -1,-1 is centered.
; sdl_width = 1024
; sdl_height = 512
; sdl_x = -1
; sdl_y= -1
; sdl_full_screen = 0
# set label on bars on the x-axis. Can be 'frequency' or 'none'. Default: 'none'
# 'frequency' displays the lower cut off frequency of the bar above.
# Only supported on ncurses and noncurses output.
; xaxis = none
# enable synchronized sync. 1 = on, 0 = off
# removes flickering in alacritty terminal emulator.
# defaults to off since the behaviour in other terminal emulators is unknown
; synchronized_sync = 0
# Shaders for sdl_glsl, located in $HOME/.config/cava/shaders
; vertex_shader = pass_through.vert
; fragment_shader = bar_spectrum.frag
; for glsl output mode, keep rendering even if no audio
; continuous_rendering = 0
# disable console blank (screen saver) in tty
# (Not supported on FreeBSD)
; disable_blanking = 0
# show a flat bar at the bottom of the screen when idle, 1 = on, 0 = off
; show_idle_bar_heads = 1
# show waveform instead of frequency spectrum, 1 = on, 0 = off
; waveform = 0
[color]
# Colors can be one of seven predefined: black, blue, cyan, green, magenta, red, white, yellow.
# Or defined by hex code '#xxxxxx' (hex code must be within ''). User defined colors requires
# a terminal that can change color definitions such as Gnome-terminal or rxvt.
# default is to keep current terminal color
; background = default
; foreground = default
# SDL and sdl_glsl only support hex code colors, these are the default:
; background = '#111111'
; foreground = '#D34b11'
# ; foreground = '#D34b11'
# Gradient mode, only hex defined colors are supported,
# background must also be defined in hex or remain commented out. 1 = on, 0 = off.
# You can define as many as 8 different colors. They range from bottom to top of screen
gradient = 1
gradient_color_1 = '#d34b11'
gradient_color_2 = '#cc5933'
@@ -245,65 +17,3 @@ gradient_color_5 = '#cccc33'
gradient_color_6 = '#a6cc33'
gradient_color_7 = '#80cc33'
gradient_color_8 = '#59cc33'
# Horizontal is only supported on noncurses output.
# Only one color will be calculated per bar.
; horizontal_gradient = 1
; horizontal_gradient_color_1 = '#c45161'
; horizontal_gradient_color_2 = '#e094a0'
; horizontal_gradient_color_3 = '#f2b6c0'
; horizontal_gradient_color_4 = '#f2dde1'
; horizontal_gradient_color_5 = '#cbc7d8'
; horizontal_gradient_color_6 = '#8db7d2'
; horizontal_gradient_color_7 = '#5e62a9'
; horizontal_gradient_color_8 = '#434279'
# If both vertical and horizontal gradient is enabled, vertical will be blended in this direction.
# Can be 'up', 'down', 'left' or 'right'. 'up' means the vertical gradient will be blended in from
# bottom to top. I.e. the bottom will be only the horizontal
# and top will be only the color of the vertical gradient.
; blend_direction = 'up'
# use theme file instead of defining colors in this file
# themes are located in $HOME/.config/cava/themes
; theme = 'none'
[smoothing]
# Percentage value for integral smoothing. Takes values from 0 - 100.
# Higher values means smoother, but less precise. 0 to disable.
# DEPRECATED as of 0.8.0, use noise_reduction instead
; integral = 77
# Disables or enables the so-called "Monstercat smoothing" with or without "waves". Set to 0 to disable.
; monstercat = 0
; waves = 0
# Set gravity percentage for "drop off". Higher values means bars will drop faster.
# Accepts only non-negative values. 50 means half gravity, 200 means double. Set to 0 to disable "drop off".
# DEPRECATED as of 0.8.0, use noise_reduction instead
; gravity = 100
# In bar height, bars that would have been lower that this will not be drawn.
# DEPRECATED as of 0.8.0
; ignore = 0
# Noise reduction, int 0 - 100. default 77
# the raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth
# 100 will be very slow and smooth, 0 will be fast but noisy.
; noise_reduction = 77
[eq]
# This one is tricky. You can have as much keys as you want.
# Remember to uncomment more than one key! More keys = more precision.
# Look at readme.md on github for further explanations and examples.
; 1 = 1 # bass
; 2 = 1
; 3 = 1 # midtone
; 4 = 1
; 5 = 1 # treble
@@ -1,79 +0,0 @@
#version 330
in vec2 fragCoord;
out vec4 fragColor;
// bar values. defaults to left channels first (low to high), then right (high to low).
uniform float bars[512];
uniform int bars_count; // number of bars (left + right) (configurable)
uniform int bar_width; // bar width (configurable), not used here
uniform int bar_spacing; // space bewteen bars (configurable)
uniform vec3 u_resolution; // window resolution
//colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
uniform vec3 bg_color; // background color
uniform vec3 fg_color; // foreground color
uniform int gradient_count;
uniform vec3 gradient_colors[8]; // gradient colors
vec3 normalize_C(float y,vec3 col_1, vec3 col_2, float y_min, float y_max)
{
//create color based on fraction of this color and next color
float yr = (y - y_min) / (y_max - y_min);
return col_1 * (1.0 - yr) + col_2 * yr;
}
void main()
{
// find which bar to use based on where we are on the x axis
float x = u_resolution.x * fragCoord.x;
int bar = int(bars_count * fragCoord.x);
//calculate a bar size
float bar_size = u_resolution.x / bars_count;
//the y coordinate and bar values are the same
float y = bars[bar];
// make sure there is a thin line at bottom
if (y * u_resolution.y < 1.0)
{
y = 1.0 / u_resolution.y;
}
//draw the bar up to current height
if (y > fragCoord.y)
{
//make some space between bars basen on settings
if (x > (bar + 1) * (bar_size) - bar_spacing)
{
fragColor = vec4(bg_color,1.0);
}
else
{
if (gradient_count == 0)
{
fragColor = vec4(fg_color,1.0);
}
else
{
//find which color in the configured gradient we are at
int color = int((gradient_count - 1) * fragCoord.y);
//find where on y this and next color is supposed to be
float y_min = color / (gradient_count - 1.0);
float y_max = (color + 1.0) / (gradient_count - 1.0);
//make color
fragColor = vec4(normalize_C(fragCoord.y, gradient_colors[color], gradient_colors[color + 1], y_min, y_max), 1.0);
}
}
}
else
{
fragColor = vec4(bg_color,1.0);
}
}
-117
View File
@@ -1,117 +0,0 @@
#version 330
// this shader was stolen from shadertoy user ChunderFPV
#define SCALE 8.0
#define PI radians(180.0)
#define TAU (PI * 2.0)
#define CS(a) vec2(cos(a), sin(a))
#define PT(u, r) smoothstep(0.0, r, r - length(u))
in vec2 fragCoord;
out vec4 fragColor;
uniform float bars[512];
uniform int bars_count; // number of bars (left + right) (configurable)
uniform float shader_time; // shader execution time s
uniform int bar_width; // bar width (configurable), not used here
uniform int bar_spacing; // space bewteen bars (configurable)
uniform vec3 u_resolution; // window resolution
// colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
uniform vec3 bg_color; // background color
uniform vec3 fg_color; // foreground color
uniform int gradient_count;
uniform vec3 gradient_colors[8]; // gradient colors
// gradient map ( color, equation, time, width, shadow, reciprocal )
vec3 gm(vec3 c, float n, float t, float w, float d, bool i) {
float g = min(abs(n), 1.0 / abs(n));
float s = abs(sin(n * PI - t));
if (i)
s = min(s, abs(sin(PI / n + t)));
return (1.0 - pow(abs(s), w)) * c * pow(g, d) * 6.0;
}
// denominator spiral, use 1/n for numerator
// ( screen xy, spiral exponent, decimal, line width, hardness, rotation )
float ds(vec2 u, float e, float n, float w, float h, float ro) {
float ur = length(u); // unit radius
float sr = pow(ur, e); // spiral radius
float a = round(sr) * n * TAU; // arc
vec2 xy = CS(a + ro) * ur; // xy coords
float l = PT(u - xy, w); // line
float s = mod(sr + 0.5, 1.0); // gradient smooth
s = min(s, 1.0 - s); // darken filter
return l * s * h;
}
void main() {
float t = shader_time / PI * 2.0;
vec4 m = vec4(0, 0, 0, 0); // iMouse;
m.xy = m.xy * 2.0 / u_resolution.xy - 1.0; // ±1x, ±1y
if (m.z > 0.0)
t += m.y * SCALE; // move time with mouse y
float z = (m.z > 0.0) ? pow(1.0 - abs(m.y), sign(m.y)) : 1.0; // zoom (+)
float e = (m.z > 0.0) ? pow(1.0 - abs(m.x), -sign(m.x))
: 1.0; // screen exponent (+)
float se = (m.z > 0.0) ? e * -sign(m.y) : 1.0; // spiral exponent
vec3 bg = vec3(0); // black background
float aa = 3.0; // anti-aliasing
for (float j = 0.0; j < aa; j++)
for (float k = 0.0; k < aa; k++) {
vec3 c = vec3(0);
vec2 o = vec2(j, k) / aa;
vec2 uv = (fragCoord * u_resolution.xy - 0.5 * u_resolution.xy + o) /
u_resolution.y * SCALE * z; // apply cartesian, scale and zoom
if (m.z > 0.0)
uv =
exp(log(abs(uv)) * e) * sign(uv); // warp screen space with exponent
float px = length(fwidth(uv)); // pixel width
float x = uv.x; // every pixel on x
float y = uv.y; // every pixel on y
float l = length(uv); // hypot of xy: sqrt(x*x+y*y)
float mc = (x * x + y * y - 1.0) / y; // metallic circle at xy
float g = min(abs(mc), 1.0 / abs(mc)); // gradient
vec3 gold = vec3(1.0, 0.6, 0.0) * g * l;
vec3 blue = vec3(0.3, 0.5, 0.9) * (1.0 - g);
vec3 rgb = max(gold, blue);
float w = 0.1; // line width
float d = 0.4; // shadow depth
c = max(c, gm(rgb, mc, -t, w * bars[0], d, false)); // metallic
c = max(c, gm(rgb, abs(y / x) * sign(y), -t, w * bars[1], d,
false)); // tangent
c = max(c, gm(rgb, (x * x) / (y * y) * sign(y), -t, w * bars[2], d,
false)); // sqrt cotangent
c = max(c, gm(rgb, (x * x) + (y * y), t, w * bars[3], d,
true)); // sqrt circles
c += rgb * ds(uv, se, t / TAU, px * 2.0 * bars[4], 2.0, 0.0); // spiral 1a
c += rgb * ds(uv, se, t / TAU, px * 2.0 * bars[5], 2.0, PI); // spiral 1b
c +=
rgb * ds(uv, -se, t / TAU, px * 2.0 * bars[6], 2.0, 0.0); // spiral 2a
c += rgb * ds(uv, -se, t / TAU, px * 2.0 * bars[7], 2.0, PI); // spiral 2b
c = max(c, 0.0); // clear negative color
c += pow(max(1.0 - l, 0.0), 3.0 / z); // center glow
if (m.z > 0.0) // display grid on click
{
vec2 xyg = abs(fract(uv + 0.5) - 0.5) / px; // xy grid
c.gb += 0.2 * (1.0 - min(min(xyg.x, xyg.y), 1.0));
}
bg += c;
}
bg /= aa * aa;
bg *= sqrt(bg) * 1.5;
fragColor = vec4(bg, 1.0);
}
@@ -1,34 +0,0 @@
#version 330
in vec2 fragCoord;
out vec4 fragColor;
// bar values. defaults to left channels first (low to high), then right (high to low).
uniform float bars[512];
uniform int bars_count; // number of bars (left + right) (configurable)
uniform vec3 u_resolution; // window resolution, not used here
//colors, configurable in cava config file
uniform vec3 bg_color; // background color(r,g,b) (0.0 - 1.0), not used here
uniform vec3 fg_color; // foreground color, not used here
void main()
{
// find which bar to use based on where we are on the x axis
int bar = int(bars_count * fragCoord.x);
float bar_y = 1.0 - abs((fragCoord.y - 0.5)) * 2.0;
float y = (bars[bar]) * bar_y;
float bar_x = (fragCoord.x - float(bar) / float(bars_count)) * bars_count;
float bar_r = 1.0 - abs((bar_x - 0.5)) * 2;
bar_r = bar_r * bar_r * 2;
// set color
fragColor.r = fg_color.x * y * bar_r;
fragColor.g = fg_color.y * y * bar_r;
fragColor.b = fg_color.z * y * bar_r;
}
@@ -1,14 +0,0 @@
#version 330
// Input vertex data, different for all executions of this shader.
layout(location = 0) in vec3 vertexPosition_modelspace;
// Output data ; will be interpolated for each fragment.
out vec2 fragCoord;
void main()
{
gl_Position = vec4(vertexPosition_modelspace,1);
fragCoord = (vertexPosition_modelspace.xy+vec2(1,1))/2.0;
}
@@ -1,53 +0,0 @@
#version 330
in vec2 fragCoord;
out vec4 fragColor;
// bar values. defaults to left channels first (low to high), then right (high
// to low).
uniform float bars[512];
uniform int bars_count; // number of bars (left + right) (configurable)
uniform int bar_width; // bar width (configurable), not used here
uniform int bar_spacing; // space bewteen bars (configurable)
uniform vec3 u_resolution; // window resolution
// colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
uniform vec3 bg_color; // background color
uniform vec3 fg_color; // foreground color
uniform int gradient_count;
uniform vec3 gradient_colors[8]; // gradient colors
uniform sampler2D inputTexture; // Texture from the first render pass
vec3 normalize_C(float y, vec3 col_1, vec3 col_2, float y_min, float y_max) {
// create color based on fraction of this color and next color
float yr = (y - y_min) / (y_max - y_min);
return col_1 * (1.0 - yr) + col_2 * yr;
}
void main() {
// find which bar to use based on where we are on the y axis
int bar = int(bars_count * fragCoord.y);
float y = bars[bar];
float band_size = 1.0 / float(bars_count);
float current_band_min = bar * band_size;
float current_band_max = (bar + 1) * band_size;
int hist_length = 512;
float win_size = 1.0 / hist_length;
if (fragCoord.x > 1.0 - win_size) {
if (fragCoord.y > current_band_min && fragCoord.y < current_band_max) {
fragColor = vec4(fg_color * y, 1.0);
}
} else {
vec2 offsetCoord = fragCoord;
offsetCoord.x += float(win_size);
fragColor = texture(inputTexture, offsetCoord);
}
}
@@ -1,112 +0,0 @@
#version 330
// Emulate the "line style" spectrum analyzer from Winamp 2.
// Try this config for a demonstration:
/*
[general]
bar_width = 2
bar_spacing = 0
higher_cutoff_freq = 22000
[output]
method = sdl_glsl
channels = mono
fragment_shader = winamp_line_style_spectrum.frag
[color]
background = '#000000'
gradient = 1
gradient_color_1 = '#319C08'
gradient_color_2 = '#29CE10'
gradient_color_3 = '#BDDE29'
gradient_color_4 = '#DEA518'
gradient_color_5 = '#D66600'
gradient_color_6 = '#CE2910'
[smoothing]
noise_reduction = 10
*/
in vec2 fragCoord;
out vec4 fragColor;
// bar values. defaults to left channels first (low to high), then right (high to low).
uniform float bars[512];
uniform int bars_count; // number of bars (left + right) (configurable)
uniform int bar_width; // bar width (configurable), not used here
uniform int bar_spacing; // space bewteen bars (configurable)
uniform vec3 u_resolution; // window resolution
//colors, configurable in cava config file (r,g,b) (0.0 - 1.0)
uniform vec3 bg_color; // background color
uniform vec3 fg_color; // foreground color
uniform int gradient_count;
uniform vec3 gradient_colors[8]; // gradient colors
vec3 normalize_C(float y,vec3 col_1, vec3 col_2, float y_min, float y_max)
{
//create color based on fraction of this color and next color
float yr = (y - y_min) / (y_max - y_min);
return col_1 * (1.0 - yr) + col_2 * yr;
}
void main()
{
// find which bar to use based on where we are on the x axis
float x = u_resolution.x * fragCoord.x;
int bar = int(bars_count * fragCoord.x);
//calculate a bar size
float bar_size = u_resolution.x / bars_count;
//the y coordinate is stretched by 4X to resemble Winamp
float y = min(bars[bar] * 4.0, 1.0);
// make sure there is a thin line at bottom
if (y * u_resolution.y < 1.0)
{
y = 1.0 / u_resolution.y;
}
vec4 bar_color;
if (gradient_count == 0)
{
bar_color = vec4(fg_color,1.0);
}
else
{
//find color in the configured gradient for the top of the bar
int color = int((gradient_count - 1) * y);
//find where on y this and next color is supposed to be
float y_min = float(color) / (gradient_count - 1.0);
float y_max = float(color + 1) / (gradient_count - 1.0);
//make a solid color for the entire bar
bar_color = vec4(normalize_C(y, gradient_colors[color], gradient_colors[color + 1], y_min, y_max), 1.0);
}
//draw the bar up to current height
if (y > fragCoord.y)
{
//make some space between bars based on settings
if (x > (bar + 1) * (bar_size) - bar_spacing)
{
fragColor = vec4(bg_color,1.0);
}
else
{
fragColor = bar_color;
}
}
else
{
fragColor = vec4(bg_color,1.0);
}
}
-15
View File
@@ -1,15 +0,0 @@
[color]
background = '#001e26'
foreground = '#708183'
gradient = 1
gradient_color_1 = '#268bd2'
gradient_color_2 = '#6c71c4'
gradient_color_3 = '#cb4b16'
horizontal_gradient = 1
horizontal_gradient_color_1 = '#586e75'
horizontal_gradient_color_2 = '#b58900'
horizontal_gradient_color_3 = '#839496'
blend_direction = 'up'
-10
View File
@@ -1,10 +0,0 @@
[color]
horizontal_gradient = 1
horizontal_gradient_color_1 = '#c45161'
horizontal_gradient_color_2 = '#e094a0'
horizontal_gradient_color_3 = '#f2b6c0'
horizontal_gradient_color_4 = '#f2dde1'
horizontal_gradient_color_5 = '#cbc7d8'
horizontal_gradient_color_6 = '#8db7d2'
horizontal_gradient_color_7 = '#5e62a9'
horizontal_gradient_color_8 = '#434279'
+49 -19
View File
@@ -7,15 +7,20 @@ back into this repo.
## What's tracked here
- `settings.json` — global settings: permissions (deny git commit/push, `gh pr` write ops, and
reads of secret files), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`,
- `settings.json` — global settings: permissions (deny git push, `gh pr merge` / `close`,
and reads of secret files; ask before `gh pr create` and `git rebase`; allow `gh` reads, `gh pr edit`, `gh workflow run`
and `gh run rerun`; `git commit` is left to `hooks/git-guard.py`, which exempts listed repos), default `model`, `enabledPlugins`, vim mode, dark theme, `effortLevel`,
fullscreen TUI, `statusLine`, `hooks`, `worktree` defaults, a generic `autoMode.environment`, …
Nothing in here may name a specific project, path or remote: this file is published by the
public mirror, and the auto-mode classifier only reads `autoMode` from user/managed settings
anyway (never from a project's `.claude/settings*.json`).
- `statusline.py` — rich status line (dir, git, model, context %, cost, disk — system metrics live in the tmux bar).
- `statusline.py` — rich status line (dir, git, unresolved PR review threads, model, context %, cost, disk — system
metrics live in the tmux bar).
See [StatusBar.md](StatusBar.md) for what each segment and colour means.
- `keybindings.json` — custom keybindings: vim-style scroll/navigation, plus `ctrl+x v` for the
- `keybindings.json` — custom keybindings: vim-style scroll/navigation (`alt+j/k` and
`ctrl+shift+j/k` scroll by line — kitty's own `kitty_mod+j/k` scroll only its main-screen
scrollback and pass the key through to the fullscreen TUI's alternate screen, so they do not
shadow these; dropped once by mistake on 2026-09-15), plus `ctrl+x v` for the
verbose transcript view (`app:toggleTranscript`, same keystroke as opencode's `<leader>v`; the
default `ctrl+o` still works, and `ctrl+e` inside the view expands everything).
- `CLAUDE.md` — global user preferences applied to every project.
@@ -29,17 +34,32 @@ back into this repo.
installed, and always exits 0.
- `hooks/git-guard.py` — PreToolUse hook on `Bash`: structurally blocks anything that commits,
rewrites history or discards working-tree changes — `git commit` / `push` / `revert` /
`cherry-pick` / `rebase` / `merge` (not `--abort`), `clean` / `reset` in any form that moves HEAD
`cherry-pick` / `rebase -i|--autostash` / `merge` (not `--abort`), `clean` / `reset` in any form that moves HEAD
or unstages everything (`reset -- <paths>`, `reset HEAD <path>` stay allowed) / `restore <paths>`
(`--staged <path>` is fine, `--staged .` is not) / `checkout --` / `checkout .` / `stash drop|pop|clear` /
`branch -D|-f|-M`, and `gh pr merge` / `create` / `close` / `edit` — wherever the flags sit
`branch -D|-f|-M`, and `gh pr merge` / `close`, `gh pr create` without `--head` (gh would push
the branch itself) and `gh pr edit` beyond `--title` / `--body` — wherever the flags sit
(`git -C . push`, `/usr/bin/git push`, `sh -c 'git push'`), which the prefix-matched deny rules
can't. The discard group is what protects parallel sessions sharing one checkout. Exit 2 + a
can't. A plain `rebase` and its `--continue` / `--abort` pass: Claude rebases and fixes
conflicts when asked (`CLAUDE.md`), and settings.json asks before each `git rebase`. The
discard group is what protects parallel sessions sharing one checkout. Exit 2 + a
one-line reason. Heredoc bodies are skipped on purpose (file contents). Regression matrix:
`bash ~/.claude/hooks/git-guard-test.sh` (a file on purpose — typed inline, the commands under
test would trip the live hook).
test would trip the live hook). Repos listed in `~/.claude/git-guard-exempt` (host-local, not in
this repo — it names projects; one main checkout per line, worktrees included): `commit` and
`merge` pass there — judged from the hook input's
`cwd`, every `cd` before the call and `git -C` — except `commit --amend` / `-a`. A user-scope deny
cannot be lifted for one repo, so `git commit` is off the deny list: the hook is its only
structural block.
- `hooks/notify.py` — Notification hook (`idle_prompt|permission_prompt`): `notify-send` desktop
notification when Claude is waiting on you; no-op on a headless host.
- `../tmux/scripts/agent-state.sh` — UserPromptSubmit (busy) / Notification (input) / Stop (done) /
StopFailure (error) / SessionEnd (off) hooks, all async: marks the tmux pane this session runs in
(`@agent` pane option, drawn as ✓ ? ! after the window name by tmux.conf; visiting the pane
greys it out, the next prompt removes it) and buzzes the phone for a new mark on a pane not in
view (termux-notification, else a bell). Claude sessions run in daemon-spawned workers with no
tmux env, so the script finds the pane by the session name Claude puts in the pane title. Shared with codex
(`~/.codex/hooks.json`, untracked), pi and opencode. No-op outside tmux.
- `hooks/drift-check.sh` — SessionStart + UserPromptSubmit hook, read-only: fails loudly when a
`~/.config/claude/*` file has broken out of the repo into a plain file, or `~/.claude/*` no longer
resolves into the dots repo. That is what Claude Code's atomic settings rewrite does through the
@@ -55,14 +75,16 @@ back into this repo.
- `skills/` — custom [Agent Skills](https://code.claude.com/docs/en/skills). See **Skills** below.
- `link.sh` — idempotent bootstrap that creates the `~/.claude` symlinks below.
- `../git/claude-commit-msg` + `../git/hooks/prepare-commit-msg` — the commit-message handoff.
A session pipes its suggestion into `claude-commit-msg save`, which stores it as its own file under
`$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **with the paths it staged**; on the next
`git commit` / lazygit `C` the hook prefills the editor with the message(s) whose paths are part of
that commit and consumes them (`used/`). Parallel sessions therefore never overwrite each other,
and committing one session's files gets that session's message. `claude-commit-msg list` shows
what is pending. The old single `CLAUDE_COMMIT_MSG` file is still honoured. Both tracked here
(deployed to `~/.config/git/`); the hook is installed **per repo** — see setup step 4 — and
deliberately not via a global `core.hooksPath`, which would override a project's own hooks.
A session pipes its suggestion into `claude-commit-msg save` (run inside the target repo), which
stores it as its own file under `$(git rev-parse --git-path CLAUDE_COMMIT_MSG.d)/` **with the paths
it staged**; on the next `git commit` / lazygit `C` the hook prefills the editor with the message(s)
whose paths are part of that commit and consumes them (`used/`). Parallel sessions therefore never
overwrite each other, and committing one session's files gets that session's message.
`claude-commit-msg list` shows what is pending. The old single `CLAUDE_COMMIT_MSG` file is still
honoured. Both tracked here (deployed to `~/.config/git/`); the hook is installed **per repo** — see
setup step 4 — and deliberately not via a global `core.hooksPath`, which would override a project's
own hooks. The prefill only runs for an editor commit: lazygit's `c` panel passes the message to git
directly, so the hook skips and the suggestion stays pending.
The `settings.json` hook commands for the newer scripts are wrapped in `[ -f … ]`, so a host that
has pulled the repo but not yet re-stowed just skips them instead of blocking every Bash call.
@@ -93,7 +115,8 @@ claude
```
```fish
# 4. In every repo you commit in: install the one-shot commit-message prefill hook
# 4. In every repo you commit in: install the commit-message prefill hook. New clones/inits get it
# from ~/.config/git/template (init.templateDir) once that template hook is a symlink to it too.
ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/prepare-commit-msg
```
@@ -101,6 +124,12 @@ ln -sf ~/.config/git/hooks/prepare-commit-msg (git rev-parse --git-path hooks)/p
need to run `link.sh` by hand (`bash ~/.config/claude/link.sh`) if a link later gets clobbered
(e.g. Claude's `/config` replaces one with a plain file) — it's idempotent and self-healing.
Tracked *files* in `~/.claude` link straight at the repo copy, not through `~/.config/claude`:
Claude Code follows one symlink hop and then replaces the file atomically (temp + rename), so the
old two-hop chain lost its middle link to a plain file on every settings write (`/model`, effort,
plugins, `/config`) and the repo stopped seeing changes. With one hop the rename lands in the
checkout. Dirs (`hooks`, `skills`) still go through `~/.config/claude`.
A **new skill/hook** added to the repo shows up after a re-stow — `./install.sh` (or `dotsync`) links
the new file into `~/.config/claude`, which `link.sh` then surfaces into `~/.claude`.
@@ -122,8 +151,9 @@ so Claude never auto-triggers them. Run them inside the repo whose branch you're
copy-paste markdown block, and copies it to the clipboard with `wl-copy`. Leaves verification
checkboxes for you; never creates/pushes the PR.
- **`/commit-msg [hint]`** — reads the **staged** diff, infers the repo's commit style from recent
`git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and copies
it to the clipboard with `wl-copy`. Never stages or commits — draft only.
`git log` (e.g. this repo's `[Scope] summary`), drafts a matching message, prints it, and saves
it for the `prepare-commit-msg` hook (lazygit `C` prefills it). Never stages or commits — draft
only.
- **`/verbose [on|off|status]`** — flips the *persisted* verbose view for the current project:
`viewMode` (`verbose` / `default` — the key that actually decides the view and beats a sticky
`/focus` choice for new sessions) plus `verbose` and `showThinkingSummaries`, written to the
+10 -5
View File
@@ -8,13 +8,13 @@ resize), trailing (lowest-priority) segments are dropped from the right until it
Wide — everything:
```
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 🔢 ↑63M ↓595k │ ♻️ 91% 1h │ 💾 81%
📁 ~/.dots │ 🌿 .dots main* │ 💬 #42 3 · codex 2 │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h │ 🔢 ↑63M ↓595k │ ⚡ 63 t/s │ ♻️ 91% 1h │ 💾 81%
```
Narrow — trailing segments trimmed:
```
📁 ~/.dots │ 🌿 .dots main* │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m │ 💰 $0.40 $12.00/h
📁 ~/.dots │ 🌿 .dots main* │ 💬 #42 3 · codex 2 │ 🤖 Fable 5 (1M) · xhigh │ 📝 84% (843k) │ 📊 34% 2h54m
```
Every segment is defensive: if its data is missing or a command fails, the segment is
@@ -28,11 +28,13 @@ is supplementary to the main colored value.
| | **Vim mode** | `[I]`/`[N]`/`[V]` editor mode | only with `editorMode: vim`; pair with `hideVimModeIndicator: true` so the built-in one doesn't duplicate it |
| 📁 | **Directory** | Current working dir, with `~` for home | cyan |
| 🌿 | **Git** | `repo branch` + `*` if dirty + `↑N`/`↓N` ahead/behind upstream | repo = magenta; branch = green when clean, yellow + red `*` when dirty; `↑` cyan, `↓` yellow. Omitted outside a repo |
| 💬 | **PR review threads** | `#N` of the current branch's open PR + its **unresolved review threads** (any author, outdated ones included; `+` past 100), Codex's share broken out as `· codex N`; green `✓` when none are left | A thread counts for whoever opened it (its first comment's author; Codex = login matching `codex`, as in `/pr-loop`). PR-conversation comments are not counted — they have no resolved state, so a summary or an LGTM would never let it reach zero. A GitHub round trip is ~0.7 s, so a render only reads the per-branch cache `~/.claude/cache/statusline.pr.<hash>.json` and, once it is older than 90 s, spawns a detached `statusline.py --pr-refresh` that updates it (one at a time, via a `.lock` claim) — new counts show on the next render. A failed fetch (no GitHub remote — e.g. this repo on gitea —, gh not authed, offline) backs off 10 min and keeps the last counts with a gray `?`. Omitted with no `gh`, no open PR or a detached HEAD |
| 🤖 | **Model** | Active model display name + `· effort` level (`low`/`medium`/`high`/`xhigh`) | name = blue, effort = light gray; effort omitted for models without the param |
| 📝 | **Context** | `% of context window used` + `(Nk)` tokens | usage comes from the payload's `context_window` (`used_percentage` + `current_usage`, Claude Code 2.1.x); the transcript's last `message.usage` is only a fallback. Window from `context_window.context_window_size`, then `(1M …)` in the display name, else 200k — **not** from `model.id`, which arrives with any `[1m]` suffix already stripped. Adds a red **⚠compact** at ≥80% |
| 📊 | **5h usage** | `% of the 5-hour rolling limit used` + time until it resets | from `rate_limits.five_hour`; Pro/Max only, and absent until the first API response of a session |
| 💰 | **Cost** | `$` session cost so far + `$/h` burn rate | burn rate shown once the session exceeds ~30s |
| 🔢 | **Session tokens** | `↑` input + `↓` output tokens used so far this session (cumulative, `k`/`M`) | summed over the transcript's assistant `message.usage` blocks, de-duplicated by message id (one line per content block shares the same usage). Input = `input + cache_creation + cache_read` — everything billed as input, so on a 1M session it dwarfs output (every call re-reads the cached prefix). Main transcript only: subagent usage lives in separate sidechain files. Incremental: a per-session state file `~/.claude/cache/statusline.tokens.<session>.json` remembers the byte offset already summed, so each refresh parses only the new tail (cold scan of a 6.5 MB transcript ≈ 70 ms, warm ≈ 0.1 ms); stale state files are pruned after 7 days once 50+ pile up. Omitted when there is no transcript; `↑` cyan, `↓` green, no thresholds |
| 🔢 | **Session tokens** | `↑` input + `↓` output tokens used so far this session (cumulative, `k`/`M`) | summed over the transcript's assistant `message.usage` blocks, de-duplicated by message id (one line per content block shares the same usage). Input = `input + cache_creation + cache_read` — everything billed as input, so on a 1M session it dwarfs output (every call re-reads the cached prefix). Main transcript only: subagent usage lives in separate sidechain files. Incremental: a per-session state file `~/.claude/cache/statusline.tokens.<session>.json` remembers the byte offset already summed, so each refresh parses only the new tail (warm ≈ 0.2 ms), at most 8 MiB per refresh (≈ 150 ms) — Claude Code kills a status script when the next refresh fires, so a cold scan of a huge resumed transcript is spread over a few refreshes (totals climb, then settle) instead of restarting from zero each time. Stale state files are pruned after 7 days once 50+ pile up. Omitted when there is no transcript; `↑` cyan, `↓` green, no thresholds |
| ⚡ | **Decode speed** | `N t/s` — tokens per second the model is generating | duration-weighted mean over the last 8 thinking blocks of the main conversation: `thinking_tokens / thinkingDurationMs` from the same transcript scan (blocks under 1 s or 50 tokens skipped). The only clean per-call timing Claude Code records: the payload's `cost.total_api_duration_ms` also absorbs subagent and advisor calls (it grew 28 s in a 12 s wall interval with no main-conversation completion) and its `output_tokens` is sometimes a mid-stream partial, so neither can give a rate. Includes first-token latency, so short blocks read a little low and long ones converge on the real decode speed (Fable 5.1 measured 60–85 t/s). Needs thinking enabled — omitted until a thinking block has completed, never shown with thinking off |
| ♻️ | **Prompt cache** | hit `%` (higher is better) + cache TTL (`5m` / `1h`), yellow `·cold` once it has expired | from the payload's `prompt_cache` (2.1.260+: `hit_ratio` 0–1, `ttl`, `warm`); computed from usage on older payloads. A `1h` TTL is what keeps a 10-min `/pr-loop` cadence warm |
| 💾 | **Disk** | `% used` of the filesystem at the cwd | from `statvfs` |
@@ -44,7 +46,9 @@ attention. Light gray = secondary detail. Thresholds (`green < … < yellow <
| Segment | green | yellow | red |
|---|---|---|---|
| Context | `< 50%` | `50–80%` | `≥ 80%` (⚠compact) |
| PR review threads | `✓` none unresolved | thread count | `codex N` |
| 5h usage | `< 50%` | `50–80%` | `≥ 80%` |
| Decode speed (inverted) | `≥ 40 t/s` | `20–40 t/s` | `< 20 t/s` |
| Prompt cache (inverted) | `≥ 80%` | `50–80%` | `< 50%` |
| Disk | `< 75%` | `75–90%` | `≥ 90%` |
@@ -74,5 +78,6 @@ events, so system metrics sat visibly stale between turns — the tmux status ba
## Where it lives
`statusline.py` is tracked in this repo and symlinked to `~/.claude/statusline.py`;
`settings.json` runs it via `statusLine` (`/usr/bin/env python3 ~/.claude/statusline.py`).
Edits take effect on the next status refresh.
`settings.json` runs it via `statusLine` (`/usr/bin/env python3 ~/.claude/statusline.py`) on every
message event and, through `refreshInterval: 60`, once a minute while the session idles — so the PR
count moves while you wait on a review. Edits take effect on the next status refresh.
+112 -10
View File
@@ -1,12 +1,24 @@
#!/usr/bin/env bash
# Regression matrix for git-guard.py: `bash ~/.claude/hooks/git-guard-test.sh` (exit 1 on mismatch).
# Kept as a file on purpose: typed inline, the very commands under test would trip the live hook.
G="$(dirname "$(readlink -f "$0")")/git-guard.py"; fail=0
G="$(dirname "$(readlink -f "$0")")/git-guard.py"
fail=0
# No exempt repository for the rows that do not ask for one, whatever this host's ~/.claude/git-guard-exempt says.
export GIT_GUARD_EXEMPT=""
# `git reset <one arg>` is allowed only for an existing path: give the matrix one to point at.
tmp=$(mktemp -d) && cd "$tmp" && touch existing.py
trap 'rm -rf "$tmp"' EXIT
run() { printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null; rc=$?
got=allow; [ $rc -eq 2 ] && got=block; [ "$got" = "$1" ] && mark=ok || { mark=MISMATCH; fail=1; }; printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"; }
run() {
printf '%s' "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$2")}}" | python3 "$G" 2>/dev/null
rc=$?
got=allow
[ $rc -eq 2 ] && got=block
[ "$got" = "$1" ] && mark=ok || {
mark=MISMATCH
fail=1
}
printf '%-8s %-6s %-6s %s\n' "$mark" "$1" "$got" "$(printf '%s' "$2" | head -1)"
}
echo "--- must block: commits / history ---"
run block 'git push'
run block 'git -C /home/coja/.dots push origin main'
@@ -28,7 +40,10 @@ run block 'git commit -m "fix: grep pattern; not a command"'
run block 'git revert HEAD'
run block 'git cherry-pick abc123'
run block 'git rebase -i HEAD~3'
run block 'git rebase main'
run block 'git rebase --interactive main'
run block 'git -C . rebase -i --autosquash main'
run block 'git rebase --autostash origin/main'
run block 'git -c rebase.autoStash=true rebase main'
run block 'git merge feature'
run block 'git merge --no-ff feature'
echo "--- must block: discards working-tree changes ---"
@@ -63,11 +78,16 @@ run block 'git stash clear'
run block 'git branch -D feature'
run block 'git branch -f main HEAD~1'
run block 'git branch -M main trunk'
echo "--- must block: gh pr write ops ---"
echo "--- must block: gh pr write ops (create only with --head, edit only title/body) ---"
run block 'gh pr merge 12 --squash'
run block 'gh -R o/r pr create --fill'
run block 'gh pr edit 3 --title x'
run block 'gh pr close 4'
run block 'gh -R o/r pr create --fill'
run block 'gh pr create --title x --body y'
run block 'gh pr create --dry-run --fill'
run block 'gh pr edit 3 --add-reviewer bob'
run block 'gh pr edit 3 --title x --base main'
run block 'gh pr edit --add-label=bug 3'
run block 'gh pr edit 3 -b y -m v2'
echo "--- must allow ---"
run allow 'git status'
run allow 'git log -1 --oneline'
@@ -88,6 +108,13 @@ run allow 'git checkout main'
run allow 'git checkout -b feature'
run allow 'git switch main'
run allow 'git merge --abort'
run allow 'git rebase main'
run allow 'git rebase origin/main'
run allow 'git rebase -Xtheirs main'
run allow 'git rebase --continue'
run allow 'git rebase --abort'
run allow 'git -c core.editor=true rebase --continue'
run allow 'GIT_EDITOR=true git rebase --continue'
run allow 'git stash'
run allow 'git stash list'
run allow 'git stash push -m wip'
@@ -110,12 +137,87 @@ run allow 'python3 - <<EOF
print("git commit is manual")
EOF'
run allow 'gh pr view 12 --json updatedAt'
run allow 'gh pr create --head feat --title x --body y'
run allow 'gh pr create -H feat --fill'
run allow 'gh -R o/r pr create --head=feat --fill'
run allow 'gh pr create --help'
run allow 'gh pr edit 3 --title x'
run allow 'gh pr edit 3 -t x -b "- item --base"'
run allow 'gh pr edit --body-file - 12'
run allow 'gh pr edit 12 -F body.md --repo o/r'
run allow 'gh pr edit 12 --title=x -bBody'
run allow 'gh workflow run ci.yml --ref main'
run allow 'gh run rerun 123 --failed'
run allow 'gh api graphql -f query="mutation{...}"'
run allow 'gh auth status'
run allow 'ls -la HEAD#x; git status'
run allow 'git log --grep="git push" -5'
run allow '~/.config/git/claude-commit-msg list'
echo "--- exempt repo (GIT_GUARD_EXEMPT = a scratch repo; commit and merge pass there, nothing else) ---"
# runin <expect> <cwd> <command>: the hook input carries the session's cwd, as Claude Code sends it.
R=$tmp/repo
WT=$tmp/repo-wt
git init -q "$R" && git -C "$R" -c user.name=t -c user.email=t@t commit -q --allow-empty -m init \
&& git -C "$R" worktree add -q "$WT" 2>/dev/null && mkdir -p "$R/sub" && git init -q "$tmp/other"
runin() {
printf '%s' "$(python3 -c 'import json,sys; print(json.dumps({"tool_name":"Bash","tool_input":{"command":sys.argv[1]},"cwd":sys.argv[2]}))' "$3" "$2")" | GIT_GUARD_EXEMPT=$R python3 "$G" 2>/dev/null
rc=$?
got=allow
[ $rc -eq 2 ] && got=block
[ "$got" = "$1" ] && mark=ok || {
mark=MISMATCH
fail=1
}
printf '%-8s %-6s %-6s [%s] %s\n' "$mark" "$1" "$got" "${2#"$tmp"/}" "$(printf '%s' "$3" | head -1)"
}
runin allow "$R" 'git commit -m "docs: x"'
runin allow "$R" 'git commit -m x -- docs/usage.md'
runin allow "$R" 'git commit -m"fix a bug"'
runin allow "$R" 'git commit -m "-a is not the flag here"'
runin allow "$R" 'git commit --allow-empty -m x'
runin allow "$R/sub" 'git commit -m x'
runin allow "$R" 'git merge --ff-only feature'
runin allow "$R" 'git merge --no-ff feature'
runin allow "$WT" 'git commit -m x'
runin allow "$tmp" "git -C $WT commit -m x"
runin allow "$tmp" "cd $R && git commit -m x"
runin allow "$tmp" 'cd repo && git commit -m x'
runin allow "$tmp" "cd $WT; git status; git merge main"
runin allow "$R" "sh -c 'git commit -m x'"
runin block "$R" 'git push'
runin block "$R" 'git commit --amend --no-edit'
runin block "$R" 'git commit -am wip'
runin block "$R" 'git commit -qa -m wip'
runin block "$R" 'git commit --all -m wip'
runin block "$R" 'git rebase -i main'
runin block "$R" 'git cherry-pick abc123'
runin block "$R" 'git revert HEAD'
runin block "$R" 'git reset --hard HEAD~1'
runin block "$R" 'git checkout -- file.py'
runin block "$R" 'GIT_DIR=/x/.git git commit -m x'
runin block "$R" 'env GIT_WORK_TREE=/x git commit -m x'
runin block "$R" 'git --git-dir=/x/.git commit -m x'
runin block "$R" 'git -C /tmp commit -m x'
runin block "$R" 'git -C $(mktemp -d) commit -m x'
runin block "$tmp" "cd $R && cd /tmp && git commit -m x"
runin block "$R" 'cd - && git commit -m x'
runin block "$R" 'cd $HOME && git commit -m x'
runin block "$R" 'pushd /tmp && git commit -m x'
runin block "$tmp" 'git commit -m x'
runin block "$tmp/other" 'git commit -m x'
runin block "$tmp" "git -C $tmp/other merge x"
echo "--- fail-open ---"
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"; [ $? -eq 0 ] && echo "ok non-Bash tool ignored" || { echo "MISMATCH non-Bash"; fail=1; }
printf 'not json' | python3 "$G"; [ $? -eq 0 ] && echo "ok garbage stdin passes" || { echo "MISMATCH garbage"; fail=1; }
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || { echo "GIT-GUARD MATRIX: mismatches above"; exit 1; }
printf '%s' '{"tool_name":"Edit","tool_input":{"command":"git push"}}' | python3 "$G"
[ $? -eq 0 ] && echo "ok non-Bash tool ignored" || {
echo "MISMATCH non-Bash"
fail=1
}
printf 'not json' | python3 "$G"
[ $? -eq 0 ] && echo "ok garbage stdin passes" || {
echo "MISMATCH garbage"
fail=1
}
[ $fail -eq 0 ] && echo "GIT-GUARD MATRIX: all ok" || {
echo "GIT-GUARD MATRIX: mismatches above"
exit 1
}
+179 -18
View File
@@ -7,15 +7,24 @@ command text, so `git -C . push`, `git -c k=v commit`, `/usr/bin/git push`,
This hook parses each shell segment instead: finds the git/gh token wherever it sits,
skips git's global options (`-C dir`, `-c k=v`, `--git-dir=...`) and blocks
git commit | push | revert | cherry-pick | rebase | merge (not --abort/--quit/--continue)
git commit | push | revert | cherry-pick | merge (not --abort/--quit/--continue)
git rebase -i / --interactive / --autostash
git clean | reset (any form that moves HEAD or unstages everything; `reset -- <paths>`,
`reset HEAD <path>` and `reset <existing path>` are fine) | restore <paths> (--staged
alone is fine, except `--staged .` / `:/`) | checkout -- / checkout . / -f
git stash drop | pop | clear git branch -D / -f / -M
gh pr merge | create | close | edit
gh pr merge | close | create without --head | edit beyond --title/--body
i.e. anything that commits, rewrites history, or discards working-tree changes -- the last
group matters most with several sessions sharing one checkout. Mirrors pi's permission-gate.
On GitHub Claude may open a PR (settings.json asks first) and retitle / redescribe one, nothing
more: `gh pr create` must name its branch with --head, without which gh pushes it itself (even
under --dry-run), and `gh pr edit` must not touch reviewers, labels, base or the like.
A plain `git rebase` (and its --continue / --abort) passes: Claude rebases and fixes the
conflicts when the user asks (global CLAUDE.md), and settings.json asks before each one, as
pi's gate does. `-i` stays blocked (the squash/reword tool, and it needs an editor), and so
does --autostash: it stashes and re-pops other sessions' uncommitted edits, where a plain
rebase refuses to start on a dirty tree.
Exit 2 + one stderr line = the call is blocked and Claude reads the reason (stage and
stop; /commit-msg drafts the message). Mirrors pi's permission-gate.ts.
@@ -31,15 +40,27 @@ README that documents pushing must stay writable -- so a script piped in through
is not inspected. Aliases (`git ci`) are not expanded. The deny list, the auto-mode
classifier and the global CLAUDE.md still cover those; this is the belt for the prefix
rules' braces. Fails open: any parse error lets the command through (exit 0).
Exempt repositories (one main checkout per line in ~/.claude/git-guard-exempt, host-local and
outside the dots repo on purpose: it names projects): there `git commit` and `git merge` pass
-- the user's decision for repos where Claude commits and merges itself. The repo is the one the call
actually targets: the hook input's `cwd`, moved by every `cd` before it in the command and by
`git -C`, resolved to its main checkout (so its worktrees count). Anything that hides the
target (`cd -`, `cd $VAR`, a substitution, `--git-dir`, `GIT_DIR=`) is judged not exempt.
`commit --amend` and `commit -a`/`--all` stay blocked there (they rewrite the last commit of
a shared checkout, or sweep other sessions' edits in), as do push, `rebase -i` and the discard
group. Known gap: a `cd` inside `( ... )` leaks into the commands after the parenthesis.
"""
import json
import os
import re
import shlex
import subprocess
import sys
GIT_BLOCK = {"commit", "push", "clean", "revert", "cherry-pick", "rebase"}
GIT_BLOCK = {"commit", "push", "clean", "revert", "cherry-pick"}
MERGE_OK = {"--abort", "--quit", "--continue"}
REBASE_BLOCK = {"-i", "--interactive", "--autostash"}
STASH_BLOCK = {"drop", "pop", "clear"}
BRANCH_FORCE = {"-D", "-f", "--force", "-M"}
RESET_MODES = {"--hard", "--soft", "--merge", "--keep"}
@@ -50,7 +71,20 @@ _HEX = re.compile(r"^[0-9a-f]{7,40}$")
def looks_like_ref(a):
return (a in ("HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD") or a.startswith("refs/")
or "~" in a or "^" in a or "@{" in a or bool(_HEX.match(a)))
GH_PR_BLOCK = {"merge", "create", "close", "edit"}
GH_PR_BLOCK = {"merge", "close"}
# `gh pr edit` may only retitle and redescribe; these take their value as the next word (or as
# `--title=x` / `-tx`).
GH_EDIT_OK = {"-t", "--title", "-b", "--body", "-F", "--body-file", "-R", "--repo"}
# Repositories (main checkouts; their worktrees count) where Claude commits and merges itself:
# one path per line, `#` comments; a missing file exempts nothing. GIT_GUARD_EXEMPT
# (colon-separated) replaces the file for the test matrix; it is read from the hook's own
# environment (Claude Code's), which a prefix on the checked command cannot reach.
EXEMPT_FILE = "~/.claude/git-guard-exempt"
EXEMPT_VERBS = {"commit", "merge"}
# `git commit` options whose value is the next word (or, for the short ones, the rest of the word).
COMMIT_ARG_SHORT = set("mFCct")
COMMIT_ARG_LONG = {"--message", "--file", "--author", "--date", "--reuse-message", "--reedit-message",
"--fixup", "--squash", "--template", "--trailer", "--cleanup", "--pathspec-from-file"}
# global options that take a *separate* argument; the `--opt=value` spelling is one token.
GIT_OPT_ARG = {"-C", "-c", "--git-dir", "--work-tree", "--exec-path", "--namespace",
"--super-prefix", "--config-env"}
@@ -72,13 +106,102 @@ def _skip_opts(args, with_arg):
return j
def git_reason(args):
def exempt_repos():
env = os.environ.get("GIT_GUARD_EXEMPT")
if env is not None:
paths = env.split(":")
else:
try:
with open(os.path.expanduser(EXEMPT_FILE)) as f:
paths = [ln.split("#", 1)[0].strip() for ln in f]
except OSError:
return set()
return {os.path.realpath(os.path.expanduser(p)) for p in paths if p}
def main_checkout(d):
"""The main checkout of the repository `d` lies in (a worktree resolves to it), or None."""
try:
out = subprocess.run(["git", "-C", d, "rev-parse", "--path-format=absolute", "--git-common-dir"],
capture_output=True, text=True, timeout=3)
except (OSError, subprocess.SubprocessError):
return None
common = out.stdout.strip().rstrip("/")
if out.returncode != 0 or not common:
return None
return os.path.realpath(os.path.dirname(common))
def in_exempt_repo(opts, cwd, pre):
"""Whether a git call with global options `opts`, run from `cwd` after the words `pre` of
its own command (`env X=1`, ...), targets an exempt repository."""
exempt = exempt_repos()
if not exempt or cwd is None or any(t.startswith(("GIT_DIR=", "GIT_WORK_TREE=", "GIT_COMMON_DIR="))
for t in pre):
return False
d, j = cwd, 0
while j < len(opts):
a = opts[j]
if a in ("--git-dir", "--work-tree") or a.startswith(("--git-dir=", "--work-tree=")):
return False
if a == "-C":
if j + 1 >= len(opts) or "SUBST" in opts[j + 1] or "$" in opts[j + 1]:
return False
d = os.path.join(d, os.path.expanduser(opts[j + 1]))
j += 2 if a in GIT_OPT_ARG else 1
return main_checkout(d) in exempt
def commit_rewrites_or_sweeps(rest):
"""`commit --amend` (rewrites the last commit) or `-a`/`--all` (takes in every modified
tracked file, other sessions' edits included)."""
skip = False
for a in rest:
if skip:
skip = False
continue
if a == "--":
break
if a in ("--amend", "--all"):
return True
if a in COMMIT_ARG_LONG:
skip = True
elif a.startswith("-") and not a.startswith("--"):
for k, ch in enumerate(a[1:], 1):
if ch == "a":
return True
if ch in COMMIT_ARG_SHORT:
skip = k == len(a) - 1 # the value is the next word
break
return False
def change_dir(cwd, args):
"""Where `cd args` leaves the shell started in `cwd`; None when that cannot be told."""
args = [a for a in args if a not in ("-L", "-P", "--")]
if not args:
return os.path.expanduser("~")
t = args[0]
if t == "-" or "SUBST" in t or "$" in t or (cwd is None and not t.startswith(("/", "~"))):
return None
return os.path.normpath(os.path.join(cwd or "/", os.path.expanduser(t)))
def git_reason(args, cwd=None, pre=()):
j = _skip_opts(args, GIT_OPT_ARG)
if j >= len(args):
return None
sub, rest = args[j], args[j + 1:]
if (sub in EXEMPT_VERBS and not (sub == "commit" and commit_rewrites_or_sweeps(rest))
and in_exempt_repo(args[:j], cwd, pre)):
return None
if sub in GIT_BLOCK:
return f"git {sub}"
if sub == "rebase":
flag = next((a for a in rest if a in REBASE_BLOCK), None)
if flag == "--autostash" or any(o.lower().startswith("rebase.autostash") for o in args[:j]):
return "git rebase --autostash (stashes other sessions' uncommitted edits)"
return f"git rebase {flag} (interactive history rewrite)" if flag else None
if sub == "reset":
opts = [a for a in rest if a.startswith("-") and a != "--"]
if RESET_MODES & set(opts):
@@ -109,14 +232,41 @@ def git_reason(args):
return None
def gh_edit_extra(rest):
"""The first `gh pr edit` option that is neither the title nor the body, else None."""
k = 0
while k < len(rest):
a = rest[k]
if a == "--":
break
name = a.split("=", 1)[0]
if name in GH_EDIT_OK:
k += 2 if name == a else 1 # skip the value, which may well start with `-`
continue
if a.startswith("-") and not (a[:2] in GH_EDIT_OK and not a.startswith("--")):
return a
k += 1
return None
def gh_reason(args):
j = _skip_opts(args, GH_OPT_ARG)
if j >= len(args) or args[j] != "pr":
return None
rest = args[j + 1:]
k = _skip_opts(rest, GH_OPT_ARG)
if k < len(rest) and rest[k] in GH_PR_BLOCK:
return f"gh pr {rest[k]}"
if k >= len(rest) or "--help" in rest:
return None
sub, rest = rest[k], rest[k + 1:]
if sub in GH_PR_BLOCK:
return f"gh pr {sub}"
if sub == "create" and not any(a in ("-H", "--head") or a.startswith("--head=")
or (a.startswith("-H") and not a.startswith("--")) for a in rest):
return "gh pr create without --head: gh would push the branch itself"
if sub == "edit":
extra = gh_edit_extra(rest)
if extra:
return f"gh pr edit {extra.split('=', 1)[0]}: only --title / --body edits are allowed"
return None
@@ -146,8 +296,8 @@ def segments(toks):
yield seg
def scan(text, depth=0):
"""Reason string for the first blocked invocation in `text`, else None."""
def scan(text, depth=0, cwd=None):
"""Reason string for the first blocked invocation in `text` (run from `cwd`), else None."""
if depth > 4:
return None
text = _HEREDOC.sub(" ", text)
@@ -163,23 +313,29 @@ def scan(text, depth=0):
while prev != text:
prev, text = text, _SUBST.sub(_grab, text)
for body in bodies:
r = scan(body, depth + 1)
r = scan(body, depth + 1, cwd)
if r:
return r
for toks in segments(tokenize(text)):
if toks[0] == "cd":
cwd = change_dir(cwd, toks[1:])
continue
if toks[0] in ("pushd", "popd"):
cwd = None
continue
for i, tok in enumerate(toks):
base = os.path.basename(tok)
r = None
if base == "git":
r = git_reason(toks[i + 1:])
r = git_reason(toks[i + 1:], cwd, toks[:i])
elif base == "gh":
r = gh_reason(toks[i + 1:])
elif base == "eval":
r = scan(" ".join(toks[i + 1:]), depth + 1)
r = scan(" ".join(toks[i + 1:]), depth + 1, cwd)
elif base in SHELLS and "-c" in toks[i + 1:]:
k = toks.index("-c", i + 1) # `sh -c '<cmd>'`: the real command is one word
if k + 1 < len(toks):
r = scan(toks[k + 1], depth + 1)
r = scan(toks[k + 1], depth + 1, cwd)
if r:
return r
return None
@@ -191,16 +347,21 @@ def main():
if data.get("tool_name", "Bash") != "Bash":
return
cmd = (data.get("tool_input") or {}).get("command") or ""
reason = scan(cmd) if cmd else None
reason = scan(cmd, 0, data.get("cwd") or os.getcwd()) if cmd else None
except Exception:
return # fail open: the deny list still applies
if reason:
short = cmd.strip().splitlines()[0]
short = short if len(short) <= 80 else short[:77] + "..."
sys.stderr.write(
f"git-guard: blocked `{short}` ({reason}). The user commits, pushes and merges "
"manually (global CLAUDE.md): stage and stop, and draft the message with /commit-msg.\n"
)
hint = ("On a PR Claude reads, opens one when asked (with --head <pushed branch>) and edits its "
"--title / --body; pushing, merging, closing and every other edit are the user's."
if reason.startswith("gh ") else
"Rebase with a plain `git rebase` on a clean tree, and only when the user asked; if the "
"tree holds other sessions' edits, tell the user instead of stashing them."
if reason.startswith("git rebase") else
"The user commits, pushes and merges manually (global CLAUDE.md): stage and stop, "
"and draft the message with /commit-msg.")
sys.stderr.write(f"git-guard: blocked `{short}` ({reason}). {hint}\n")
sys.exit(2)
+3 -1
View File
@@ -19,7 +19,9 @@
"context": "Global",
"bindings": {
"alt+j": "scroll:lineDown",
"alt+k": "scroll:lineUp"
"alt+k": "scroll:lineUp",
"ctrl+shift+j": "scroll:lineDown",
"ctrl+shift+k": "scroll:lineUp"
}
},
{
+14 -4
View File
@@ -41,6 +41,16 @@ items="settings.json statusline.py keybindings.json CLAUDE.md hooks skills"
# Expose each item in ~/.claude (where Claude actually reads).
for item in $items; do
[ -e "$SRC/$item" ] || continue
# Files link straight at the repo copy, not through ~/.config/claude: Claude Code
# resolves ONE symlink hop, then writes atomically (temp + rename) at that path —
# verified on 2.1.296 with `claude plugin disable` against a scratch CLAUDE_CONFIG_DIR.
# Through a ~/.claude -> ~/.config/claude -> repo chain, every settings write (/model,
# effort, plugins, /config) swapped the middle link for a plain file and severed the
# repo. One hop puts the rename inside the checkout, so both links survive. A SRC file
# that already drifted into a plain file keeps being the target (it may hold the newest
# edits) until the drift guard below is reconciled. Dirs keep the stow path.
target="$SRC/$item"
[ -f "$repo/$item" ] && [ -L "$SRC/$item" ] && target="$repo/$item"
if [ -d "$DST/$item" ] && [ ! -L "$DST/$item" ]; then
echo "skip ~/.claude/$item (real directory in the way — move it aside first)" >&2
continue
@@ -62,14 +72,14 @@ for item in $items; do
if [ "$same" != 1 ]; then
echo "DRIFT ~/.claude/$item is a real file that differs from the repo copy —" >&2
echo " it may hold newer edits. Review, then re-link:" >&2
echo " diff '$DST/$item' '$SRC/$item' # compare; keep whichever is right" >&2
echo " ln -sfn '$SRC/$item' '$DST/$item' # re-link" >&2
echo " diff '$DST/$item' '$target' # compare; keep whichever is right" >&2
echo " ln -sfn '$target' '$DST/$item' # re-link" >&2
continue
fi
echo "healed ~/.claude/$item (plain copy identical to repo — re-linking)"
fi
ln -sfn "$SRC/$item" "$DST/$item"
echo "linked ~/.claude/$item -> ~/.config/claude/$item"
ln -sfn "$target" "$DST/$item"
echo "linked ~/.claude/$item -> ${target/#$HOME/\~}"
done
# Stow-layer drift guard: each SRC *file* should be a symlink into the repo, so edits
+116 -8
View File
@@ -4,23 +4,60 @@
"pr": ""
},
"permissions": {
"allow": [
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(sed -n:*)",
"Bash(grep:*)",
"Bash(rg:*)",
"Bash(ls:*)",
"Bash(wc:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git show:*)",
"Bash(git rev-parse:*)",
"Bash(git branch:*)",
"Bash(git stash list:*)",
"Bash(pnpm run:*)",
"Bash(pnpm exec:*)",
"Bash(node -p:*)",
"Bash(gh pr view:*)",
"Bash(gh pr list:*)",
"Bash(gh pr diff:*)",
"Bash(gh pr checks:*)",
"Bash(gh pr status:*)",
"Bash(gh issue view:*)",
"Bash(gh issue list:*)",
"Bash(gh issue status:*)",
"Bash(gh run view:*)",
"Bash(gh run list:*)",
"Bash(gh run watch:*)",
"Bash(gh workflow view:*)",
"Bash(gh workflow list:*)",
"Bash(gh repo view:*)",
"Bash(gh release view:*)",
"Bash(gh release list:*)",
"Bash(gh search:*)",
"Bash(gh label list:*)",
"Bash(gh status:*)",
"Bash(gh auth status:*)",
"Bash(gh pr edit:*)",
"Bash(gh workflow run:*)",
"Bash(gh run rerun:*)"
],
"deny": [
"Bash(git push)",
"Bash(git push:*)",
"Bash(git commit)",
"Bash(git commit:*)",
"Bash(git reset --hard)",
"Bash(git reset --hard:*)",
"Bash(git clean)",
"Bash(git clean:*)",
"Bash(gh pr merge)",
"Bash(gh pr merge:*)",
"Bash(gh pr create)",
"Bash(gh pr create:*)",
"Bash(gh pr close)",
"Bash(gh pr close:*)",
"Bash(gh pr edit)",
"Bash(gh pr edit:*)",
"Read(**/.env)",
"Read(**/.env.*)",
"Read(**/*.env)",
@@ -42,9 +79,15 @@
"Read(**/.gnupg/**)",
"Read(**/fish_variables)"
],
"ask": [
"Bash(gh pr create)",
"Bash(gh pr create:*)",
"Bash(git rebase)",
"Bash(git rebase:*)"
],
"defaultMode": "auto"
},
"model": "claude-fable-5-1[1m]",
"model": "opus",
"hooks": {
"SessionStart": [
{
@@ -67,6 +110,16 @@
"timeout": 5
}
]
},
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc busy; exit 0",
"timeout": 5,
"async": true
}
]
}
],
"UserPromptExpansion": [
@@ -125,6 +178,51 @@
"timeout": 5
}
]
},
{
"matcher": "idle_prompt|permission_prompt",
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc input; exit 0",
"timeout": 5,
"async": true
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc done; exit 0",
"timeout": 5,
"async": true
}
]
}
],
"StopFailure": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc error; exit 0",
"timeout": 5,
"async": true
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "f=\"$HOME/.config/tmux/scripts/agent-state.sh\"; [ -x \"$f\" ] && \"$f\" cc off; exit 0"
}
]
}
]
},
@@ -136,13 +234,23 @@
"type": "command",
"command": "/usr/bin/env python3 ~/.claude/statusline.py",
"padding": 0,
"refreshInterval": 60,
"hideVimModeIndicator": true
},
"enabledPlugins": {
"typescript-lsp@claude-plugins-official": true,
"frontend-design@claude-plugins-official": true
"frontend-design@claude-plugins-official": true,
"rust-analyzer-lsp@claude-plugins-official": true
},
"effortLevel": "xhigh",
"modelSettings": {
"claude-fable-5-1": {
"effortLevel": "xhigh"
},
"claude-opus-5-5": {
"effortLevel": "xhigh"
}
},
"advisorModel": "fable",
"tui": "fullscreen",
"skipWorkflowUsageWarning": true,
@@ -1,8 +1,8 @@
---
description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply, save it to the repo's CLAUDE_COMMIT_MSG file, and copy it to the clipboard. Never stages or commits. Use once you've staged what you want to commit.
description: Draft a commit message for the staged changes, matching this repo's commit style, then print it in the reply and save it to the repo's CLAUDE_COMMIT_MSG file. Never stages or commits. Use once you've staged what you want to commit.
argument-hint: [optional focus/scope hint]
disable-model-invocation: true
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Glob
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(~/.config/git/claude-commit-msg:*), Read, Glob
---
# Draft a commit message for the staged changes
@@ -26,9 +26,8 @@ Context for the current commit — what's staged, plus this repo's recent style:
- Add a short body (wrapped ~72 cols) only when the change needs the *why*; otherwise
subject-only. Don't invent motivation that isn't evident from the diff.
4. **Print the message in your reply first** — as a single fenced ` ```text ` block, copy-paste
ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even if
the clipboard gets overwritten later (by me copying something else, or another Claude session
running `wl-copy`). Always print it; don't rely on the clipboard alone.
ready. This in-reply copy is the durable one: it lives in the transcript, so it survives even
after the hook has consumed the handoff. Always print it.
5. **Hand it off** so I can recover it any time — feed the raw message to the CLI as a quoted
heredoc (so quotes, backticks and `$` in the body survive untouched):
```sh
@@ -41,8 +40,6 @@ Context for the current commit — what's staged, plus this repo's recent style:
`prepare-commit-msg` hook prefills the next `git commit` / lazygit `C` with the message(s) whose
paths are part of that commit, then consumes them. Never write `CLAUDE_COMMIT_MSG` or
`COMMIT_EDITMSG` directly. The `save` output names the saved id and file path.
6. **Also copy it to the clipboard** as a convenience: `wl-copy < <file path printed by save>`.
Then tell me all three places it is: **printed above**, **saved for the hook** (with its id),
and **on the clipboard** — so I never lose it to a stray copy.
7. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit
Then tell me both places it is: **printed above** and **saved for the hook** (with its id).
6. **Never** run `git add`, `git commit`, or `git push`, and never offer to — I stage and commit
manually. Draft only.
@@ -2,7 +2,7 @@
description: Babysit the current branch's PR — address review comments that don't need my input, re-check every ~10 min, and stop when there are no open comments left or the automated reviewers (Codex, Claude) hit their limit. Prep-only — stages fixes and drafts the commit message; never commits, pushes, or merges.
argument-hint: [pr number or url — optional; defaults to the current branch's PR]
disable-model-invocation: true
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(wl-copy:*), Bash(~/.config/git/claude-commit-msg:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
allowed-tools: Bash(gh:*), Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git rev-parse:*), Bash(git fetch:*), Bash(git add:*), Bash(~/.config/git/claude-commit-msg:*), Read, Edit, Write, Grep, Glob, ScheduleWakeup
---
# Watch this PR and resolve review comments (prep-only)
@@ -238,8 +238,7 @@ Current state (auth + the current branch's PR):
quoted heredoc, so quotes and backticks survive) — it is stored with the paths you staged this cycle, so the `prepare-commit-msg` hook prefills it only
for the commit that contains them (other sessions' handoffs are not overwritten). Never write
`CLAUDE_COMMIT_MSG` / `COMMIT_EDITMSG` directly.
- **(c) Copy it:** `wl-copy < <file path printed by save>`.
- Tell me all three locations. **Never** run `git commit`.
- Tell me both locations. **Never** run `git commit`.
(Skip this step when nothing new was staged this cycle.)
10. **Ping + schedule.** Give me a tight summary — deltas only, don't restate unchanged threads:
@@ -271,9 +270,10 @@ Current state (auth + the current branch's PR):
- **Never** `git commit`, `git push`, `gh pr merge`, `gh pr create`, `gh pr close`, or `gh pr edit`
— I do all of those. You stage, react, resolve, and draft; nothing more. The only comment you may
post is a needs-input decision question. This is enforced, not just asked: `settings.json` denies
those commands and the `git-guard.py` PreToolUse hook blocks any spelling of them, so a slip is
refused with a reason rather than executed.
post is a needs-input decision question. Commit, push, merge and close are enforced, not just
asked: `settings.json` denies them and the `git-guard.py` PreToolUse hook blocks any spelling of
them. `gh pr create` / `edit` are allowed globally (create asks first), so for this loop the rule
above is the only thing holding them back.
- Act only on the target PR. Don't touch unrelated files or other PRs.
- **Keep context lean:** no raw JSON dumps in replies, no re-listing threads that haven't changed,
idle cycles ≤2 lines. Every line you emit is re-read (uncached) on every later cycle.
+217 -30
View File
@@ -2,13 +2,15 @@
"""Claude Code status line.
One line: vim mode | dir (repo-relative, hidden at the root) |
git(repo/branch +dirty +ahead/behind) | model (+effort) |
git(repo/branch +dirty +ahead/behind) | PR review threads (open count, Codex's share) | model (+effort) |
context% (+compact warn) | 5h usage (+reset eta) | cost (+burn rate) |
session tokens (cumulative in/out) | prompt-cache hit% (+ttl, cold marker) | disk. If the
session tokens (cumulative in/out) | decode speed t/s (thinking blocks) |
prompt-cache hit% (+ttl, cold marker) | disk. If the
rendered line would overflow the terminal, trailing (lowest-priority) segments are
dropped until it fits.
(Dormant, re-addable in main(): ram, cpu%, temp -- retired 2026-07-26 because the
status line only refreshes on message events, so system metrics sat visibly stale;
status line then only refreshed on message events, so system metrics sat visibly stale
(settings.json's 60 s refreshInterval, added for the PR count, is still too coarse for them);
tmux's status bar owns those now -- plus velocity, cache hit %, api ratio, version,
style.)
@@ -16,6 +18,7 @@ Reads the status JSON from stdin (Claude Code statusLine command). Every segment
is wrapped defensively so the status line can never crash the UI -- on any error
a segment is simply omitted.
"""
import hashlib
import json
import os
import re
@@ -159,6 +162,9 @@ def last_usage(data):
# --- session token totals (transcript, incremental) ---------------------------
TOKENS_STATE_DIR = os.path.expanduser("~/.claude/cache")
TOKENS_RECENT_IDS = 20 # message ids remembered across runs for de-duplication
TPS_SAMPLES = 8 # thinking blocks the t/s readout is averaged over
TPS_MIN_MS, TPS_MIN_TOKENS = 1000, 50 # shorter blocks are first-token latency, not decode speed
TOKENS_SCAN_BUDGET = 8 << 20 # bytes parsed per refresh (~0.1 s); see session_tokens
TOKENS_STATE_MAX_AGE = 7 * 86400 # prune per-session state files older than this
@@ -183,9 +189,10 @@ def _tokens_state_path(data, path):
def _prune_tokens_state(keep):
"""Drop stale per-session state files (one scandir, only when the dir has piled up)."""
"""Drop stale per-session token and per-branch PR state files (one scandir, only when
the dir has piled up)."""
try:
entries = [e for e in os.scandir(TOKENS_STATE_DIR) if e.name.startswith("statusline.tokens.")]
entries = [e for e in os.scandir(TOKENS_STATE_DIR) if e.name.startswith(("statusline.tokens.", "statusline.pr."))]
if len(entries) < 50:
return
cutoff = time.time() - TOKENS_STATE_MAX_AGE
@@ -197,12 +204,21 @@ def _prune_tokens_state(keep):
def session_tokens(data):
"""Cumulative (input, output) tokens this session, summed over the transcript's
assistant `message.usage` blocks -- or None when there is no transcript.
"""Cumulative session token totals plus decode-speed samples, from the transcript --
`{"in", "out", "tps"}` or None when there is no transcript.
Input = input + cache_creation + cache_read (everything the API billed as input), so
on a 1M session it dwarfs output: every call re-reads the whole cached prefix.
Only the main transcript is counted; subagent usage lives in separate sidechain files.
in/out: summed over assistant `message.usage` blocks. Input = input + cache_creation +
cache_read (everything the API billed as input), so on a 1M session it dwarfs output:
every call re-reads the whole cached prefix. Only the main transcript is counted;
subagent usage lives in separate sidechain files.
tps: the last TPS_SAMPLES `[thinking_tokens, thinkingDurationMs]` pairs -- the only
clean per-call timing Claude Code records. The status payload's cost.total_api_duration_ms
is useless for a rate: subagent and advisor calls land in the same counter (it grew 28s
in a 12s wall interval with no main-conversation completion), and the payload's
output_tokens is sometimes a mid-stream partial. thinkingDurationMs is main-conversation
only; it includes first-token latency, so short blocks read low and long ones converge
on the real decode speed -- hence the duration-weighted mean and the minimum-size filter.
Incremental: the status line runs on every refresh and transcripts grow to tens of
MB, so a per-session state file (~/.claude/cache/statusline.tokens.<session>.json)
@@ -210,12 +226,18 @@ def session_tokens(data):
de-duplicated by message id: the transcript writes one line per content block of a
turn, all carrying the same id and the same usage block. Only complete lines are
consumed (the writer may be mid-line); a shrunken file (truncate/rotate) resets.
A cold scan is spread over refreshes, TOKENS_SCAN_BUDGET bytes each: Claude Code kills
a status script when the next refresh fires, and a multi-second scan of a 100 MB
transcript that never gets to write its state would start from zero every time.
Totals climb for a few refreshes on a huge resumed transcript, then settle.
"""
path = data.get("transcript_path")
if not path or not os.path.exists(path):
return None
state_path = _tokens_state_path(data, path)
state = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": []}
fresh = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": [], "tps": [], "tps_ids": []}
state = dict(fresh)
try:
with open(state_path) as fh:
s = json.load(fh)
@@ -225,27 +247,36 @@ def session_tokens(data):
and all(isinstance(s.get(k), int) and s[k] >= 0 for k in ("offset", "in", "out"))
and isinstance(s.get("ids"), list)
):
state = s
state = s # "tps"/"tps_ids" are optional: older state files predate them
except Exception:
pass
def result(st):
return {"in": st["in"], "out": st["out"], "tps": st.get("tps") or []}
size = os.path.getsize(path)
if size < state["offset"]: # truncated or rotated under us: start over
state = {"path": path, "offset": 0, "in": 0, "out": 0, "ids": []}
state = dict(fresh)
if size == state["offset"]:
return state["in"], state["out"]
return result(state)
with open(path, "rb") as fh:
fh.seek(state["offset"])
chunk = fh.read()
chunk = fh.read(TOKENS_SCAN_BUDGET)
end = chunk.rfind(b"\n")
if end < 0 and len(chunk) == TOKENS_SCAN_BUDGET: # one line bigger than the budget: finish it
chunk += fh.readline()
end = chunk.rfind(b"\n")
if end < 0: # no complete new line yet
return state["in"], state["out"]
return result(state)
text = chunk[: end + 1].decode("utf-8", "ignore")
tin, tout = state["in"], state["out"]
ids = [i for i in state["ids"] if isinstance(i, str)]
seen = set(ids)
tps = [s for s in (state.get("tps") or []) if isinstance(s, list) and len(s) == 2]
tps_ids = [i for i in (state.get("tps_ids") or []) if isinstance(i, str)]
tps_seen = set(tps_ids)
for line in text.splitlines():
if '"usage"' not in line:
continue
@@ -258,19 +289,38 @@ def session_tokens(data):
if not isinstance(usage, dict):
continue
mid = msg.get("id") or obj.get("requestId") or obj.get("uuid")
if not isinstance(mid, str) or mid in seen:
if not isinstance(mid, str):
continue
def num(d, k):
v = d.get(k, 0) if isinstance(d, dict) else 0
return int(v) if isinstance(v, (int, float)) else 0
# Decode-speed sample: checked on every line (not just the first-wins one below) so
# it does not depend on the thinking block being the message's first line.
ms = num(obj, "thinkingDurationMs")
toks = num(usage.get("output_tokens_details"), "thinking_tokens")
if ms >= TPS_MIN_MS and toks >= TPS_MIN_TOKENS and mid not in tps_seen:
tps_seen.add(mid)
tps_ids.append(mid)
tps.append([toks, ms])
if mid in seen:
continue
seen.add(mid)
ids.append(mid)
tin += num(usage, "input_tokens") + num(usage, "cache_creation_input_tokens") + num(usage, "cache_read_input_tokens")
tout += num(usage, "output_tokens")
def num(k):
v = usage.get(k, 0)
return int(v) if isinstance(v, (int, float)) else 0
tin += num("input_tokens") + num("cache_creation_input_tokens") + num("cache_read_input_tokens")
tout += num("output_tokens")
state = {"path": path, "offset": state["offset"] + end + 1, "in": tin, "out": tout, "ids": ids[-TOKENS_RECENT_IDS:]}
state = {
"path": path,
"offset": state["offset"] + end + 1,
"in": tin,
"out": tout,
"ids": ids[-TOKENS_RECENT_IDS:],
"tps": tps[-TPS_SAMPLES:],
"tps_ids": tps_ids[-TOKENS_RECENT_IDS:],
}
try:
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
tmp = f"{state_path}.{os.getpid()}.tmp"
@@ -280,7 +330,7 @@ def session_tokens(data):
_prune_tokens_state(state_path)
except Exception:
pass
return tin, tout
return result(state)
# --- row 1: work ------------------------------------------------------------
@@ -395,6 +445,122 @@ def git_segment(cwd, top):
return ""
# --- PR review threads (gh, cached, fetched in the background) ------------------
PR_TTL = 90 # seconds a fetched count stays fresh
PR_ERR_TTL = 600 # back-off after a failed fetch (no GitHub remote, gh not authed, offline)
PR_CLAIM = 30 # a refresher spawned this recently is still running: don't start another
PR_CODEX = re.compile(r"codex", re.I) # chatgpt-codex-connector; same match as /pr-loop
PR_QUERY = """query($owner:String!,$repo:String!,$branch:String!){
repository(owner:$owner,name:$repo){
pullRequests(headRefName:$branch,states:OPEN,first:1,orderBy:{field:UPDATED_AT,direction:DESC}){
nodes{ number reviewThreads(first:100){ pageInfo{hasNextPage}
nodes{ isResolved comments(first:1){ nodes{ author{login} } } } } } } } }"""
def _pr_cache_path(top, branch):
key = hashlib.sha1(f"{top}\0{branch}".encode()).hexdigest()[:16]
return os.path.join(TOKENS_STATE_DIR, f"statusline.pr.{key}.json")
def _read_json(path):
try:
with open(path) as fh:
d = json.load(fh)
return d if isinstance(d, dict) else {}
except Exception:
return {}
def pr_refresh(cwd, branch, path):
"""Background half of pr_segment (`statusline.py --pr-refresh CWD BRANCH CACHE`): fetch the
branch's open PR and write its unresolved review-thread counts to the cache. `{owner}` /
`{repo}` are gh placeholders resolved from the repo at `cwd`. A failed fetch keeps the last
counts and flags them stale; a branch without an open PR clears them."""
state = _read_json(path)
state["ts"] = time.time()
try:
r = subprocess.run(
["gh", "api", "graphql", "-F", "owner={owner}", "-F", "repo={repo}",
"-f", f"branch={branch}", "-f", f"query={PR_QUERY}"],
cwd=cwd, capture_output=True, text=True, timeout=20,
)
if r.returncode != 0:
raise RuntimeError(r.stderr)
nodes = json.loads(r.stdout)["data"]["repository"]["pullRequests"]["nodes"]
state = {"ts": state["ts"]}
if nodes:
threads = nodes[0]["reviewThreads"]
# a thread belongs to whoever opened it: its first comment's author
authors = [((t["comments"]["nodes"] or [{}])[0].get("author") or {}).get("login") or ""
for t in threads["nodes"] if not t["isResolved"]]
state.update(number=nodes[0]["number"], open=len(authors),
codex=sum(1 for a in authors if PR_CODEX.search(a)),
more=threads["pageInfo"]["hasNextPage"])
except Exception:
state["error"] = True
try:
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
tmp = f"{path}.{os.getpid()}.tmp"
with open(tmp, "w") as fh:
json.dump(state, fh)
os.replace(tmp, path)
except Exception:
pass
def pr_segment(cwd, top):
"""Unresolved review threads (any author, outdated ones included) on the current branch's
open PR: `💬 #N ✓` when none are left, else the count with Codex's share broken out.
A GitHub round trip takes ~0.7 s, too slow for a render, so this only reads a per-branch
cache (~/.claude/cache/statusline.pr.<hash>.json) and, once it is older than PR_TTL,
spawns a detached `--pr-refresh` child to update it -- the new count shows on the next
render. PR-conversation comments are not counted: they have no resolved state, so a
summary or an LGTM would keep the count from ever reaching zero.
"""
try:
if not top or not shutil.which("gh"):
return ""
branch = _git(cwd, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
if not branch or branch == "HEAD":
return ""
path = _pr_cache_path(top, branch)
state = _read_json(path)
if time.time() - (state.get("ts") or 0) > (PR_ERR_TTL if state.get("error") else PR_TTL):
claim = path + ".lock"
try:
claimed = time.time() - os.path.getmtime(claim) < PR_CLAIM
except OSError:
claimed = False
if not claimed:
os.makedirs(TOKENS_STATE_DIR, exist_ok=True)
with open(claim, "w"):
pass
# own session + no inherited stdout: Claude Code reads ours to EOF, and
# cancels an in-flight render when the next one triggers
subprocess.Popen(
[sys.executable, os.path.abspath(__file__), "--pr-refresh", cwd, branch, path],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True,
)
num = state.get("number")
if not num:
return ""
n, codex = state.get("open") or 0, state.get("codex") or 0
out = "💬 " + color(f"#{num} ", "gray")
if not n:
out += color("✓", "green")
else:
out += color(f"{n}{'+' if state.get('more') else ''}", "yellow", bold=True)
if codex:
out += color(" · ", "gray") + color(f"codex {codex}", "red")
if state.get("error"):
out += color(" ?", "gray") # last fetch failed: these counts are stale
return out
except Exception:
return ""
def model_segment(data):
name = (data.get("model") or {}).get("display_name")
if not name:
@@ -503,13 +669,12 @@ def velocity_segment(data):
return ""
def tokens_segment(data):
def tokens_segment(totals):
"""Cumulative session tokens: ↑ input (incl. cache reads/writes) and ↓ output."""
try:
totals = session_tokens(data)
if not totals:
return ""
tin, tout = totals
tin, tout = totals["in"], totals["out"]
if not (tin or tout):
return ""
return "🔢 " + color(f"↑{fmt_tokens(tin)}", "cyan") + " " + color(f"↓{fmt_tokens(tout)}", "green")
@@ -517,6 +682,22 @@ def tokens_segment(data):
return ""
def tps_segment(totals):
"""Decode speed in tokens/s: duration-weighted mean over the last few thinking blocks
(see session_tokens). Omitted until a thinking block ≥1s has completed -- so never
shown with thinking disabled."""
try:
samples = (totals or {}).get("tps") or []
toks = sum(s[0] for s in samples)
ms = sum(s[1] for s in samples)
if toks <= 0 or ms <= 0:
return ""
rate = toks / ms * 1000
return "⚡ " + color(f"{rate:.0f} t/s", bucket(rate, 20, 40, invert=True))
except Exception:
return ""
def cache_segment(usage, data=None):
"""Prompt-cache hit % (higher is better) + the cache TTL, `·cold` when the cache has
expired. Hit % from the 2.1.260+ payload `prompt_cache.hit_ratio` (a 0..1 fraction, null
@@ -697,6 +878,9 @@ def style_segment(data):
def main():
if sys.argv[1:2] == ["--pr-refresh"] and len(sys.argv) == 5:
pr_refresh(*sys.argv[2:])
return
try:
data = json.load(sys.stdin)
except Exception:
@@ -708,6 +892,7 @@ def main():
or os.getcwd()
)
usage = payload_usage(data) or last_usage(data)
totals = session_tokens(data)
top = git_top(cwd)
# One row: work (left) then system (right), joined. If it would overflow the
@@ -720,11 +905,13 @@ def main():
vim_segment(data),
dir_segment(cwd, top),
git_segment(cwd, top),
pr_segment(cwd, top),
model_segment(data),
context_segment(data, usage),
usage_segment(data),
cost_segment(data),
tokens_segment(data),
tokens_segment(totals),
tps_segment(totals),
cache_segment(usage, data),
) if s]
system = [s for s in (
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json",
"logo": {
"source": "$(find \"${XDG_CONFIG_HOME:-$HOME/.config}/fastfetch/pngs/\" -name \"*.png\" | shuf -n 1)",
"source": "$(find \"${XDG_CONFIG_HOME:-$HOME/.config}/fastfetch/pngs/\" -name \"*.png\" -xtype f | shuf -n 1)",
"height": 18
},
"display": {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1,27 @@
# Mark the tmux window when a long shell command finishes, through the same
# ~/.config/tmux/scripts/agent-state.sh the coding agents use (tool tag "sh"):
# ✓ on exit 0, ! on failure — coloured when it happened while you were elsewhere, grey once
# visited, gone at the next command. Only commands longer than $agent_state_min_ms
# (default 30 s) count, and interactive programs are skipped: the agents mark themselves,
# and quitting a long nvim or lazygit session is not news. No-op outside tmux.
set -q agent_state_min_ms; or set -g agent_state_min_ms 30000
function __agent_state_preexec --on-event fish_preexec
set -q TMUX_PANE; or return
~/.config/tmux/scripts/agent-state.sh sh busy
end
function __agent_state_postexec --on-event fish_postexec
set -l st $status # first statement: anything before it would clobber the command's status
set -q TMUX_PANE; or return
test "$CMD_DURATION" -gt "$agent_state_min_ms"; or return
switch (string split -m1 ' ' -- $argv[1])[1]
case nvim vim vi less man lazygit ssh mosh tmux claude claudes pi opencode opencodes codex aichat htop btop top journalctl tail watch
return
end
if test $st -eq 0
~/.config/tmux/scripts/agent-state.sh sh done
else
~/.config/tmux/scripts/agent-state.sh sh error
end
end
@@ -0,0 +1,20 @@
# Drop kitty's KITTY_PUBLIC_KEY from the shell environment.
#
# kitty generates a random 40-char base85 public key per instance and exports it
# to every child (kitty/child.py). It is only read on the password path of kitty
# remote control (kitty/remote_control.py: "Password usage requested but
# KITTY_PUBLIC_KEY environment variable is not available"), which needs
# allow_remote_control — not enabled here. `kitten ssh`, icat, clipboard and
# shell integration never touch it.
#
# Why bother: tools that inline the environment into generated code get corrupted
# when a value contains a String.replace replacement sequence. prettier's
# --experimental-cli inlines JSON.stringify(process.env) into its parallel worker
# and splices it with .replace(placeholder, bootloader); a `$` + backtick in the
# key expands to "everything before the match", injecting the worker's own source
# mid-string. Every file then fails with a bogus
# "SyntaxError: Unexpected identifier 'string'". Roughly a 1-in-185 chance per
# kitty launch, which is exactly as fun to debug as it sounds.
if set -q KITTY_PUBLIC_KEY
set -e KITTY_PUBLIC_KEY
end
+1 -1
View File
@@ -106,7 +106,7 @@ if status is-interactive
abbr scrcpys 'scrcpy -wS --power-off-on-close'
abbr tts "tt -notheme -bold -showwpm -json"
abbr tuioss "tuios --show-clock --show-keys --show-cpu --show-ram --confirm-quit"
abbr keybinds "cd ~/.dots/ && nvim gui/.config/hypr/keybindings.conf"
abbr keybinds "cd ~/.dots/ && nvim gui/.config/hypr/dots_binds.lua"
abbr vpnhome "sudo wg-quick up wg0"
abbr vpnkralizec "sudo wg-quick up kralizec-wg0"
abbr vpnsumadija "sudo wg-quick up sumadija-wg0"
+6
View File
@@ -0,0 +1,6 @@
function glow --wraps glow --description 'glow wrapped at the current terminal width; an explicit -w still wins'
# glow never sizes itself to the terminal: glow.yml's width (or its built-in 80) applies, and
# `width: 0` in the config means 80 too. Wide tables get squeezed into that, one word per line.
# $COLUMNS tracks window resizes; a later -w/--width in $argv overrides this one.
command glow --width $COLUMNS $argv
end
+11
View File
@@ -0,0 +1,11 @@
function md2pdf --description 'Markdown to PDF next to the source: cmark-gfm renders it, headless chromium prints it'
# Relative image paths don't resolve (the HTML is rendered from /tmp), and chromium
# leaves out backgrounds when printing, so code blocks lose their grey box.
set -l html (mktemp --suffix=.html)
begin
echo '<meta charset="utf-8"><style>body{font-family:sans-serif;max-width:46em;margin:auto;line-height:1.5}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:.3em .6em}</style>'
cmark-gfm -e table -e strikethrough -e autolink -e tasklist $argv[1]
end >$html
chromium --headless --no-pdf-header-footer --print-to-pdf=(path change-extension pdf $argv[1]) $html 2>/dev/null
rm $html
end
+1 -1
View File
@@ -35,4 +35,4 @@ traceroute
fuck
atool
aria2c
cmark-gfm
+14 -14
View File
@@ -1,31 +1,31 @@
{
"LuaSnip": { "branch": "master", "commit": "0abc8f390b278c3b4aabc4c004ac8a088b65cf24" },
"NvChad": { "branch": "v2.5", "commit": "add44b952d631981614bbb8cfc6f7002f296dfe6" },
"base46": { "branch": "v3.0", "commit": "267954c8663607823f03a3259bb8deb15688212f" },
"NvChad": { "branch": "v2.5", "commit": "87d6a41ec07ab995de912a892749b6b4805ad2b3" },
"base46": { "branch": "v3.0", "commit": "d870ec89a7b2f28c4accce515f97b7bcc5b9d200" },
"cmp-async-path": { "branch": "main", "commit": "98185a91d49ff5dd249aebf2f7456e18063fa2a0" },
"cmp-buffer": { "branch": "main", "commit": "b74fab3656eea9de20a9b8116afa3cfc4ec09657" },
"cmp-nvim-lsp": { "branch": "main", "commit": "cbc7b02bb99fae35cb42f514762b89b5126651ef" },
"cmp-nvim-lua": { "branch": "main", "commit": "e3a22cb071eb9d6508a156306b102c45cd2d573d" },
"cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" },
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
"gitsigns.nvim": { "branch": "main", "commit": "d29ecf6626cf6ffb109fb04a8ecd92c46f4a1ce9" },
"friendly-snippets": { "branch": "main", "commit": "b2ea5c41a150ba72fb656eedb4ed0a86d138d945" },
"gitsigns.nvim": { "branch": "main", "commit": "4bcd99167c265f135886b282baad77afe06206b2" },
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
"menu": { "branch": "main", "commit": "7a0a4a2896b715c066cfbe320bdc048091874cc6" },
"minty": { "branch": "main", "commit": "aafc9e8e0afe6bf57580858a2849578d8d8db9e0" },
"nvim-autopairs": { "branch": "master", "commit": "7b9923abad60b903ece7c52940e1321d39eccc79" },
"nvim-cmp": { "branch": "main", "commit": "2ffe79f1f021def8dd1fcd81deb16f1bb0d989f3" },
"nvim-lspconfig": { "branch": "master", "commit": "4267d26e60323a45e18f8e7a63a964cc8f53ca4e" },
"nvim-tree.lua": { "branch": "master", "commit": "b2aadda94b107480c48e548d6db51c6840b7b33c" },
"nvim-treesitter": { "branch": "main", "commit": "c9f9ed6c1892f629ea399f4ee7905f2686fa13f2" },
"nvim-web-devicons": { "branch": "master", "commit": "2ae6958df7ced50baac5035cec0c15799eedfbf7" },
"nvim-autopairs": { "branch": "master", "commit": "3172f40c16563db871a58f20d205c710ad782311" },
"nvim-cmp": { "branch": "main", "commit": "e04302f1fefcb5653848346eb3723b39b82c549b" },
"nvim-lspconfig": { "branch": "master", "commit": "cc404d425c7752f56b8eaa269107be44d888ea51" },
"nvim-tree.lua": { "branch": "master", "commit": "69eb0c34ed3d836d5a0d92d913b77dd73b054f2f" },
"nvim-treesitter": { "branch": "main", "commit": "610734c5d3e59761d5f79318d120633374537fa5" },
"nvim-web-devicons": { "branch": "master", "commit": "ec309a0eaff6ea967e58c275996155ab5c16938d" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" },
"render-markdown.nvim": { "branch": "main", "commit": "8e8795368391036643b622875a6da6a51bcd33f4" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"telescope.nvim": { "branch": "master", "commit": "427b576c16792edad01a92b89721d923c19ad60f" },
"ui": { "branch": "v3.0", "commit": "fe781d1c12860d6a25d45e588fe4fdd27eb34a1a" },
"volt": { "branch": "main", "commit": "620de1321f275ec9d80028c68d1b88b409c0c8b1" },
"telescope.nvim": { "branch": "master", "commit": "40aedd8a68c78a656a10a8d62d80c54af59420fb" },
"ui": { "branch": "v3.0", "commit": "8cbf0dc1883b20a1df114149c187d250f0af0853" },
"volt": { "branch": "main", "commit": "4351c5cd790a0d39491c4d5c75dd62d7db3d3414" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
}
+1
View File
@@ -15,6 +15,7 @@ service caches config).
| `agents/*.md` | Four subagents with real prompts and scoped permissions: `code-reviewer`, `test-runner` (hidden), `doc-writer` (edits docs only), `security-auditor` (hidden). |
| `commands/*.md` | `/commit` (Conventional Commits message, never commits) and `/review` (read-only diff review), both on the `plan` agent. |
| `plugins/verbose/` | 2.x TUI plugin: `<leader>v`, `/verbose`, or the palette entry toggles the *verbose view* — thinking blocks expanded and tool calls ungrouped — reading the live state from the built-in toggles so a mixed state converges. `server.ts` is a no-op half required by the loader. |
| `plugins/agent-state.ts` | Server-side plugin: mirrors the session state (`message.updated` user → busy, `permission.asked` → input, `session.idle` → done, `session.error` → error) into the tmux pane opencode runs in via `~/.config/tmux/scripts/agent-state.sh`, for the status-bar agent badges. Skips child sessions; only right when the TUI in the pane spawned the server (not `serve`/`attach`); under `opencodes` (bwrap) the tmux socket dir must be bound in. |
| `themes/catppuccin-mocha.json` | Catppuccin Mocha (dark) / Latte (light) theme following the pi theme's semantic mapping. Built-in `catppuccin` exists too; this one matches pi. |
| `tui.json` | TUI settings in the v1 schema (2.x compat): theme, leader timeout, stacked diffs, block cursor, attention notifications, a few extra keybinds. |
+2 -2
View File
@@ -151,7 +151,7 @@
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s, remote)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
"GLM-4.7-Flash-UD-Q3_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash Q3 · 24k — fast coder (~28 t/s, remote)", "limit": { "context": 24576, "output": 8192 } },
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 24k · vision — agentic coder, thinking (~32 t/s, remote)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 32k · vision — agentic coder, thinking (~32 t/s, remote)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s, remote)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s, remote)", "limit": { "context": 65536, "output": 8192 } }
}
@@ -174,7 +174,7 @@
"gemma-4-E4B-it-UD-Q8_K_XL": { "name": "Gemma 4 E4B · 64k · vision — fast generalist, long docs (~57 t/s)", "attachment": true, "limit": { "context": 65536, "output": 16384 } },
"GLM-4.7-Flash-UD-Q4_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash · 24k — quality coder (~21 t/s)", "limit": { "context": 24576, "output": 8192 } },
"GLM-4.7-Flash-UD-Q3_K_XL": { "reasoning": true, "interleaved": "reasoning_content", "name": "GLM-4.7 Flash Q3 · 24k — fast coder (~28 t/s)", "limit": { "context": 24576, "output": 8192 } },
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 24k · vision — agentic coder, thinking (~32 t/s)", "attachment": true, "limit": { "context": 24576, "output": 8192 } },
"Ornith-1.5-35B-A3B-IQ3_XXS": { "reasoning": true, "name": "Ornith 1.5 35B · 32k · vision — agentic coder, thinking (~32 t/s)", "attachment": true, "limit": { "context": 32768, "output": 8192 } },
"gpt-oss-20b": { "reasoning": true, "name": "gpt-oss 20B · 64k — fast reasoning + tools (~38 t/s)", "limit": { "context": 65536, "output": 8192 } },
"gpt-oss-20b-low": { "reasoning": true, "name": "gpt-oss 20B low · 64k — snappy answers (~37 t/s)", "limit": { "context": 65536, "output": 8192 } }
}
@@ -0,0 +1,53 @@
// opencode agent-state plugin: mirrors the session state into the tmux pane opencode runs in via
// ~/.config/tmux/scripts/agent-state.sh (oc busy|done|input|error). No-op outside tmux.
//
// Runs in the SERVER process, so TMUX_PANE is only right when the TUI in the pane spawned the
// server (plain `opencode`), not for `opencode serve` / `opencode attach`. Under the bwrap
// `opencodes` wrapper the tmux socket dir must be bound in, or the script gives up quietly.
// Child (subagent) sessions are skipped so a subagent finishing does not flag "done".
import type { Plugin } from "@opencode-ai/plugin"
export const AgentState: Plugin = async ({ client, $ }) => {
const script = `${process.env.HOME}/.config/tmux/scripts/agent-state.sh`
const set = async (state: string) => {
if (!process.env.TMUX_PANE) return
try {
await $`${script} oc ${state}`.quiet().nothrow()
} catch {}
}
const childCache = new Map<string, boolean>()
const isChild = async (sessionID?: string) => {
if (!sessionID) return false
if (!childCache.has(sessionID)) {
try {
const r = await client.session.get({ path: { id: sessionID } })
childCache.set(sessionID, Boolean(r.data?.parentID))
} catch {
childCache.set(sessionID, false)
}
}
return childCache.get(sessionID) ?? false
}
return {
event: async ({ event }) => {
const p = (event as { properties?: Record<string, any> }).properties ?? {}
switch (event.type) {
case "message.updated": // a new user message = a prompt went in
if (p.info?.role === "user" && !(await isChild(p.info?.sessionID))) await set("busy")
break
case "permission.asked":
if (!(await isChild(p.sessionID))) await set("input")
break
case "permission.replied":
if (!(await isChild(p.sessionID))) await set("busy")
break
case "session.idle":
if (!(await isChild(p.sessionID))) await set("done")
break
case "session.error":
if (!(await isChild(p.sessionID))) await set("error")
break
}
},
}
}
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# agent-next.sh: switch to the next window (after the current one, wrapping) that holds an
# agent mark you have not looked at yet; with none left, fall back to any grey mark; with
# nothing marked, say so. Bound to prefix+Tab and to the » tap target in the status bar.
cur=$(tmux display -p '#{window_index}')
pick() { # $1 empty = unseen marks only, "any" = grey ones too
tmux list-panes -a -F '#{window_index} #{@agent} #{@agent_seen}' |
awk -v c="$cur" -v any="$1" '
$2 != "" && $2 != "busy" && (any != "" || $3 == "") { if ($1 > c && !n) n = $1; if (!f) f = $1 }
END { if (n) print n; else if (f) print f }'
}
w=$(pick); [ -n "$w" ] || w=$(pick any)
if [ -n "$w" ]; then tmux select-window -t ":$w"; else tmux display-message "no agent waiting"; fi
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
# agent-state.sh <cc|oc|pi|cx> <busy|done|input|error|off>
#
# Per-pane "what is the agent in this pane doing" for tmux, shared by every agent's hooks:
# Claude Code (claude/settings.json), codex (~/.codex/hooks.json), pi (agent-state.ts
# extension) and opencode (plugins/agent-state.ts). No-op outside tmux.
#
# Which pane: $TMUX_PANE when the agent runs in the pane itself (pi, opencode, codex). Claude
# Code sessions run in a worker the claude daemon pre-spawned ("claude bg-spare"), whose
# environment has no tmux variables, so for them the worker is found by walking up the parent
# chain to the pid registered in ~/.claude/sessions/<pid>.json, and the pane is the one whose
# title Claude set to that session's name ("✳ <name>").
#
# Pane options read by tmux.conf's @agent_badge: @agent (the state, drawn as ✓ ? ! after the
# window name; busy draws nothing), @agent_seen (set by tmux's pane-focus-in hook: the glyph
# loses its colour once you visit the pane and stays, grey, until the next prompt turns the
# state busy) and @agent_tool (only the termux notification title uses it). `off` unsets all.
#
# Phone-level notice only for a NEW mark on a pane that is not in view: termux-notification
# when the agent runs natively in termux, otherwise a BEL into the pane (tmux forwards the bell
# to the outer terminal; termux's default bell-character=vibrate buzzes, and the window gets
# tmux's bell flag). A state change on an already-marked pane (e.g. Claude's idle_prompt ~60s
# after Stop turning done into input) only swaps the glyph: no second buzz, seen stays as is.
command -v tmux >/dev/null 2>&1 || exit 0
tool=$1 state=$2 pane=$TMUX_PANE
if [ -z "$pane" ]; then
p=$$ worker=""
for _ in 1 2 3 4 5 6 7 8; do
p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d ' ')
[ -n "$p" ] && [ "$p" != 1 ] || break
[ -f "$HOME/.claude/sessions/$p.json" ] && { worker=$p; break; }
done
[ -n "$worker" ] || exit 0
name=$(python3 -I -c 'import json,sys; print(json.load(open(sys.argv[1])).get("name",""))' \
"$HOME/.claude/sessions/$worker.json" 2>/dev/null)
[ -n "$name" ] || exit 0
pane=$(tmux list-panes -a -F '#{pane_id} #{pane_title}' 2>/dev/null |
awk -F'\t' -v n="$name" '$2==n || substr($2,length($2)-length(n))==" " n {print $1; exit}')
[ -n "$pane" ] || exit 0
fi
if [ "$state" = off ]; then
for o in @agent @agent_seen @agent_tool; do tmux set -p -t "$pane" -u "$o" 2>/dev/null; done
exit 0
fi
tmux set -p -t "$pane" @agent_tool "$tool" 2>/dev/null || exit 0 # no server reachable (sandbox): give up quietly
case $state in
busy)
tmux set -p -t "$pane" @agent busy
tmux set -p -t "$pane" -u @agent_seen 2>/dev/null
command -v termux-notification-remove >/dev/null 2>&1 && termux-notification-remove "agent-$pane"
;;
*)
already=$(tmux show -pv -t "$pane" @agent 2>/dev/null)
tmux set -p -t "$pane" @agent "$state"
if [ "$(tmux display -p -t "$pane" '#{pane_active}#{window_active}')" = 11 ]; then
tmux set -p -t "$pane" @agent_seen 1 # in view: you saw it happen, so it starts out seen
exit 0
fi
[ -n "$already" ] && [ "$already" != busy ] && exit 0 # glyph swapped on an existing mark
tmux set -p -t "$pane" -u @agent_seen 2>/dev/null
if command -v termux-notification >/dev/null 2>&1; then
termux-notification --id "agent-$pane" --title "$tool: $state" --content "${PWD##*/}" \
--vibrate 200 --action "tmux select-pane -t $pane"
else
printf '\a' > "$(tmux display -p -t "$pane" '#{pane_tty}')" 2>/dev/null
fi
;;
esac
exit 0
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# CPU utilisation (%) averaged over the interval since the PREVIOUS status refresh:
# /proc/stat totals persist in a state file between runs (same pattern as net.sh),
# so the value covers the whole status-interval. (The old version slept 0.3s and
# sampled just that window — a 6% duty cycle that aliased bursty loads.) No sleep,
# ~5ms. First run (no state yet) shows the since-boot average. Glyph = Waybar's
# cpu module icon (U+F035B).
printf '\U000f035b '
rt="${XDG_RUNTIME_DIR:-/tmp}"
st="$rt/tmux_cpu_stat" # previous totals: tot idle
read -r _ u n s i w rest </proc/stat
idle=$((i + w))
tot=$((u + n + s + i + w))
for v in $rest; do tot=$((tot + v)); done
p_tot=0
p_idle=0
[ -r "$st" ] && read -r p_tot p_idle <"$st"
[ "$p_tot" -gt "$tot" ] && {
p_tot=0
p_idle=0
} # stale state from a previous boot
printf '%s %s\n' "$tot" "$idle" >"$st"
dt=$((tot - p_tot))
di=$((idle - p_idle))
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
# RAM utilisation (%) — used = MemTotal - MemAvailable, from /proc/meminfo.
# Called from tmux status-right via #(); cached per status-interval.
printf '\U000f0f86 ' # Waybar's memory glyph (U+F0F86), then the percentage
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# metrics.sh full|compact — the status-right system segment in ONE process per refresh.
# (tmux used to spawn net dl, net ul, cpu, mem and temp as five separate #() jobs per tick.)
# Prints #[fg=…] style markers, which tmux honours inside #() output.
# full : net dl · net ul · cpu · mem · temp (temp vanishes when no sensor exists)
# compact : cpu · mem
# CPU and net are averaged over the interval since the PREVIOUS refresh: the /proc totals
# persist in $XDG_RUNTIME_DIR state files, so no sleeps and the value covers the whole
# status-interval. First run (no state yet) shows the since-boot average. Glyphs are Waybar's:
# cpu U+F035B, memory U+F0F86, thermometer U+F050F, download U+F019, upload U+F093.
mode=${1:-full}
rt="${XDG_RUNTIME_DIR:-/tmp}"
cpu() { # utilisation % since the previous refresh
local st="$rt/tmux_cpu_stat" u n s i w rest idle tot v p_tot=0 p_idle=0 dt di
read -r _ u n s i w rest </proc/stat
idle=$((i + w))
tot=$((u + n + s + i + w))
for v in $rest; do tot=$((tot + v)); done
[ -r "$st" ] && read -r p_tot p_idle <"$st"
[ "$p_tot" -gt "$tot" ] && { p_tot=0; p_idle=0; } # stale state from a previous boot
printf '%s %s\n' "$tot" "$idle" >"$st"
dt=$((tot - p_tot))
di=$((idle - p_idle))
printf '\U000f035b '
if [ "$dt" -le 0 ]; then printf -- '--%%'; else printf '%d%%' $(((100 * (dt - di) + dt / 2) / dt)); fi
}
mem() { # used = MemTotal - MemAvailable
printf '\U000f0f86 '
awk '/^MemTotal:/{t=$2} /^MemAvailable:/{a=$2} END{if(t>0)printf "%d%%",(t-a)*100/t; else printf "--%%"}' /proc/meminfo
}
temp() { # CPU °C: a CPU-ish thermal zone, else a CPU hwmon chip, else the hottest zone; nothing if none
local cpu_zone="" any_zone="" hw="" d t ty f
for d in /sys/class/thermal/thermal_zone*/; do
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
t=$(<"${d}temp")
case $t in '' | *[!0-9]*) continue ;; esac
ty=$(<"${d}type"); ty=${ty,,}
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi ;;
esac
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
done
if [ -z "$cpu_zone" ]; then
for d in /sys/class/hwmon/hwmon*/; do
[ -r "${d}name" ] || continue
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
for f in "${d}"temp*_input; do
[ -r "$f" ] || continue
t=$(<"$f")
case $t in '' | *[!0-9]*) continue ;; esac
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
done ;;
esac
done
fi
t=${cpu_zone:-${hw:-$any_zone}}
[ -n "$t" ] || return 0
printf '\U000f050f %d° ' $(((t + 500) / 1000))
}
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
while (b >= 1024 && i < 5) { b /= 1024; i++ }
if (i == 1) printf "%d%s", b, u[i]; else printf "%.1f%s", b, u[i] }'; }
net() { # sets dl/ul: bytes per second over all non-lo interfaces since the previous refresh
local ctr="$rt/tmux_net_ctr" now rx tx prx ptx pnow drx=0 dtx=0 dt=1
now=$(date +%s.%N)
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
if [ -r "$ctr" ]; then
read -r prx ptx pnow <"$ctr"
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
drx=$((rx - prx)); dtx=$((tx - ptx))
[ "$drx" -lt 0 ] && drx=0
[ "$dtx" -lt 0 ] && dtx=0
fi
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
}
case $mode in
compact)
printf '#[fg=#89b4fa]%s #[fg=#94e2d5]%s' "$(cpu)" "$(mem)"
;;
*)
net
printf '#[fg=#a6e3a1]\U0000f019 %s #[fg=#f9e2af]\U0000f093 %s #[fg=#89b4fa]%s #[fg=#94e2d5]%s #[fg=#fab387]%s' \
"$dl" "$ul" "$(cpu)" "$(mem)" "$(temp)"
;;
esac
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env bash
# Network throughput from /proc/net/dev (sum of all non-lo interfaces), averaged
# over the interval between status refreshes — no blocking sleep.
#
# Usage: net.sh dl -> " 1.2M" (download icon U+F019 + rate)
# net.sh ul -> " 45K" (upload icon U+F093 + rate)
#
# tmux calls both from status-right. A short (<3s) cache lets the two calls of a
# single refresh share one computation, so the up/down figures are consistent and
# only one of them re-samples the counters per refresh.
which=${1:-dl}
rt="${XDG_RUNTIME_DIR:-/tmp}"
ctr="$rt/tmux_net_ctr" # previous counters: rx tx epoch.ns
cache="$rt/tmux_net_cache" # computed rates: dl_rate ul_rate epoch_s
emit() { # emit <icon-printf-escape> <rate>
printf "$1 %s" "$2"
}
# Reuse a fresh cache so dl and ul agree and we only sample once per refresh.
fresh_emit() { # print from the cache and exit — if the cache is fresh enough
[ -r "$cache" ] || return 1
read -r cdl cul cts <"$cache"
{ [ -n "$cts" ] && [ "$(($(date +%s) - cts))" -lt 3 ]; } || return 1
if [ "$which" = ul ]; then emit '\U0000f093' "$cul"; else emit '\U0000f019' "$cdl"; fi
exit 0
}
fresh_emit
# tmux launches the dl and ul jobs CONCURRENTLY each refresh, so without a lock
# both miss the stale cache and compute independently — worst case the loser reads
# the counters milliseconds after the winner rewrote them and shows a ~0 rate.
# Serialize: the loser waits ~30ms on the lock, then hits the winner's fresh cache.
exec 9>"$rt/tmux_net_lock"
flock 9
fresh_emit
now=$(date +%s.%N)
read -r rx tx < <(awk 'NR>2{gsub(/:/," "); if($1!="lo"){r+=$2; t+=$10}} END{print r+0, t+0}' /proc/net/dev)
drx=0
dtx=0
dt=1
if [ -r "$ctr" ]; then
read -r prx ptx pnow <"$ctr"
dt=$(awk -v a="$now" -v b="$pnow" 'BEGIN{d=a-b; print (d>0.05)?d:1}')
drx=$((rx - prx))
dtx=$((tx - ptx))
[ "$drx" -lt 0 ] && drx=0
[ "$dtx" -lt 0 ] && dtx=0
fi
printf '%s %s %s\n' "$rx" "$tx" "$now" >"$ctr"
human() { awk -v b="$1" 'BEGIN{ split("B K M G T", u, " "); i=1;
while (b >= 1024 && i < 5) { b /= 1024; i++ }
if (i == 1) printf "%d%s", b, u[i]; else printf "%.1f%s", b, u[i] }'; }
dl=$(human "$(awk -v x="$drx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
ul=$(human "$(awk -v x="$dtx" -v d="$dt" 'BEGIN{printf "%d", x/d}')")
printf '%s %s %s\n' "$dl" "$ul" "$(date +%s)" >"$cache"
if [ "$which" = ul ]; then emit '\U0000f093' "$ul"; else emit '\U0000f019' "$dl"; fi
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# CPU temperature (°C) for the tmux status bar (thermometer glyph U+F050F).
# Sensor hunt, most→least specific: a CPU-ish thermal zone (x86_pkg_temp/…),
# else a CPU hwmon chip (coretemp/k10temp/…), else the hottest remaining zone
# (acpitz can be a bogus constant on desktops, hence last). Prints NOTHING when
# no sensor exists (VMs/containers) so the segment simply disappears — which is
# also why the trailing spacing lives in here, not in status-right.
cpu_zone=""
any_zone=""
hw=""
for d in /sys/class/thermal/thermal_zone*/; do
[ -r "${d}temp" ] && [ -r "${d}type" ] || continue
t=$(<"${d}temp")
case $t in '' | *[!0-9]*) continue ;; esac
ty=$(<"${d}type")
ty=${ty,,}
case $ty in *x86_pkg_temp* | *coretemp* | *k10temp* | *tctl* | *cpu*)
if [ -z "$cpu_zone" ] || [ "$t" -gt "$cpu_zone" ]; then cpu_zone=$t; fi
;;
esac
if [ -z "$any_zone" ] || [ "$t" -gt "$any_zone" ]; then any_zone=$t; fi
done
if [ -z "$cpu_zone" ]; then
for d in /sys/class/hwmon/hwmon*/; do
[ -r "${d}name" ] || continue
case $(<"${d}name") in coretemp | k10temp | zenpower | cpu_thermal)
for f in "${d}"temp*_input; do
[ -r "$f" ] || continue
t=$(<"$f")
case $t in '' | *[!0-9]*) continue ;; esac
if [ -z "$hw" ] || [ "$t" -gt "$hw" ]; then hw=$t; fi
done
;;
esac
done
fi
t=${cpu_zone:-${hw:-$any_zone}}
[ -n "$t" ] || exit 0
printf '\U000f050f %d° ' $(((t + 500) / 1000))
+60 -15
View File
@@ -2,13 +2,21 @@
# Without this, tmux collapses them to a plain Enter. Requires tmux 3.5+ for csi-u.
set -g extended-keys on
set -g extended-keys-format csi-u
# termux reports xterm-256color: without RGB every #rrggbb below is approximated to the 256
# palette on the phone, and without extkeys tmux never asks the terminal for Shift/Ctrl+Enter.
# (kitty is xterm-kitty, whose terminfo already carries both.)
set -as terminal-features 'xterm-256color:RGB:extkeys'
# Let apps wrap escape sequences in DCS tmux passthrough (Claude Code / pi notifications,
# Claude's progress bar) so they reach the outer terminal instead of being swallowed.
set -g allow-passthrough on
# Mouse/touch reporting. Chiefly so the window list in the status bar is
# tappable — tmux binds MouseDown1Status to `switch-client -t =` out of the box,
# and status-format[1] below keeps the `range=window|…` markers that make each
# entry a hit target, so a tap on a tab switches to it with no extra binding.
# Wheel over the bar cycles windows; right-click a tab or the session name for
# tmux's built-in menus (no touch equivalent — a phone can't right-click).
# tmux's built-in menus; tmux-menus (plugin below) puts the same menus on prefix+\,
# which a phone can tap through.
#
# The cost: drag-select now goes to tmux instead of kitty, so hold Shift when you
# want kitty's own selection. Scroll wheel enters copy-mode on the pane rather
@@ -21,6 +29,18 @@ set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
# One long agent turn scrolls past tmux's default 2000 lines; resurrect captures pane
# contents from this buffer too. Applies to panes created after the change.
set -g history-limit 50000
# Copy-mode: vi keys, v/y like an editor. set-clipboard on pushes every copy — tmux's own and
# those apps inside make (nvim, pi) — over OSC 52 to the CLIENT terminal, so a yank on the
# phone lands on the phone and on kitty lands on the desktop, wherever the server runs.
setw -g mode-keys vi
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi y send -X copy-selection-and-cancel
set -s set-clipboard on
# --- Session persistence (survives reboot) ---------------------------------
# resurrect: save/restore sessions, windows, panes, layout and working dirs
# (prefix + Ctrl-s to save, prefix + Ctrl-r to restore).
@@ -29,6 +49,8 @@ set -g renumber-windows on
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
# menus: tmux's context menus on prefix+\ (tappable; no right-click needed on a phone).
set -g @plugin 'jaclu/tmux-menus'
# Also capture pane scrollback and restore running programs (nvim, …).
# nvim 'session' strategy restores via Session.vim when present, else plain nvim.
@@ -87,20 +109,23 @@ set -g status-left "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@left_ful
# Middle: flat window list — inactive muted, active mauve/bold, 2-space gaps.
set -g window-status-separator " "
setw -g window-status-format "#[fg=#7f849c]#I #[fg=#6c7086]#W"
setw -g window-status-format "#[fg=#7f849c]#I#{P:#{E:@agent_badge}} #[fg=#6c7086]#W#[default]"
setw -g window-status-current-format "#[fg=#cba6f7,bold]#I #[fg=#cdd6f4,bold]#W"
setw -g window-status-activity-style "fg=#f9e2af"
# The agent-state bell flags the window (monitor-bell) so the outer terminal buzzes; tmux's
# default bell style is `reverse`, which painted the window entry — only the glyph gets colour.
setw -g window-status-bell-style default
# Right: net dl/ul · cpu · mem │ date │ clock. continuum prepends its invisible
# save hook to status-right (this block sits above the tpm `run` line, so the
# hook survives every reload). Metrics are scripts/*.sh, symlinked into
# ~/.config/tmux/scripts/. The hook lands ahead of the #{?} below, i.e. outside
# it, so it keeps running for every client regardless of which variant is drawn.
set -g @right_full "#[fg=#a6e3a1]#(~/.config/tmux/scripts/net.sh dl) #[fg=#f9e2af]#(~/.config/tmux/scripts/net.sh ul) #[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) #[fg=#fab387]#(~/.config/tmux/scripts/temp.sh)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
# Right: net dl/ul · cpu · mem · temp │ date │ clock. One #() job per refresh: metrics.sh
# prints the whole coloured segment (it was five separate scripts per tick). continuum
# prepends its invisible save hook to status-right (this block sits above the tpm `run`
# line, so the hook survives every reload). The hook lands ahead of the #{?} below, i.e.
# outside it, so it keeps running for every client regardless of which variant is drawn.
set -g @right_full "#(~/.config/tmux/scripts/metrics.sh full)#[fg=#45475a]│ #[fg=#a6adc8]%a %b %-d #[fg=#45475a]│ #[fg=#cba6f7,bold]%H:%M "
# Compact: cpu + mem only. Net rates and the temp glyph go first (widest, least
# useful on a phone), then the date, then the clock — a phone shows its own.
set -g @right_compact "#[fg=#89b4fa]#(~/.config/tmux/scripts/cpu.sh) #[fg=#94e2d5]#(~/.config/tmux/scripts/mem.sh) "
set -g @right_compact "#(~/.config/tmux/scripts/metrics.sh compact) "
set -g status-right "#{?#{e|>=:#{client_width},#{E:@status_cutoff}},#{E:@right_full},#{E:@right_compact}}"
@@ -125,28 +150,48 @@ bind S {
# Two rows: [0] drawn top-border rule · [1] the bar content. The rule doubles as
# the separator from the pane content above; tmux status height is whole rows, so
# one row above the content is the smallest gap that still shows a border.
# Full-width run of ─ (yes|head|paste avoids %, backslashes and nested parens so
# tmux's strftime/quote/#() parsing leave it alone); tmux truncates it to width.
# The rule is a literal run of ─ in @rule (300 columns; tmux clips it to the client
# width), so drawing it costs no process.
# status-format[1] is tmux 3.7's default content template (status-left/windows/right).
set -g status 2
set -g status-format[0] "#[fg=#6c7086,bg=default]#(yes ─ | head -n 400 | paste -sd '')"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
set -g @rule "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
set -g status-format[0] "#[fg=#6c7086,bg=default]#{@rule}"
set -g status-format[1] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?loop_last_flag,,#{E:window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?loop_last_flag,,#{E:window-status-separator}}}#[range=user|newwin] #[fg=#6c7086]+#[default] #[norange]#[range=user|agentnext] #[fg=#6c7086]»#[default] #[norange]#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]"
# The `+` appended to the window list above sits in a `range=user|newwin`
# marker. Per tmux(1), a user range triggers the same `Status` mouse key as a
# window range and exposes its argument in #{mouse_status_range} — window ranges
# report the literal string "window" — so one binding can serve both: click a
# tab to select it (tmux's stock behaviour, kept in the else branch), click the
# `+` to open a new window. Padded with spaces on both sides so it's a wide
# enough target to hit with a thumb.
# `+` to open a new window, click the `»` to jump to the next window with an agent
# mark (scripts/agent-next.sh, also prefix+Tab). Padded with spaces on both sides so
# each is a wide enough target to hit with a thumb.
bind -n MouseDown1Status {
if -F '#{==:#{mouse_status_range},newwin}' {
new-window
} {
if -F '#{==:#{mouse_status_range},agentnext}' {
run-shell '~/.config/tmux/scripts/agent-next.sh'
} {
switch-client -t =
}
}
}
# Agent state per pane — set by ~/.config/tmux/scripts/agent-state.sh <tool> <state> from the
# Claude Code / codex / pi / opencode hooks: @agent = busy|done|input|error. @agent_badge draws
# one glyph glued to the window number ("2✓ name") in the window list, the same for every tool: ✓ done (green),
# ? waiting for input (yellow), ! error (red); busy draws nothing. Visiting the pane only takes
# the colour away (pane-focus-in sets @agent_seen; needs focus-events): the glyph stays, grey,
# until the next prompt turns the state busy. prefix K clears every mark; prefix Tab (or the »
# in the bar) jumps to the next window with a mark, coloured ones first. A coloured glyph
# always means "this happened while you were not looking".
set -g @agent_badge "#{?#{==:#{@agent},done},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#a6e3a1]}✓,#{?#{==:#{@agent},input},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f9e2af]}?,#{?#{==:#{@agent},error},#{?#{@agent_seen},#[fg=#6c7086],#[fg=#f38ba8]}!,}}}"
set -g focus-events on
set-hook -g pane-focus-in 'set -p @agent_seen 1'
bind Tab run-shell '~/.config/tmux/scripts/agent-next.sh'
bind K run-shell 'for p in $(tmux list-panes -a -F "##{pane_id}"); do tmux set -p -t $p -u @agent; tmux set -p -t $p -u @agent_seen; done' \; display-message "agent marks cleared"
# Round out the theme so no default green leaks into borders/messages/modes.
set -g pane-border-style "fg=#313244"
set -g pane-active-border-style "fg=#cba6f7"
+7 -4
View File
@@ -59,10 +59,11 @@ have a slash command to disable them for the current session without deleting.
| `vim-toggle.ts` | `/vim` — drops the modal editor for the rest of the session, and brings it back via `ctx.reload()`. Pairs with the **`pi-vim` package** (below), which registers no command of its own | **custom** | delete file → you lose the toggle, not the editor; remove the package to drop modal editing itself |
| `plan-mode/` | Read-only "plan" mode (disables edit/write, restricts bash) bound to **Shift+Tab**; `/plan` toggles, `--plan` starts in it, `/plan-done` clears a stuck progress checklist | **vendored** copy of pi's bundled `examples/extensions/plan-mode/`, rebound from upstream `Ctrl+Alt+P` | delete the folder **and** revert the `keybindings.json` `app.thinking.cycle` remap (see below) |
| `notify.ts` | Native terminal notification (OSC 777/99, Windows toast) when the agent goes idle | **verbatim** from pi's `examples/extensions/notify.ts` | delete file |
| `agent-state.ts` | Mirrors Pi's state (`agent_start` busy, `agent_settled` done, `ui_prompt_*` input, `session_shutdown` off) into the tmux pane it runs in via `~/.config/tmux/scripts/agent-state.sh`, for the status-bar agent badges; no-op outside tmux | own | delete file |
| `questionnaire.ts` | Interactive multi-question overlay the model can call as a tool | **near-verbatim** from pi's `examples/extensions/questionnaire.ts` (one-line fix) | delete file |
| `permission-gate.ts` | Prompts before dangerous `bash` commands (blocks outright with no UI). Local list covers git history rewrites, package managers, `systemctl` and irreversible disk ops on top of upstream's `rm -rf`/`sudo`/`chmod 777` | **adapted** from pi's `examples/extensions/permission-gate.ts` | delete file |
| `protected-paths.ts` | Blocks `write`/`edit` to secrets and repo plumbing (`secrets.fish`, `auth.json`, `.env`, `.ssh/`, `.git/`, …) | **adapted** from pi's `examples/extensions/protected-paths.ts` | delete file |
| `compaction-watch.ts` | Surfaces every compaction and its `reason`; loudly flags `overflow` (recovery rather than prevention), which means `reserveTokens` is below the model's `maxTokens` | **custom** | delete file |
| `compaction-watch.ts` | Surfaces every compaction and its `reason`; on `overflow` (recovery rather than prevention) it reads the turn that caused it and says which kind it was, with numbers — a reply pi itself had clamped (`max_tokens` = window − estimate − 4096, the usual one on thinking models) or a prompt the server really rejected | **custom** | delete file |
| `verbose.ts` | `/verbose [on|off|status]` and **Alt+V** toggle the *persisted* verbose default (`hideThinkingBlock`, `showCacheMissNotices`) and report pi's live keys — `ctrl+o` tool output, `ctrl+t` thinking, which an extension cannot drive itself | **custom** | delete file; the live keys are built in and keep working |
| `confirm-destructive.ts` | Confirms before session clear / switch / fork | **verbatim** from pi's `examples/extensions/confirm-destructive.ts` | delete file |
| `session-name.ts` | `/session-name [name]` — friendly names in the session picker instead of the first message | **verbatim** from pi's `examples/extensions/session-name.ts` | delete file |
@@ -117,7 +118,9 @@ pi install npm:pi-vim
> caret range would quietly drift off `pi --version`.
Configured under `settings.json > piVim`: `modeColors` (theme tokens, not raw ANSI),
`borderSync`/`labelSync` pinned so only the label is tinted, and
`borderSync` so the input border itself takes the mode color in NORMAL/VISUAL
(mauve/yellow) and is left to pi in INSERT/EX, `labelSync` so the label always carries
its mode color, and
`clipboardMirror: "yank"` — upstream's default `"all"` mirrors deletes too, so every
`dd`/`x` would clobber the OS clipboard. `exCommand.copyInputToClipboard` is left
**off**: it copies the composed prompt out, so upstream only honours it from the
@@ -171,14 +174,14 @@ Beyond pointing at local models, these values deviate from pi's defaults:
| Key | Value here | Stock default | Why |
|---|---|---|---|
| `compaction.reserveTokens` | `8192` | 16384 | small local windows (24k–128k), but **≥ every model's `maxTokens`** — otherwise a turn can overflow the window before compaction even fires |
| `compaction.reserveTokens` | `8192` | 16384 | small local windows (24k–128k), but **≥ every model's `maxTokens` + 4096**: pi clamps each request's `max_tokens` to `contextWindow − estimate − 4096`, and with 24k/4096 that clamp starts exactly at this threshold (see CLAUDE.md for the `overflow` recoveries it can still produce) |
| `compaction.keepRecentTokens` | `4000` | 20000 | keep less verbatim, to pay for the larger reserve and leave room after a compaction for the next one |
| `npmCommand` | `fnm exec --using=22 -- npm` | `npm` | pin extension npm installs to Node 22 via fnm |
| `retry` / `httpIdleTimeoutMs` | long (1h provider timeout, 10m idle) | shorter | local models can be slow to first token |
| `defaultThinkingLevel` | `medium` | — | the default model is a thinking preset (`Qwen3.6-35B-A3B-Thinking`); Alt+T cycles it per session |
| `thinkingBudgets` | `512 / 1024 / 2048 / 3072` (minimal / low / medium / high) | 1024 / 2048 / 8192 / 16384 | per-request cap on the `<think>` block, sent only for models whose `compat.thinkingTokenBudgetField` is set (`Ornith`, as `thinking_budget_tokens`). pi clamps the budget to `maxTokens − 1024`, so on a 4096-token reply the stock values collapse to 3072 and leave the answer 1024 tokens — the recipe for "Response was truncated before completion." |
| `packages` | `["npm:pi-vim"]` | `[]` | the modal editor — see the pi-vim section above |
| `piVim` | mode colors, `clipboardMirror: "yank"`, `:` → pi bridge | — | package config, not a pi setting; ignored if the package is removed |
| `piVim` | mode colors, border painted per mode (NORMAL/VISUAL), `clipboardMirror: "yank"`, `:` → pi bridge | — | package config, not a pi setting; ignored if the package is removed |
| `collapseChangelog` | `true` | `false` | condensed changelog after an upgrade instead of the full screen |
**Remove:** delete each key to fall back to pi's default (or drop the whole
@@ -0,0 +1,26 @@
/**
* Pi agent-state extension: mirrors Pi's state into the tmux pane it runs in via
* ~/.config/tmux/scripts/agent-state.sh (pi busy|done|input|off), so the tmux status bar
* shows which pane is done or waiting for you. The script is a no-op outside tmux.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { execFile } from "node:child_process";
import { homedir } from "node:os";
import { join } from "node:path";
const SCRIPT = join(homedir(), ".config", "tmux", "scripts", "agent-state.sh");
function state(s: "busy" | "done" | "input" | "error" | "off"): void {
if (!process.env.TMUX_PANE) return;
execFile(SCRIPT, ["pi", s], () => {});
}
export default function (pi: ExtensionAPI) {
pi.on("agent_start", async () => state("busy"));
// agent_settled, not agent_end: Pi may still retry, compact, or run queued follow-ups.
pi.on("agent_settled", async () => state("done"));
// Blocking confirm/select prompts from extensions (permission-gate, confirm-destructive, questionnaire).
pi.on("ui_prompt_start", async () => state("input"));
pi.on("ui_prompt_end", async () => state("busy"));
pi.on("session_shutdown", async () => state("off"));
}
+78 -18
View File
@@ -3,31 +3,94 @@
*
* Compaction is otherwise silent, which is why "compacting fails a lot" took code
* reading and arithmetic to diagnose rather than a glance at a log. This makes it
* visible, and specifically names the failure mode that was breaking it here before
* 2026-08-16.
* visible and, for the one reason that signals trouble, says WHICH trouble.
*
* pi reports a `reason` on every compaction:
* "manual" - you ran /compact
* "threshold" - the healthy path: contextTokens passed contextWindow - reserveTokens
* "overflow" - the window was ALREADY full; this is recovery, not prevention
* "overflow" - recovery, not prevention: the previous assistant turn ended badly,
* pi compacts and (when `willRetry`) re-runs that turn once
*
* An "overflow" compaction means the turn blew past contextWindow before the threshold
* could fire, which happens whenever `compaction.reserveTokens` is smaller than the
* model's `maxTokens`. pi then gets one recovery attempt, summarizing from an
* already-overflowed context — and when that does not fit either, the session dies with
* "Context overflow recovery failed after one compact-and-retry attempt".
* Since pi 0.84.3 "overflow" covers TWO different situations, and the numbers on the
* last assistant message tell them apart (CLAUDE.md > Compaction tuning has the
* arithmetic for both):
*
* So: seeing "threshold" is fine. Seeing "overflow" means the settings are wrong for the
* model in use, and this says so at the moment it happens instead of leaving you to
* infer it later. See CLAUDE.md > Compaction tuning.
* 1. A real overflow — the server rejected the prompt (stopReason "error" matching
* pi's overflow patterns), or reported more input than the declared window.
* Fix: contextWindow / reserveTokens / maxTokens are wrong for the model in use.
* 2. Truncated-reply recovery — the reply stopped with stopReason "length" while
* usage.output is BELOW the model's maxTokens. The request itself carried a smaller
* max_tokens: before every call pi-ai clamps it to
* `contextWindow − its own token estimate − 4096` (clampMaxTokensToContext,
* CONTEXT_SAFETY_TOKENS) and then shrinks the thinking budget so that only 1024
* answer tokens remain. The window was never full. On a 24k window with maxTokens
* 4096 the clamp bites at any estimate above 16384 — the same number as the
* compaction threshold with reserveTokens 8192 — so it hits in the gaps the threshold
* check never sees: the first request after a compaction (no server usage to anchor
* on, so the system prompt and the tool schemas join the estimate) and the first call
* of a user turn (the new message). Thinking models feel it first: 2048 of thinking
* leaves a clamped reply about 1024 tokens of answer, and a `write` payload is cut.
*
* The old message here blamed every "overflow" on `reserveTokens < maxTokens`, which is
* wrong for case 2 — the one that actually shows up (Ornith, 2026-10-22). Now the
* notification names the case and prints the numbers (output vs maxTokens, context vs
* window, the estimate the clamp implies), so the next occurrence is diagnosable from the
* screen instead of from a session file.
*
* Note there is no event for a *failed* auto-compaction — `onError` exists only on the
* manual `ctx.compact()` call — so catching the overflow entry is the earliest reliable
* warning available.
* warning available. Observe-only: it never alters the compaction itself.
*/
import type { AssistantMessage } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
/** pi-ai's CONTEXT_SAFETY_TOKENS: the margin clampMaxTokensToContext keeps below the window. */
const PI_CONTEXT_SAFETY_TOKENS = 4096;
/** Last assistant message on the branch — the turn that triggered the recovery. */
function lastAssistant(entries: readonly unknown[]): AssistantMessage | undefined {
for (let i = entries.length - 1; i >= 0; i--) {
const e = entries[i] as { type?: string; message?: { role?: string } };
if (e.type === "message" && e.message?.role === "assistant") return e.message as AssistantMessage;
}
return undefined;
}
/** One line saying which kind of "overflow" this is, with the numbers that prove it. */
export function describeOverflow(last: AssistantMessage | undefined, window: number, maxTokens: number): string {
if (!last) return "no assistant message on the branch to inspect.";
const u = last.usage;
const output = u?.output ?? 0;
const context = (u?.input ?? 0) + (u?.cacheRead ?? 0) + (u?.cacheWrite ?? 0) + output;
const ctxStr = window > 0 ? `${context}/${window}` : `${context}`;
const checkWindow = "check contextWindow against the server ctx-size (CLAUDE.md > Compaction tuning).";
if (last.stopReason === "error") {
return `the server rejected the request at context ${ctxStr} (${last.errorMessage ?? "no message"}) — a real overflow; ${checkWindow}`;
}
if (last.stopReason === "length") {
if (output === 0 && window > 0 && context >= window * 0.99) {
return `the server produced no tokens at context ${ctxStr} — the prompt alone filled the window; ${checkWindow}`;
}
if (window > 0 && context >= window - 64) {
return (
`the server ran out of context mid-reply (${ctxStr}, output ${output}/${maxTokens}) — the prompt was ` +
`under-estimated (big tool results or images count more than chars ÷ 4); not a reserveTokens problem.`
);
}
if (maxTokens > 0 && output < maxTokens) {
const estimate = window > 0 ? ` pi's estimate at request time ≈ ${window - PI_CONTEXT_SAFETY_TOKENS - output}, server-side context ${ctxStr}.` : "";
return (
`truncated-reply recovery, NOT a full window: the reply stopped at ${output} of ${maxTokens} max tokens, ` +
`so pi had clamped max_tokens to window − estimate − ${PI_CONTEXT_SAFETY_TOKENS}.${estimate} ` +
`reserveTokens is not the knob — see CLAUDE.md > Compaction tuning > max_tokens clamp.`
);
}
}
return `stopReason ${last.stopReason}, context ${ctxStr}, output ${output}/${maxTokens} — input reported above the declared window; ${checkWindow}`;
}
export default function (pi: ExtensionAPI) {
let overflowCount = 0;
@@ -36,12 +99,9 @@ export default function (pi: ExtensionAPI) {
if (event.reason === "overflow") {
overflowCount++;
ctx.ui.notify(
`Compacting in OVERFLOW recovery (#${overflowCount}) — the context window was already ` +
`full before compaction could fire. Raise compaction.reserveTokens above this model's ` +
`maxTokens, or cap maxTokens in models.json. See CLAUDE.md > Compaction tuning.`,
"warning",
);
const model = ctx.model;
const verdict = describeOverflow(lastAssistant(event.branchEntries), model?.contextWindow ?? 0, model?.maxTokens ?? 0);
ctx.ui.notify(`Compacting in OVERFLOW recovery (#${overflowCount}) — ${verdict}`, "warning");
}
// Never alter the compaction itself — observe only.
+151 -14
View File
@@ -17,7 +17,13 @@
* 󰚩 model model id + · thinking level (Claude 🤖) U+F06A9 nf-md-robot
* 󰈚 context % of context window used (+ tokens) (Claude 📝) U+F021A nf-md-text_box
* 󰓅 t/s last response's decode throughput U+F04C5 nf-md-speedometer
* ↑ ↓ tokens session input / output tokens (Claude 💰) plain arrows
* (llama.cpp's own server-side number + dim ` pp N` prompt-processing
* speed when the final SSE chunk carries `timings`; otherwise the
* client-side clock -- see "server-side timings" below)
* ↑ ↓ tokens session prompt / output tokens (Claude 🔢) plain arrows
* (prompt = input + cache read + cache write: everything the server
* processed, matching the Claude bar — llama.cpp reports a cached
* prefix as cacheRead, so bare usage.input is a few dozen tokens)
*
* git, context, t/s and tokens only appear once there is data for them (i.e. after
* the first response, and git only inside a git repo). Extension statuses (e.g.
@@ -58,12 +64,111 @@ const CTX_WARN_AT = 80;
// direct access to it). Defaults to the settings.json default of "off".
let thinkingLevel = "off";
// Decode throughput (tokens/sec) of the most recent assistant response, measured
// from the first streamed token to message end so prompt-eval time is excluded.
// Decode throughput (tokens/sec) of the most recent assistant response. Preferred
// source: the server's own timings (below). Fallback: a client-side clock from the
// first streamed token to message end, so prompt-eval time is excluded.
let lastTps = 0;
let lastPp = 0; // prompt-processing t/s, server-side only (0 = unknown)
let genFirstTokenAt = 0;
let sawToken = false;
// --- server-side timings (llama.cpp) ----------------------------------------
// llama.cpp puts a `timings` object on the final SSE chunk (the one carrying `usage`):
// prompt_n / prompt_ms / prompt_per_second, predicted_n / predicted_ms /
// predicted_per_second, cache_n, draft_n / draft_n_accepted. pi-ai's openai-completions
// provider reads only `usage` and drops the rest, and the extension API has no HTTP
// hook, so the one way to see it is to wrap globalThis.fetch and tee the SSE body.
// That is sanctioned: pi's http-dispatcher installs undici's fetch at startup and
// deliberately keeps a later override ("If a caller replaced fetch after module load,
// preserve that deliberate override"), and pi-ai builds `new OpenAI({ fetch: undefined })`
// per request, which falls back to globalThis.fetch -- so a wrapper installed at
// session_start sees every chat completion. Only POST */chat/completions responses
// with a text/event-stream body are teed; other providers' streams carry no `timings`
// and are passed through untouched apart from the tee. The tap stays installed for the
// life of the process (`/statusbar` off just stops displaying it; `/reload` rebinds the
// sink instead of wrapping twice).
interface ServerTimings {
tps: number; // predicted_per_second
pp: number; // prompt_per_second
predictedN: number;
promptN: number;
cacheN: number;
}
// Latest timings seen on the wire, consumed by message_end. Paired with the message by
// predicted_n === usage.output, so a compaction/summary call's timings never get
// attributed to the next visible response.
let pendingServerTimings: ServerTimings | undefined;
const FETCH_TAP_KEY = "__piStatusbarFetchTap";
/** Read one branch of a teed SSE body and report every `timings` object it carries. */
async function scanSse(stream: ReadableStream<Uint8Array>, onTimings: (t: ServerTimings) => void): Promise<void> {
const reader = stream.getReader();
const dec = new TextDecoder();
let buf = "";
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let nl: number;
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl).trim();
buf = buf.slice(nl + 1);
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
if (!payload.includes('"timings"')) continue; // cheap pre-filter: parse only the final chunk
try {
const t = JSON.parse(payload)?.timings;
if (t && typeof t.predicted_per_second === "number") {
onTimings({
tps: t.predicted_per_second,
pp: typeof t.prompt_per_second === "number" ? t.prompt_per_second : 0,
predictedN: t.predicted_n ?? 0,
promptN: t.prompt_n ?? 0,
cacheN: t.cache_n ?? 0,
});
}
} catch {
// not JSON: ignore
}
}
}
} catch {
// aborted / cancelled stream: the primary branch reports that, we just stop
}
}
/** Wrap globalThis.fetch once; on /reload only rebind the sink. */
function installFetchTap(): void {
const g = globalThis as unknown as Record<string, unknown>;
const sink = (t: ServerTimings) => {
pendingServerTimings = t;
};
const existing = g[FETCH_TAP_KEY] as { sink: (t: ServerTimings) => void } | undefined;
if (existing) {
existing.sink = sink;
return;
}
const tap = { sink };
g[FETCH_TAP_KEY] = tap;
const orig = globalThis.fetch;
globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {
const res = await orig(input, init);
let url = "";
try {
url = typeof input === "string" ? input : input instanceof URL ? input.href : ((input as Request)?.url ?? "");
} catch {
return res;
}
if (!url.endsWith("/chat/completions") || !res.body) return res;
if (!(res.headers.get("content-type") ?? "").includes("text/event-stream")) return res;
const [ours, theirs] = res.body.tee();
void scanSse(ours, (t) => tap.sink(t));
return new Response(theirs, res);
}) as typeof fetch;
}
// Set from inside the footer factory so events can trigger a re-render.
let requestRender: (() => void) | undefined;
@@ -74,12 +179,27 @@ function collapseHome(p: string): string {
return p;
}
/** 1234 -> 1.2k, 843000 -> 843k, 1234567 -> 1.2M (same boundaries as the Claude bar's
* fmt_tokens: 0.9995 / 9.95 so a value never prints as "10.0k" or "1000k"). */
function fmtTokens(n: number): string {
return n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`;
for (const [unit, div] of [
["M", 1e6],
["k", 1e3],
] as const) {
const v = n / div;
if (v < 0.9995) continue;
return v >= 9.95 ? `${v.toFixed(0)}${unit}` : `${v.toFixed(1)}${unit}`;
}
return `${Math.round(n)}`;
}
/** Cumulative session tokens, plus the last request's input tokens (~ current
* context-window fill). */
/** Cumulative session tokens, plus the last request's prompt size (~ current
* context-window fill).
*
* Prompt = input + cacheRead + cacheWrite. pi-ai splits a cached prefix out of
* `usage.input` (llama.cpp too: the second call of a session reports e.g. input 20,
* cacheRead 2417), so summing bare `input` undercounted ↑ and -- worse -- made the
* context % and the ≥80% warning read near zero on every cached call. */
function tokenStats(ctx: ExtensionContext): {
input: number;
output: number;
@@ -92,9 +212,10 @@ function tokenStats(ctx: ExtensionContext): {
if (entry.type === "message" && entry.message.role === "assistant") {
const usage = (entry.message as AssistantMessage).usage;
if (!usage) continue; // aborted stream: no usage — a throwing render kills the TUI
input += usage.input;
output += usage.output;
lastInput = usage.input;
const prompt = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
input += prompt;
output += usage.output ?? 0;
lastInput = prompt;
}
}
return { input, output, lastInput };
@@ -161,8 +282,19 @@ export default function (pi: ExtensionAPI) {
pi.on("message_end", async (event, ctx) => {
if (event.message?.role !== "assistant") return;
const output = (event.message as AssistantMessage).usage?.output ?? 0;
// The tee reader and pi-ai consume the same final chunk; pi-ai's path to this
// event is longer, but give the reader one macrotask in case it is still behind.
if (!pendingServerTimings) await new Promise((r) => setTimeout(r, 20));
const srv = pendingServerTimings;
pendingServerTimings = undefined;
if (srv && srv.predictedN > 0 && (output === 0 || srv.predictedN === output)) {
lastTps = srv.tps;
lastPp = srv.pp;
} else {
const secs = genFirstTokenAt ? (Date.now() - genFirstTokenAt) / 1000 : 0;
if (secs > 0 && output > 0) lastTps = output / secs;
lastPp = 0;
}
requestRender?.();
if (enabled) updateContextWidget(ctx);
});
@@ -217,14 +349,15 @@ export default function (pi: ExtensionAPI) {
parts.push(seg);
}
// 󰓅 tokens/sec of the last response
// 󰓅 tokens/sec of the last response (+ dim prompt-processing t/s when the
// server reported it)
if (lastTps > 0) {
parts.push(
theme.fg("accent", ICON.tps) + " " + theme.fg("text", lastTps.toFixed(1)) + theme.fg("dim", " t/s"),
);
let seg = theme.fg("accent", ICON.tps) + " " + theme.fg("text", lastTps.toFixed(1)) + theme.fg("dim", " t/s");
if (lastPp > 0) seg += theme.fg("dim", ` pp ${lastPp.toFixed(0)}`);
parts.push(seg);
}
// ↑ ↓ cumulative session tokens (Claude's 💰 slot; local models are $0)
// ↑ ↓ cumulative session prompt / output tokens (Claude's 🔢 segment)
if (input || output) {
parts.push(theme.fg("dim", `${ICON.up}${fmtTokens(input)} ${ICON.down}${fmtTokens(output)}`));
}
@@ -236,6 +369,10 @@ export default function (pi: ExtensionAPI) {
};
pi.on("session_start", async (_event, ctx) => {
// Installed here, not at load time: pi's http-dispatcher only swaps in undici's
// fetch (idle timeout, proxy) while globalThis.fetch is still the original, so the
// tap must not get there first. By session_start, startup is done.
installFetchTap();
if (enabled) install(ctx);
});
+4 -4
View File
@@ -107,11 +107,11 @@
},
{
"id": "Ornith-1.5-35B-A3B-IQ3_XXS",
"name": "Ornith 1.5 35B · 24k · vision — agentic coder, thinking (~32 t/s)",
"name": "Ornith 1.5 35B · 32k · vision — agentic coder, thinking (~32 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template", "thinkingTokenBudgetField": "thinking_budget_tokens" },
"input": ["text", "image"],
"contextWindow": 24576,
"contextWindow": 32768,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
},
@@ -242,11 +242,11 @@
},
{
"id": "Ornith-1.5-35B-A3B-IQ3_XXS",
"name": "Ornith 1.5 35B · 24k · vision — agentic coder, thinking (~32 t/s)",
"name": "Ornith 1.5 35B · 32k · vision — agentic coder, thinking (~32 t/s)",
"reasoning": true,
"compat": { "thinkingFormat": "qwen-chat-template", "thinkingTokenBudgetField": "thinking_budget_tokens" },
"input": ["text", "image"],
"contextWindow": 24576,
"contextWindow": 32768,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
},
+3 -3
View File
@@ -53,8 +53,8 @@
},
"borderSync": {
"insert": "host",
"normal": "host",
"visual": "host",
"normal": "mode",
"visual": "mode",
"ex": "host"
},
"labelSync": {
@@ -64,7 +64,7 @@
"ex": "mode"
}
},
"lastChangelogVersion": "1.0.0",
"lastChangelogVersion": "1.5.0",
"packages": [
"npm:pi-vim"
]
-21
View File
@@ -1,21 +0,0 @@
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
# See https://wiki.hyprland.org/Configuring/Animations/
animations {
enabled = yes
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}
-153
View File
@@ -1,153 +0,0 @@
#   ░▒▒▒░░░░░▓▓          ___________
# ░░▒▒▒░░░░░▓▓        //___________/
# ░░▒▒▒░░░░░▓▓     _   _ _    _ _____
# ░░▒▒░░░░░▓▓▓▓▓▓ | | | | |  | |  __/
# ░▒▒░░░░▓▓   ▓▓ | |_| | |_/ /| |___
#  ░▒▒░░▓▓   ▓▓   \__  |____/ |____/
#    ░▒▓▓   ▓▓  //____/
$scrPath = $HOME/.local/share/bin # set scripts path
# █▀▄▀█ █▀█ █▄░█ █ ▀█▀ █▀█ █▀█
# █░▀░█ █▄█ █░▀█ █ ░█░ █▄█ █▀▄
# See https://wiki.hyprland.org/Configuring/Monitors/
# monitor = ,preferred,auto,auto
# █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█
# █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█
# See https://wiki.hyprland.org/Configuring/Keywords/
exec-once = $scrPath/resetxdgportal.sh # reset XDPH for screenshare
exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP # for XDPH
exec-once = dbus-update-activation-environment --systemd --all # for XDPH
exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP # for XDPH
exec-once = $scrPath/polkitkdeauth.sh # authentication dialogue for GUI apps
exec-once = waybar # launch the system bar
exec-once = blueman-applet # systray app for Bluetooth
exec-once = udiskie --no-automount --smart-tray # front-end that allows to manage removable media
exec-once = nm-applet --indicator # systray app for Network/Wifi
exec-once = dunst # start notification demon
exec-once = wl-paste --type text --watch cliphist store # clipboard store text data
exec-once = wl-paste --type image --watch cliphist store # clipboard store image data
exec-once = $scrPath/swwwallpaper.sh # start wallpaper daemon
exec-once = $scrPath/batterynotify.sh # battery notification
# (fl remaps keys with evremap (see .config/evremap/) — the old xremap exec-once was dead, no config here)
exec-once = swayidle -w timeout 180 'swaylock --timestr %H:%M & brightnessctl -s & brightnessctl set 15' resume 'brightnessctl -r'
# █▀▀ █▄░█ █░█
# ██▄ █░▀█ ▀▄▀
# See https://wiki.hyprland.org/Configuring/Environment-variables/
env = PATH,$PATH:$scrPath
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_TYPE,wayland
env = XDG_SESSION_DESKTOP,Hyprland
env = QT_QPA_PLATFORM,wayland;xcb
env = QT_QPA_PLATFORMTHEME,qt6ct
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
env = QT_AUTO_SCREEN_SCALE_FACTOR,1
env = MOZ_ENABLE_WAYLAND,1
env = GDK_SCALE,1
# █ █▄░█ █▀█ █░█ ▀█▀
# █ █░▀█ █▀▀ █▄█ ░█░
# See https://wiki.hyprland.org/Configuring/Variables/
# keyremap
input {
kb_layout = us
# kb_options=caps:swapescape
# kb_options=caps:ctrl_modifier
# follow_mouse = 1
touchpad {
natural_scroll = no
}
force_no_accel = 0
sensitivity = +1.0
}
# See https://wiki.hyprland.org/Configuring/Keywords/#executing
device {
name = ps/2-generic-mouse
sensitivity = +1.0
middle_button_emulation = true
}
device {
name = synps/2-synaptics-touchpad
sensitivity = 0.1
}
device {
name = xremap
sensitivity = +1.0
}
# See https://wiki.hyprland.org/Configuring/Variables/
# gestures {
# workspace_swipe = true
# workspace_swipe_fingers = 3
# }
# █░░ ▄▀█ █▄█ █▀█ █░█ ▀█▀ █▀
# █▄▄ █▀█ ░█░ █▄█ █▄█ ░█░ ▄█
# See https://wiki.hyprland.org/Configuring/Dwindle-Layout/
dwindle {
pseudotile = yes
preserve_split = yes
}
# See https://wiki.hyprland.org/Configuring/Master-Layout/
master {
new_status = master
}
# █▀▄▀█ █ █▀ █▀▀
# █░▀░█ █ ▄█ █▄▄
# See https://wiki.hyprland.org/Configuring/Variables/
misc {
vrr = 0
disable_hyprland_logo = true
disable_splash_rendering = true
force_default_wallpaper = 0
}
xwayland {
force_zero_scaling = true
}
# █▀ █▀█ █░█ █▀█ █▀▀ █▀▀
# ▄█ █▄█ █▄█ █▀▄ █▄▄ ██▄
source = ~/.config/hypr/animations.conf
source = ~/.config/hypr/keybindings.conf
source = ~/.config/hypr/windowrules.conf
source = ~/.config/hypr/themes/common.conf # shared theme settings
source = ~/.config/hypr/themes/theme.conf # theme specific settings
source = ~/.config/hypr/themes/colors.conf # wallbash color override
source = ~/.config/hypr/monitors.conf # initially empty, to be configured by user and remains static
source = ~/.config/hypr/userprefs.conf # initially empty, to be configured by user and remains static
# Note: as userprefs.conf is sourced at the end, settings configured in this file will override the defaults
-221
View File
@@ -1,221 +0,0 @@
# █▄▀ █▀▀ █▄█ █▄▄ █ █▄░█ █▀▄ █ █▄░█ █▀▀ █▀
# █░█ ██▄ ░█░ █▄█ █ █░▀█ █▄▀ █ █░▀█ █▄█ ▄█
# See https://wiki.hyprland.org/Configuring/Keywords/
# & https://wiki.hyprland.org/Configuring/Binds/
# Main modifier
$mainMod = Super # super / meta / windows key
# Assign apps
$term = kitty # fish alacritty
$file = dolphin
$browser = librewolf
$yt = freetube
$wm=Window Management
$d=[$wm]
# Window/Session actions
bind = $mainMod, Q, exec, $scrPath/dontkillsteam.sh # close focused window
bind = Alt, F4, exec, $scrPath/dontkillsteam.sh # close focused window
bind = $mainMod, Delete, exit, # kill hyprland session
bind = $mainMod, W, togglefloating, # toggle the window between focus and float
bind = $mainMod, G, togglegroup, # toggle the window between focus and group
bind = Alt, Return, fullscreen, # toggle the window between focus and fullscreen
bind = $mainMod, L, exec, swaylock --timestr %H:%M # launch lock screen
bind = Ctrl+Alt, W, exec, killall waybar || waybar # toggle waybar
# bind = $mainMod+Shift, F, exec, $scrPath/windowpin.sh # toggle pin on focused window
# bind = $mainMod, Backspace, exec, $scrPath/logoutlaunch.sh # launch logout menu
# Application shortcuts
bind = $mainMod, T, exec, $term
bind = Ctrl+Alt, T, exec, $term # launch terminal emulator
bind = Ctrl+Shift+Alt, T, exec, st # launch tilix terminal emulator - using for ssh
bind = $mainMod+Shift, T, exec, st
bind = $mainMod+Ctrl, T, exec, tilix # launch tilix terminal emulator
bind = $mainMod, E, exec, $file # launch file manager
bind = $mainMod, F, exec, $yt # launch yt alternative
bind = $mainMod, B, exec, $browser # launch web browser
bind = $mainMod, G, exec, gajim # launch messanger
bind = $mainMod, K, exec, keepassxc # launch password manager
# bindd = $mainMod, N, Neovim, exec, $term -e nvim
bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf
bindd = Ctrl+Shift, Escape, Btop, exec, kitty -e btop
bindd = Ctrl+Alt, Delete, Btop, exec, kitty -e btop
# bind = Ctrl+Shift, Escape, exec, $scrPath/sysmonlaunch.sh # launch system monitor (htop/btop or fallback to top)
# bind = $mainMod+Shift, B, exec, $browser, title: ^(LibreWolf Private Browsing)$ # launch private web browser
# Rofi menus
bind = $mainMod, A, exec, pkill -x rofi || $scrPath/rofilaunch.sh d # launch application launcher
bind = $mainMod, Tab, exec, pkill -x rofi || $scrPath/rofilaunch.sh w # launch window switcher
bind = $mainMod+Shift, E, exec, pkill -x rofi || $scrPath/rofilaunch.sh f # launch file explorer
# Audio control
# bindl = , F10, exec, $scrPath/volumecontrol.sh -o m # toggle audio mute
# bindel = , F11, exec, $scrPath/volumecontrol.sh -o d # decrease volume
# bindel = , F12, exec, $scrPath/volumecontrol.sh -o i # increase volume
bindl = , XF86AudioMute, exec, $scrPath/volumecontrol.sh -o m # toggle audio mute
bindl = , XF86AudioMicMute, exec, $scrPath/volumecontrol.sh -i m # toggle microphone mute
bindel = , XF86AudioLowerVolume, exec, $scrPath/volumecontrol.sh -o d # decrease volume
bindel = , XF86AudioRaiseVolume, exec, $scrPath/volumecontrol.sh -o i # increase volume
# Media control
bindl = , XF86AudioPlay, exec, playerctl play-pause # toggle between media play and pause
bindl = , XF86AudioPause, exec, playerctl play-pause # toggle between media play and pause
bindl = , XF86AudioNext, exec, playerctl next # media next
bindl = , XF86AudioPrev, exec, playerctl previous # media previous
# Brightness control
bindel = , XF86MonBrightnessUp, exec, $scrPath/brightnesscontrol.sh i # increase brightness
bindel = , XF86MonBrightnessDown, exec, $scrPath/brightnesscontrol.sh d # decrease brightness
# Move between grouped windows
# bind = $mainMod CTRL , H, changegroupactive, b
# bind = $mainMod CTRL , L, changegroupactive, f
# Screenshot/Screencapture
bind = $mainMod, P, exec, $scrPath/screenshot.sh s # partial screenshot capture
bind = $mainMod+Ctrl, P, exec, $scrPath/screenshot.sh sf # partial screenshot capture (frozen screen)
bind = $mainMod+Alt, P, exec, $scrPath/screenshot.sh m # monitor screenshot capture
bind = $mainMod+Shift, P, exec, hyprpicker # colorpicker
bind = , Print, exec, $scrPath/screenshot.sh p # all monitors screenshot capture
# Custom scripts
bind = $mainMod+Alt, G, exec, $scrPath/gamemode.sh # disable hypr effects for gamemode
bind = $mainMod+Alt, Right, exec, $scrPath/swwwallpaper.sh -n # next wallpaper
bind = $mainMod+Alt, Left, exec, $scrPath/swwwallpaper.sh -p # previous wallpaper
# bind = $mainMod+Alt, Up, exec, $scrPath/wbarconfgen.sh n # next waybar mode
# bind = $mainMod+Alt, Down, exec, $scrPath/wbarconfgen.sh p # previous waybar mode
bind = $mainMod+Shift, R, exec, pkill -x rofi || $scrPath/wallbashtoggle.sh -m # launch wallbash mode select menu
# bind = $mainMod+Shift, T, exec, pkill -x rofi || $scrPath/themeselect.sh # launch theme select menu
bind = $mainMod+Shift, A, exec, pkill -x rofi || $scrPath/rofiselect.sh # launch select menu
bind = $mainMod+Shift, W, exec, pkill -x rofi || $scrPath/swwwallselect.sh # launch wallpaper select menu
bind = $mainMod, V, exec, pkill -x rofi || $scrPath/cliphist.sh c # launch clipboard
# bind = $mainMod, K, exec, $scrPath/keyboardswitch.sh # switch keyboard layout
bind = $mainMod, slash, exec, pkill -x rofi || $scrPath/keybinds_hint.sh c # launch keybinds hint
# Move/Change window focus
bind = $mainMod, Left, movefocus, l #chage focus to left window
bind = $mainMod, Right, movefocus, r
bind = $mainMod, Up, movefocus, u
bind = $mainMod, Down, movefocus, d
bindd = $mainMod Control, Left, $d focus left, movefocus, l
bindd = $mainMod Control, Right, $d focus right , movefocus, r
bindd = $mainMod Control, Up, $d focus up , movefocus, u
bindd = $mainMod Control, Down, $d focus down, movefocus, d
bindd = $mainMod Control, h, $d focus left, movefocus, l
bindd = $mainMod Control, l, $d focus right , movefocus, r
bindd = $mainMod Control, k, $d focus up , movefocus, u
bindd = $mainMod Control, j , $d focus down, movefocus, d
bindd = Alt Control, Left, $d focus left, movefocus, l
bindd = Alt Control, Right, $d focus right , movefocus, r
bindd = Alt Control, Up, $d focus up , movefocus, u
bindd = Alt Control, Down, $d focus down, movefocus, d
bindd = Alt Control, h, $d focus left, movefocus, l
bindd = Alt Control, l, $d focus right , movefocus, r
bindd = Alt Control, k, $d focus up , movefocus, u
bindd = Alt Control, j , $d focus down, movefocus, d
bindd = Alt, Tab, $d focus , movefocus, d
# Move active window around current workspace with mainMod + Shift + Control [←→↑↓]
$d=[$wm|Move active window across workspace]
$moveactivewindow=grep -q "true" <<< $(hyprctl activewindow -j | jq -r .floating) && hyprctl dispatch moveactive
bindde = $mainMod Alt, left, Move activewindow to the left, exec, $moveactivewindow -30 0 || hyprctl dispatch movewindow l
bindde = $mainMod Alt, right, Move activewindow to the right, exec, $moveactivewindow 30 0 || hyprctl dispatch movewindow r
bindde = $mainMod Alt, up, Move activewindow up, exec, $moveactivewindow 0 -30 || hyprctl dispatch movewindow u
bindde = $mainMod Alt, down, Move activewindow down, exec, $moveactivewindow 0 30 || hyprctl dispatch movewindow d
bindde = $mainMod Alt, h, Move activewindow to the left, exec, $moveactivewindow -30 0 || hyprctl dispatch movewindow l
bindde = $mainMod Alt, l, Move activewindow to the right, exec, $moveactivewindow 30 0 || hyprctl dispatch movewindow r
bindde = $mainMod Alt, k, Move activewindow up, exec, $moveactivewindow 0 -30 || hyprctl dispatch movewindow u
bindde = $mainMod Alt, j, Move activewindow down, exec, $moveactivewindow 0 30 || hyprctl dispatch movewindow d
# Switch workspaces with mod + [0-9]
bind = $mainMod, 1, workspace, r~1
bind = $mainMod, 2, workspace, r~2
bind = $mainMod, 3, workspace, r~3
bind = $mainMod, 4, workspace, r~4
bind = $mainMod, 5, workspace, r~5
bind = $mainMod, 6, workspace, r~6
bind = $mainMod, 7, workspace, r~7
bind = $mainMod, 8, workspace, r~8
bind = $mainMod, 9, workspace, r~9
bind = $mainMod, 0, workspace, r~10
# Move focused window to a workspace
bind = $mainMod+Ctrl, 1, movetoworkspace, r~1
bind = $mainMod+Ctrl, 2, movetoworkspace, r~2
bind = $mainMod+Ctrl, 3, movetoworkspace, r~3
bind = $mainMod+Ctrl, 4, movetoworkspace, r~4
bind = $mainMod+Ctrl, 5, movetoworkspace, r~5
bind = $mainMod+Ctrl, 6, movetoworkspace, r~6
bind = $mainMod+Ctrl, 7, movetoworkspace, r~7
bind = $mainMod+Ctrl, 8, movetoworkspace, r~8
bind = $mainMod+Ctrl, 9, movetoworkspace, r~9
bind = $mainMod+Ctrl, 0, movetoworkspace, r~10
# Move focused window to a workspace silently
bind = $mainMod+Alt, 1, movetoworkspacesilent, r~1
bind = $mainMod+Alt, 2, movetoworkspacesilent, r~2
bind = $mainMod+Alt, 3, movetoworkspacesilent, r~3
bind = $mainMod+Alt, 4, movetoworkspacesilent, r~4
bind = $mainMod+Alt, 5, movetoworkspacesilent, r~5
bind = $mainMod+Alt, 6, movetoworkspacesilent, r~6
bind = $mainMod+Alt, 7, movetoworkspacesilent, r~7
bind = $mainMod+Alt, 8, movetoworkspacesilent, r~8
bind = $mainMod+Alt, 9, movetoworkspacesilent, r~9
bind = $mainMod+Alt, 0, movetoworkspacesilent, r~10
# Move/Switch to special workspace (scratchpad)
bind = $mainMod+Alt, S, movetoworkspacesilent, special
bind = $mainMod, S, togglespecialworkspace,
# Toggle focused window split
bind = $mainMod, J, togglesplit
$d=[$ws|Navigation|Relative workspace]
bindd = $mainMod Control Shift, Right, $d change active workspace forwards , workspace, r+1
bindd = $mainMod Control Shift, Left, $d change active workspace backwards , workspace, r-1
bindd = $mainMod Control Shift, L, $d change active workspace forwards , workspace, r+1
bindd = $mainMod Control Shift, H, $d change active workspace backwards , workspace, r-1
# Resize windows
binde = $mainMod+Shift, Right, resizeactive, 30 0
binde = $mainMod+Shift, Left, resizeactive, -30 0
binde = $mainMod+Shift, Up, resizeactive, 0 -30
binde = $mainMod+Shift, Down, resizeactive, 0 30
bindde = $mainMod Shift, l, $d resize window right , resizeactive, 30 0
bindde = $mainMod Shift, h, $d resize window left, resizeactive, -30 0
bindde = $mainMod Shift, k, $d resize window up, resizeactive, 0 -30
bindde = $mainMod Shift, j, $d resize window down, resizeactive, 0 30
# added that to mimic i3's behavior, where pushing a workspace to another monitor also focuses it
# bind = $mainMod SHIFT, P, focusmonitor, +1
# bind = $mainMod SHIFT, P, movecurrentworkspacetomonitor, +1
# Move active window around current workspace with mainMod + SHIFT + CTRL [←→↑↓]
# $moveactivewindow=grep -q "true" <<< $(hyprctl activewindow -j | jq -r .floating) && hyprctl dispatch moveactive
binded = $mainMod+Shift+Ctrl, left,Move activewindow to the left,exec, $moveactivewindow -30 0 || hyprctl dispatch movewindow l
binded = $mainMod+Shift+Ctrl, right,Move activewindow to the right,exec, $moveactivewindow 30 0 || hyprctl dispatch movewindow r
binded = $mainMod+Shift+Ctrl, up,Move activewindow to the up,exec, $moveactivewindow 0 -30 || hyprctl dispatch movewindow u
binded = $mainMod+Shift+Ctrl, down,Move activewindow to the down,exec, $moveactivewindow 0 30 || hyprctl dispatch movewindow d
binded = $mainMod+Shift+Ctrl, h,Move activewindow to the left,exec, $moveactivewindow -30 0 || hyprctl dispatch movewindow l
binded = $mainMod+Shift+Ctrl, l,Move activewindow to the right,exec, $moveactivewindow 30 0 || hyprctl dispatch movewindow r
binded = $mainMod+Shift+Ctrl, k,Move activewindow to the up,exec, $moveactivewindow 0 -30 || hyprctl dispatch movewindow u
binded = $mainMod+Shift+Ctrl, j,Move activewindow to the down,exec, $moveactivewindow 0 30 || hyprctl dispatch movewindow d
# Scroll through existing workspaces
bind = $mainMod, mouse_down, workspace, e+1
bind = $mainMod, mouse_up, workspace, e-1
# Move/Resize focused window
bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
bindm = $mainMod, Z, movewindow
bindm = $mainMod, X, resizewindow
-62
View File
@@ -1,62 +0,0 @@
# █▀▄▀█ █▀█ █▄░█ █ ▀█▀ █▀█ █▀█ █▀
# █░▀░█ █▄█ █░▀█ █ ░█░ █▄█ █▀▄ ▄█
# Set your monitor configuration here
# See https://wiki.hyprland.org/Configuring/Monitors/
# For a sample file, please refer to https://github.com/prasanthrangan/hyprdots/blob/main/Configs/.config/hypr/monitors.t2
$mainScreen = eDP-1
$leftScreen = DP-2
$rightScreen = HDMI-A-1
monitor = $mainScreen, 1366x768@60.14, 0x0, 1 # main screen
monitor = $leftScreen, 1920x1080@60, -1920x0, 1 # left
monitor = $rightScreen, 1920x1080@60,1366x0, 1 # right
# monitor=HDMI-A-1,1920x1080@60,-1366x0,1 # hdmi on left
# monitor=HDMI-A-1,1920x1080@60,0x-768,1 # hdmi on top
# monitor=DP-2,640x480@59.94Hz,0x0,1 # vga
# monitor = HDMI-A-1, 1366x768@60.00Hz, auto , 1, mirror, eDP-1 #mirror lapscreen
# monitor = eDP-1, 1920x1080@60.00Hz, auto , 1, mirror, HDMI-A-1 #main hdmi, lapscreen mirrors
# monitor = , preferred, auto, 1, mirror, DP-2 # dmz projector
# monitor = eDP-1, 1920x1080@60, auto , 1, mirror, DP-2 #mirror vga test
#Main Monitor
workspace = 1, monitor:$mainScreen, default:true
workspace = 2, monitor:$mainScreen
workspace = 3, monitor:$mainScreen
workspace = 4, monitor:$mainScreen
workspace = 5, monitor:$mainScreen
workspace = 6, monitor:$mainScreen
workspace = 7, monitor:$mainScreen
workspace = 8, monitor:$mainScreen
workspace = 9, monitor:$mainScreen
workspace = 10,monitor:$mainScreen
# Second Monitor
workspace = 11, monitor:$rightScreen, default:true
workspace = 12, monitor:$rightScreen
workspace = 13, monitor:$rightScreen
workspace = 14, monitor:$rightScreen
workspace = 15, monitor:$rightScreen
workspace = 16, monitor:$rightScreen
workspace = 17, monitor:$rightScreen
workspace = 18, monitor:$rightScreen
workspace = 19, monitor:$rightScreen
workspace = 20, monitor:$rightScreen
# Third Monitor
workspace = 21, monitor:$leftScreen, default:true
workspace = 22, monitor:$leftScreen
workspace = 23, monitor:$leftScreen
workspace = 24, monitor:$leftScreen
workspace = 25, monitor:$leftScreen
workspace = 26, monitor:$leftScreen
workspace = 27, monitor:$leftScreen
workspace = 28, monitor:$leftScreen
workspace = 29, monitor:$leftScreen
workspace = 30, monitor:$leftScreen
-68
View File
@@ -1,68 +0,0 @@
# █░█ █▀ █▀▀ █▀█   █▀█ █▀█ █▀▀ █▀▀ █▀
# █▄█ ▄█ ██▄ █▀▄   █▀▀ █▀▄ ██▄ █▀░ ▄█
# Set your personal hyprland configuration here
# For a sample file, please refer to https://github.com/prasanthrangan/hyprdots/blob/main/Configs/.config/hypr/userprefs.t2
#
########################
# █▀▀ █▄░█ █░█
# ██▄ █░▀█ ▀▄▀
# env = MANGOHUD,1
# env = MANGOHUD_DLSYM,1
# █▄▀ █▀▀ █▄█ █▄▄ █ █▄░█ █▀▄ █ █▄░█ █▀▀ █▀
# █░█ ██▄ ░█░ █▄█ █ █░▀█ █▄▀ █ █░▀█ █▄█ ▄█
# bind = Ctrl+Alt+Shift, R, pass, ^(com\.obsproject\.Studio)$ # toggle obs screen recording // install obs flatpak
# bind = $mainMod+Shift, G, exec, pkill -x rofi || $scrPath/gamelauncher.sh # launch steam game launcher // install steam
# bind = $mainMod, Period, exec, emote # launch emoji selector // install emote
bind = $mainMod+Alt, XF86MonBrightnessDown, exec, hyprshade on blue-light-filter # enable blue light filter // install hyprshade
bind = $mainMod+Alt, XF86MonBrightnessUp, exec, hyprshade off # disable blue light filter // install hyprshade
# █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█
# █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█
# exec-once = swayidle -w timeout 600 'swaylock' timeout 900 'hyprctl dispatch dpms off' resume 'hyprctl dispatch dpms on' # lock after 10 mins, sleep after 15 mins // install swayidle
# exec-once = swayidle -w timeout 1200 'swaylock; hyprctl dispatch dpms off' resume 'hyprctl dispatch dpms on' timeout 1800 'systemctl suspend' # lock and sleep after 20 mins, suspend after 30 mins // install swayidle
# exec-once = libinput-gestures // install libinput-gestures
# exec-once = swayosd-server # enable swayosd service
# exec-once = `pkexec swayosd-libinput-backend` # swayosd service for keyboard input (requires to be run in a subshell)
# █░█░█ █ █▄░█ █▀▄ █▀█ █░█░█   █▀█ █░█ █░░ █▀▀ █▀
# ▀▄▀▄▀ █ █░▀█ █▄▀ █▄█ ▀▄▀▄▀   █▀▄ █▄█ █▄▄ ██▄ ▄█
windowrulev2 = opacity 0.90 0.90,class:^(neovide)$
windowrulev2 = opacity 0.90 0.90,class:^(firefox)$
windowrulev2 = opacity 0.80 0.80,class:^(betterbird)$
windowrulev2 = opacity 0.80 0.80,class:^(org.qbittorrent.qBittorrent)$
windowrulev2 = opacity 0.80 0.80,class:^(org.gajim.Gajim)$
windowrulev2 = opacity 0.80 0.80,class:^(Chromium)$
windowrulev2 = opacity 0.80 0.80,class:^(FreeTube)$
windowrulev2 = opacity 0.80 0.80,class:^(LibreWolf)$
windowrulev2 = opacity 0.80 0.60,class:^(obsidian)$
windowrulev2 = opacity 0.80 0.80,class:^(VSCodium)$
windowrulev2 = opacity 0.80 0.80,class:^(codium-url-handler)$
windowrulev2 = opacity 0.80 0.80,class:^(code-oss)$
windowrulev2 = opacity 0.80 0.80,class:^(Code)$
windowrulev2 = opacity 0.80 0.80,class:^(kitty)$
windowrulev2 = opacity 0.70 0.70,class:^(tilix)$
windowrulev2 = opacity 0.70 0.70,class:^(rustdesk)$
windowrulev2 = opacity 0.70 0.70,class:^(Ferdium)$
windowrulev2 = opacity 0.70 0.70,class:^(org.keepassxc.KeePassXC)$
# █░█ █▀▄▀█
# ▀▄▀ █░▀░█
# bind = Ctrl+Alt_L, V, submap, passthrough
# submap = passthrough
# bind = Ctrl+Alt_L, V, submap, reset
# submap = reset
-91
View File
@@ -1,91 +0,0 @@
# █░█░█ █ █▄░█ █▀▄ █▀█ █░█░█   █▀█ █░█ █░░ █▀▀ █▀
# ▀▄▀▄▀ █ █░▀█ █▄▀ █▄█ ▀▄▀▄▀   █▀▄ █▄█ █▄▄ ██▄ ▄█
# See https://wiki.hyprland.org/Configuring/Window-Rules/
# hyprctl clients to get names
windowrulev2 = opacity 0.80 0.80,class:^(code-url-handler)$
windowrulev2 = opacity 0.80 0.80,class:^(code-insiders-url-handler)$
windowrulev2 = opacity 0.80 0.80,class:^(org.kde.dolphin)$
windowrulev2 = opacity 0.80 0.80,class:^(org.kde.ark)$
windowrulev2 = opacity 0.80 0.80,class:^(nwg-look)$
windowrulev2 = opacity 0.80 0.80,class:^(qt5ct)$
windowrulev2 = opacity 0.80 0.80,class:^(qt6ct)$
windowrulev2 = opacity 0.80 0.80,class:^(kvantummanager)$
windowrulev2 = opacity 0.80 0.70,class:^(org.pulseaudio.pavucontrol)$
windowrulev2 = opacity 0.80 0.70,class:^(lagrange)$
windowrulev2 = opacity 0.60 0.60,class:^(com.github.wwmm.easyeffects)$
windowrulev2 = opacity 0.80 0.70,class:^(blueman-manager)$
windowrulev2 = opacity 0.80 0.70,class:^(nm-applet)$
windowrulev2 = opacity 0.80 0.70,class:^(nm-connection-editor)$
windowrulev2 = opacity 0.80 0.70,class:^(org.kde.polkit-kde-authentication-agent-1)$
windowrulev2 = opacity 0.80 0.70,class:^(polkit-gnome-authentication-agent-1)$
windowrulev2 = opacity 0.80 0.70,class:^(org.freedesktop.impl.portal.desktop.gtk)$
windowrulev2 = opacity 0.80 0.70,class:^(org.freedesktop.impl.portal.desktop.hyprland)$
windowrulev2 = opacity 0.70 0.70,class:^([Ss]team)$
windowrulev2 = opacity 0.90 0.90,class:^(com.github.rafostar.Clapper)$ # Clapper-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(com.github.tchx84.Flatseal)$ # Flatseal-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(hu.kramo.Cartridges)$ # Cartridges-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(com.obsproject.Studio)$ # Obs-Qt
windowrulev2 = opacity 0.80 0.80,class:^(gnome-boxes)$ # Boxes-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(discord)$ # Discord-Electron
windowrulev2 = opacity 0.80 0.80,class:^(WebCord)$ # WebCord-Electron
windowrulev2 = opacity 0.80 0.80,class:^(ArmCord)$ # ArmCord-Electron
windowrulev2 = opacity 0.80 0.80,class:^(app.drey.Warp)$ # Warp-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(net.davidotek.pupgui2)$ # ProtonUp-Qt
windowrulev2 = opacity 0.80 0.80,class:^(yad)$ # Protontricks-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(Signal)$ # Signal-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(io.github.alainm23.planify)$ # planify-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(io.gitlab.theevilskeleton.Upscaler)$ # Upscaler-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(com.github.unrud.VideoDownloader)$ # VideoDownloader-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(io.gitlab.adhami3310.Impression)$ # Impression-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(io.missioncenter.MissionCenter)$ # MissionCenter-Gtk
windowrulev2 = opacity 0.80 0.80,class:^(io.github.flattool.Warehouse)$ # Warehouse-Gtk
windowrulev2 = float,class:^(org.kde.dolphin)$,title:^(Progress Dialog — Dolphin)$
windowrulev2 = float,class:^(org.kde.dolphin)$,title:^(Copying — Dolphin)$
windowrulev2 = float,title:^(About Mozilla Firefox)$
windowrulev2 = float,class:^(firefox)$,title:^(Picture-in-Picture)$
windowrulev2 = float,class:^(firefox)$,title:^(Library)$
windowrulev2 = float,class:^(kitty)$,title:^(top)$
windowrulev2 = float,class:^(kitty)$,title:^(btop)$
windowrulev2 = float,class:^(kitty)$,title:^(htop)$
windowrulev2 = float,class:^(vlc)$
windowrulev2 = float,class:^(kvantummanager)$
windowrulev2 = float,class:^(qt5ct)$
windowrulev2 = float,class:^(qt6ct)$
windowrulev2 = float,class:^(nwg-look)$
windowrulev2 = float,class:^(org.kde.ark)$
windowrulev2 = float,class:^(org.pulseaudio.pavucontrol)$
windowrulev2 = float,class:^(blueman-manager)$
windowrulev2 = float,class:^(nm-applet)$
windowrulev2 = float,class:^(nm-connection-editor)$
windowrulev2 = float,class:^(org.kde.polkit-kde-authentication-agent-1)$
windowrulev2 = float,class:^(Signal)$ # Signal-Gtk
windowrulev2 = float,class:^(com.github.rafostar.Clapper)$ # Clapper-Gtk
windowrulev2 = float,class:^(app.drey.Warp)$ # Warp-Gtk
windowrulev2 = float,class:^(net.davidotek.pupgui2)$ # ProtonUp-Qt
windowrulev2 = float,class:^(yad)$ # Protontricks-Gtk
windowrulev2 = float,class:^(eog)$ # Imageviewer-Gtk
windowrulev2 = float,class:^(io.github.alainm23.planify)$ # planify-Gtk
windowrulev2 = float,class:^(io.gitlab.theevilskeleton.Upscaler)$ # Upscaler-Gtk
windowrulev2 = float,class:^(com.github.unrud.VideoDownloader)$ # VideoDownloader-Gkk
windowrulev2 = float,class:^(io.gitlab.adhami3310.Impression)$ # Impression-Gtk
windowrulev2 = float,class:^(io.missioncenter.MissionCenter)$ # MissionCenter-Gtk
# █░░ ▄▀█ █▄█ █▀▀ █▀█   █▀█ █░█ █░░ █▀▀ █▀
# █▄▄ █▀█ ░█░ ██▄ █▀▄   █▀▄ █▄█ █▄▄ ██▄ ▄█
layerrule = blur,rofi
layerrule = ignorezero,rofi
layerrule = blur,notifications
layerrule = ignorezero,notifications
layerrule = blur,swaync-notification-window
layerrule = ignorezero,swaync-notification-window
layerrule = blur,swaync-control-center
layerrule = ignorezero,swaync-control-center
layerrule = blur,logout_dialog
+55 -24
View File
@@ -11,10 +11,6 @@ Switch to Last-Used Keyboard Layout=none,Meta+Alt+L,Switch to Last-Used Keyboard
Switch to Next Keyboard Layout=none,Meta+Alt+K,Switch to Next Keyboard Layout
_k_friendly_name=Keyboard Layout Switcher
[kaccess]
Toggle Screen Reader On and Off=Meta+Alt+S,Meta+Alt+S,Toggle Screen Reader On and Off
_k_friendly_name=Accessibility
[kmix]
_k_friendly_name=KMix
decrease_microphone_volume=Microphone Volume Down,Microphone Volume Down,Decrease Microphone Volume
@@ -53,8 +49,8 @@ Increase Opacity=none,none,Increase Opacity of Active Window by 5%
Kill Window=Meta+Ctrl+Esc,Meta+Ctrl+Esc,Kill Window
Move Tablet to Next LogicalOutput=none,none,Move the tablet to the next output
Move Tablet to Next Output=none,none,Move the tablet to the next output
MoveMouseToCenter=Meta+F6,Meta+F6,Move Mouse to Center
MoveMouseToFocus=Meta+F5,Meta+F5,Move Mouse to Focus
MoveMouseToCenter=Meta+F6,Meta+F6,Center Pointer in Screen
MoveMouseToFocus=Meta+F5,Meta+F5,Center Pointer in Focused Window
MoveZoomDown=none,none,Move Zoomed Area Downwards
MoveZoomLeft=none,none,Move Zoomed Area to Left
MoveZoomRight=none,none,Move Zoomed Area to Right
@@ -62,15 +58,15 @@ MoveZoomUp=none,none,Move Zoomed Area Upwards
Overview=none,Meta+W,Toggle Overview
Setup Window Shortcut=none,none,Setup Window Shortcut
Show Desktop=Meta+D,Meta+D,Peek at Desktop
Switch One Desktop Down=none,Meta+Ctrl+Down,Switch One Desktop Down
Switch One Desktop Up=Meta+Ctrl+Shift+Up,Meta+Ctrl+Up,Switch One Desktop Up
Switch One Desktop to the Left=none,Meta+Ctrl+Left,Switch One Desktop to the Left
Switch One Desktop to the Right=none,Meta+Ctrl+Right,Switch One Desktop to the Right
Switch Window Down=Meta+Shift+K\tMeta+Shift+Down\tMeta+Ctrl+Down\tMeta+Ctrl+J\tCtrl+Alt+Down\tCtrl+Alt+J,Meta+Alt+Down,Switch to Window Below
Switch Window Left=Meta+Shift+H\tMeta+Shift+Left\tMeta+Ctrl+H\tCtrl+Alt+Left\tCtrl+Alt+H,Meta+Alt+Left,Switch to Window to the Left
Switch Window Right=Meta+Shift+L\tMeta+Shift+Right\tMeta+Ctrl+L\tCtrl+Alt+Right\tCtrl+Alt+L,Meta+Alt+Right,Switch to Window to the Right
Switch Window Up=Meta+Shift+J\tMeta+Shift+Up\tMeta+Ctrl+Up\tMeta+Ctrl+K\tCtrl+Alt+Up\tCtrl+Alt+K,Meta+Alt+Up,Switch to Window Above
Switch to Desktop 1=Ctrl+F1\tMeta+1\tMeta+Ctrl+Left,Ctrl+F1\tMeta+F1,Switch to Desktop 1
Switch One Desktop Down=Meta+Ctrl+Down,Meta+Ctrl+Down,Switch One Desktop Down
Switch One Desktop Up=Meta+Ctrl+Up\tMeta+Ctrl+Shift+Up,Meta+Ctrl+Up,Switch One Desktop Up
Switch One Desktop to the Left=Meta+Ctrl+Left,Meta+Ctrl+Left,Switch One Desktop to the Left
Switch One Desktop to the Right=Meta+Ctrl+Right,Meta+Ctrl+Right,Switch One Desktop to the Right
Switch Window Down=Ctrl+Alt+J\tCtrl+Alt+Down\tMeta+Shift+K\tMeta+Shift+Down\tMeta+Ctrl+J,Meta+Alt+Down,Switch to Window Below
Switch Window Left=Ctrl+Alt+H\tCtrl+Alt+Left\tMeta+Shift+H\tMeta+Shift+Left\tMeta+Ctrl+H,Meta+Alt+Left,Switch to Window to the Left
Switch Window Right=Ctrl+Alt+L\tCtrl+Alt+Right\tMeta+Shift+L\tMeta+Shift+Right\tMeta+Ctrl+L,Meta+Alt+Right,Switch to Window to the Right
Switch Window Up=Ctrl+Alt+K\tCtrl+Alt+Up\tMeta+Shift+J\tMeta+Shift+Up\tMeta+Ctrl+K,Meta+Alt+Up,Switch to Window Above
Switch to Desktop 1=Ctrl+F1\tMeta+1,Ctrl+F1\tMeta+F1,Switch to Desktop 1
Switch to Desktop 10=Meta+0,none,Switch to Desktop 10
Switch to Desktop 11=none,none,Switch to Desktop 11
Switch to Desktop 12=none,none,Switch to Desktop 12
@@ -81,7 +77,7 @@ Switch to Desktop 16=none,none,Switch to Desktop 16
Switch to Desktop 17=none,none,Switch to Desktop 17
Switch to Desktop 18=none,none,Switch to Desktop 18
Switch to Desktop 19=none,none,Switch to Desktop 19
Switch to Desktop 2=Ctrl+F2\tMeta+2\tMeta+Ctrl+Right,Ctrl+F2\tMeta+F2,Switch to Desktop 2
Switch to Desktop 2=Ctrl+F2\tMeta+2,Ctrl+F2\tMeta+F2,Switch to Desktop 2
Switch to Desktop 20=none,none,Switch to Desktop 20
Switch to Desktop 21=none,none,Switch to Desktop 21
Switch to Desktop 22=none,none,Switch to Desktop 22
@@ -95,9 +91,9 @@ Switch to Desktop 6=Meta+6,none,Switch to Desktop 6
Switch to Desktop 7=Meta+7,none,Switch to Desktop 7
Switch to Desktop 8=Meta+8,none,Switch to Desktop 8
Switch to Desktop 9=Meta+9,none,Switch to Desktop 9
Switch to Next Desktop=Meta+Ctrl+Shift+Right\tMeta+Ctrl+Shift+L,none,Switch to Next Desktop
Switch to Next Desktop=Meta+Ctrl+Shift+L\tMeta+Ctrl+Shift+Right,none,Switch to Next Desktop
Switch to Next Screen=none,none,Switch to Next Screen
Switch to Previous Desktop=Meta+Ctrl+Shift+Left\tMeta+Ctrl+Shift+H,none,Switch to Previous Desktop
Switch to Previous Desktop=Meta+Ctrl+Shift+H\tMeta+Ctrl+Shift+Left,none,Switch to Previous Desktop
Switch to Previous Screen=none,none,Switch to Previous Screen
Switch to Screen 0=none,none,Switch to Screen 0
Switch to Screen 1=none,none,Switch to Screen 1
@@ -112,10 +108,11 @@ Switch to Screen Below=none,none,Switch to Screen Below
Switch to Screen to the Left=none,none,Switch to Screen to the Left
Switch to Screen to the Right=none,none,Switch to Screen to the Right
Toggle Night Color=none,none,Suspend/Resume Night Light
Toggle Show Compositing=none,none,Toggle “Show Compositing” effect
Toggle Window Raise/Lower=none,none,Toggle Window Raise/Lower
ToggleMouseClick=Meta+*,Meta+*,Toggle Mouse Click Animation
Walk Through Windows=Alt+Tab,Alt+Tab\tMeta+Tab,Walk Through Windows
Walk Through Windows (Reverse)=Alt+Shift+Backtab,Alt+Shift+Tab\tMeta+Shift+Tab,Walk Through Windows (Reverse)
Walk Through Windows (Reverse)=Alt+Shift+Tab,Alt+Shift+Tab\tMeta+Shift+Tab,Walk Through Windows (Reverse)
Walk Through Windows Alternative=none,none,Walk Through Windows Alternative
Walk Through Windows Alternative (Reverse)=none,none,Walk Through Windows Alternative (Reverse)
Walk Through Windows of Current Application=Alt+`,Alt+`\tMeta+`,Walk Through Windows of Current Application
@@ -129,7 +126,7 @@ Window Custom Quick Tile Bottom=none,none,Custom Quick Tile Window to the Bottom
Window Custom Quick Tile Left=none,none,Custom Quick Tile Window to the Left
Window Custom Quick Tile Right=none,none,Custom Quick Tile Window to the Right
Window Custom Quick Tile Top=none,none,Custom Quick Tile Window to the Top
Window Fullscreen=Alt+Return\tShift+F11,none,Make Window Fullscreen
Window Fullscreen=Shift+F11\tAlt+Return,none,Make Window Fullscreen
Window Grow Horizontal=none,none,Expand Window Horizontally
Window Grow Vertical=none,none,Expand Window Vertically
Window Lower=none,none,Lower Window
@@ -192,9 +189,9 @@ Window to Desktop 6=Meta+Ctrl+6,none,Window to Desktop 6
Window to Desktop 7=Meta+Ctrl+7,none,Window to Desktop 7
Window to Desktop 8=Meta+Ctrl+8,none,Window to Desktop 8
Window to Desktop 9=Meta+Ctrl+9,none,Window to Desktop 9
Window to Next Desktop=Meta+Ctrl+Alt+Right\tMeta+Ctrl+Alt+L,none,Window to Next Desktop
Window to Next Desktop=Meta+Ctrl+Alt+L\tMeta+Ctrl+Alt+Right,none,Window to Next Desktop
Window to Next Screen=none,Meta+Shift+Right,Move Window to Next Screen
Window to Previous Desktop=Meta+Ctrl+Alt+Left\tMeta+Ctrl+Alt+H,none,Window to Previous Desktop
Window to Previous Desktop=Meta+Ctrl+Alt+H\tMeta+Ctrl+Alt+Left,none,Window to Previous Desktop
Window to Previous Screen=none,Meta+Shift+Left,Move Window to Previous Screen
Window to Screen 0=none,none,Move Window to Screen 0
Window to Screen 1=none,none,Move Window to Screen 1
@@ -210,6 +207,36 @@ view_actual_size=Meta+),Meta+0,Zoom to Actual Size
view_zoom_in=Meta++,Meta++\tMeta+=,Zoom In
view_zoom_out=Meta+_,Meta+-,Zoom Out
[kwin][$InverseAction]
Cycle Overview=Cycle Overview Opposite
Increase Opacity=Decrease Opacity
MoveZoomDown=MoveZoomUp
MoveZoomLeft=MoveZoomRight
Switch One Desktop Down=Switch One Desktop Up
Switch One Desktop to the Left=Switch One Desktop to the Right
Switch Window Right=Switch Window Left
Switch Window Up=Switch Window Down
Switch to Next Screen=Switch to Previous Screen
Switch to Previous Desktop=Switch to Next Desktop
Switch to Screen Below=Switch to Screen Above
Switch to Screen to the Right=Switch to Screen to the Left
Walk Through Windows (Reverse)=Walk Through Windows
Walk Through Windows Alternative=Walk Through Windows Alternative (Reverse)
Walk Through Windows of Current Application (Reverse)=Walk Through Windows of Current Application
Walk Through Windows of Current Application Alternative (Reverse)=Walk Through Windows of Current Application Alternative
Window Grow Horizontal=Window Shrink Horizontal
Window Grow Vertical=Window Shrink Vertical
Window One Desktop Up=Window One Desktop Down
Window One Desktop to the Left=Window One Desktop to the Right
Window One Screen Up=Window One Screen Down
Window One Screen to the Right=Window One Screen to the Left
Window Pack Right=Window Pack Left
Window Pack Up=Window Pack Down
Window Raise=Window Lower
Window to Next Screen=Window to Previous Screen
Window to Previous Desktop=Window to Next Desktop
view_zoom_in=view_zoom_out
[mediacontrol]
_k_friendly_name=Media Controller
mediavolumedown=none,none,Media volume down
@@ -245,6 +272,8 @@ powerProfile=Battery,Battery\tMeta+B,Switch Power Profile
Slideshow Wallpaper Next Image=Meta+Ctrl+Alt+Shift+Right,none,Next Wallpaper Image
_k_friendly_name=plasmashell
activate application launcher=Meta,Meta\tAlt+F1,Activate Application Launcher
activate next task manager entry=Meta+Ctrl+PgDown,Meta+Ctrl+PgDown,Activate Next Task Manager Entry
activate previous task manager entry=Meta+Ctrl+PgUp,Meta+Ctrl+PgUp,Activate Previous Task Manager Entry
activate task manager entry 1=none,Meta+1,Activate Task Manager Entry 1
activate task manager entry 10=none,none,Activate Task Manager Entry 10
activate task manager entry 2=none,Meta+2,Activate Task Manager Entry 2
@@ -261,12 +290,14 @@ activate widget 69=none,none,Activate System Monitor Widget
activate widget 70=none,none,Activate System Monitor Widget
clear history=none,none,Clear Notification History
clear-history=none,none,Clear Clipboard History
clipboard_action=Meta+Ctrl+X,Meta+Ctrl+X,Automatic Action Popup Menu
clipboard_action=none,none,Toggle Automatic Popping Up of Action Menu
cycle-panels=Meta+Alt+P,Meta+Alt+P,Move keyboard focus between panels
cycleNextAction=none,none,Next History Item
cyclePrevAction=none,none,Previous History Item
edit_clipboard=none,none,Edit Contents…
manage activities=none,Meta+Q,Show Activity Switcher
move active task to left=Meta+Ctrl+Shift+PgUp,Meta+Ctrl+Shift+PgUp,Move Active Task Manager Entry Backward
move active task to right=Meta+Ctrl+Shift+PgDown,Meta+Ctrl+Shift+PgDown,Move Active Task Manager Entry Forward
next activity=none,none,Walk through activities
previous activity=Meta+Shift+A,none,Walk through activities (Reverse)
repeat_action=Meta+Ctrl+R,none,Manually Invoke Action on Current Clipboard
@@ -335,7 +366,7 @@ _launch=Meta+K
_launch=Meta+Ctrl+T
[services][systemsettings.desktop]
_launch=\tTools
_launch=Tools
[services][thorium-browser.desktop]
_launch=Meta+Ctrl+B
+32 -9
View File
@@ -70,9 +70,9 @@ starves the desktop and freezes the PC (reboot). Hard-won rules:
| `GLM-4.7-Flash-UD-Q4_K_XL` | **21.5** @moe22 | 24k | reasoning | quality coder (opencode subagents); KV fix + moe22 ✓ verified clean 08-08 |
| `GLM-4.7-Flash-UD-Q3_K_XL` | **26.0** @moe12 (2.0 daytime baseline; ~28 clean) | 24k | reasoning | **fast coder** — re-added 09-21 because Q4's 21.5 dragged in agent loops; 3-bit, evening-safe at moe12 (13.4 used / 3.8 free @2.0); Q4 stays for hard reviews |
| `Qwen3-Coder-Next-UD-IQ3_XXS` | 16.0 | **128k** | 80B-A3B | long-session coder (128k ctx) |
| `Qwen3.8-27B-UD-IQ3_XXS` | **15.5** (10.9 w/o MTP) | 24k | vision, MTP, hybrid reasoner @ **xhigh** | ⚠ **clean-desktop specialty** (kept 09-10 over the ≥22 gate) — dense-27B quality, slow: its Gated-DeltaNet layers are Vulkan-bound (prefill ~35). 2.5 GB free @1.7 baseline @24k (✓ 10-02 on b11146, `-n 512`, acceptance 0.84). The *felt* slowness is the **xhigh thinking preamble** (17.5 s before the first answer token on a trivial prompt; `reasoning_effort=low` measured 6.45 s at the same decode on 10-02 — siblings benched then removed by decision, the kwarg is one line if ever wanted); **never on a busy evening desktop** (~1 GB free = GTT freeze) |
| `Qwen3.8-27B-UD-IQ3_XXS` | **36–37** on b11429 (10-22 ×2; was 15.5 on b11146, 10.9 base) | 24k | vision, MTP, hybrid reasoner @ **xhigh** | ⚠ **clean-desktop specialty** (kept 09-10 over the ≥22 gate — **the speed half is now passed 1.7× over**, since llama.cpp 0.6.0 halved the per-step MTP cost; role re-rank pending the user) — dense-27B quality; prefill still GDN-bound (~20–35). 2.7 GB free @1.3 baseline @24k (✓ 10-22 on b11429, `-n 512`, acceptance 0.89, len 2.78). The *felt* slowness is the **xhigh thinking preamble** (17.5 s before the first answer token on a trivial prompt; `reasoning_effort=low` measured 6.45 s at the same decode on 10-02 — siblings benched then removed by decision, the kwarg is one line if ever wanted); **never on a busy evening desktop** (~1 GB free = GTT freeze) |
| `Qwen3-Embedding-0.6B` | 33 (CPU) | 8k | CPU-only, `/v1/embeddings` | RAG/search embedder (not a chat model) |
| `Ornith-1.5-35B-A3B-IQ3_XXS` | **32.1** @moe16 | 24k | MTP, vision, thinking | **agentic coder** (added 09-21) — Qwen3.5-MoE derivative claiming +15 Terminal-Bench / +5.6 SWE-bench over Qwen3.6-35B; the only coder with MTP; 13.8 used / 3.3 free @2.0 |
| `Ornith-1.5-35B-A3B-IQ3_XXS` | **32.1** @moe16 | **32k** (10-22, unbenched) | MTP, vision, thinking | **agentic coder** (added 09-21) — Qwen3.5-MoE derivative claiming +15 Terminal-Bench / +5.6 SWE-bench over Qwen3.6-35B; the only coder with MTP; 13.8 used / 3.3 free @2.0 |
Expected run-to-run spread: MTP models swing ±15% with draft **acceptance rate** (content-
dependent); CPU-heavy presets (Coder-Next, embedder) dip under daytime CPU contention.
@@ -94,9 +94,9 @@ Treat clean night runs as the reference; don't retune on daytime deltas.
- **Hard reasoning**: `35B-Thinking` (quality) or `gpt-oss-20b` (speed + tool use).
Their multi-second TTFT is the reasoning phase streaming first — not a slow load.
For quick interactive gpt-oss answers use `gpt-oss-20b-low` (reasoning effort low).
- **Best single-model quality, speed no object** (quiet desktop only): `Qwen3.8-27B` — dense
27B hybrid reasoner + vision at ~14.5 t/s; it cannot go faster on this card (GDN layers on
Vulkan), and a busy evening desktop pushes it into GTT-freeze territory — check `rocm-smi` first.
- **Best single-model quality** (quiet desktop only): `Qwen3.8-27B` — dense 27B hybrid reasoner +
vision at **37 t/s since llama.cpp 0.6.0** (was ~15 on 0.5.0; prefill still GDN-bound on Vulkan),
but a busy evening desktop pushes it into GTT-freeze territory — check `rocm-smi` first.
- **Images**: `gemma-4-26B` for quality, `gemma-4-E4B` or `Qwen3.5-9B` for speed,
`Qwen3.6-35B` when you want the daily driver to see the screenshot.
- **Long one-shot documents**: `gemma-4-E4B` — 185 t/s prefill eats 64k in ~6 min
@@ -150,8 +150,13 @@ the router, but chats/presets saved against the old id need re-picking.
2026-09-10: kept as ⚠ clean-desktop specialty after the gate bench finally ran on the v3 gguf
(14.5 t/s MTP / 10.9 base, 2.8 GB free @1.5); server ctx restored to 24576 = client ctx, no client edits.
2026-09-21: `GLM-4.7-Flash-UD-Q3_K_XL` (⚡ fast coder) added to all six client files next to the Q4.
2026-10-22: Qwen3.8-27B quant race opened (ISTA GSQ-RCO IQ3_XXS-mtp + DFlash2 sibling) — bench-only,
NO client edits until a candidate wins; see the watch list. Same night: fl found on llama.cpp 0.6.0
(b11429) and the Qwen3.8 incumbent re-measured 37.1 t/s (15.5 on b11146) — every MTP/hybrid preset
on the roster needs a re-sweep on this build; ledger numbers above it are from 0.5.0 unless dated 10-22.
2026-09-21: `Ornith-1.5-35B-A3B-IQ3_XXS` (agentic coder, thinking + vision) added to all six client files after passing its gate (32.1 t/s @moe16).
2026-09-21: default model → `Qwen3.6-35B-A3B-Thinking` everywhere (opencode global + plan/build/auto, pi defaultModel + thinking level medium, aichat common/lw/wm) — gemma-4-26B refused routine sysadmin work. Thinking preset gets `reasoning-budget 8192`. wm aichat caught up (GLM Q3, Ornith).
2026-10-22: Ornith in pi: `Compacting in OVERFLOW recovery` right after a truncated reply is pi's *truncated-reply* recovery, not a full window — pi clamps each request's `max_tokens` to `contextWindow − estimate − 4096`, which on 24k/4096 starts exactly at the compaction threshold, so the first request after a compaction (estimate gains the system prompt + tool schemas) can leave a thinking reply ~1024 answer tokens → `length` → compact + retry. `compaction-watch.ts` now names the case with numbers; `common/.pi/CLAUDE.md` > Compaction tuning has the arithmetic. Applied the same day, UNBENCHED: Ornith `ctx-size` 24576 → 32768 + the seven client files (pi/opencode/aichat, common+lw+wm; predicted ≈ 0.1 GB of KV on this hybrid arch) — restart llama.service, then `./bench.py -m Ornith-1.5-35B-A3B-IQ3_XXS`; gate and the revert path are in the preset comment.
2026-10-12: Ornith in pi: "Response was truncated before completion." = `finish_reason: length` — the `<think>` block counts against pi's 4096 `max_tokens` and the preset has no reasoning-budget. Fixed client-side: pi sends `thinking_budget_tokens` per thinking level on Ornith (`compat.thinkingTokenBudgetField` + `settings.json > thinkingBudgets`, common + lw). The preset deliberately keeps NO CLI reasoning-budget — it would override the per-request field (see the preset comment).
## Tuning cheat-sheet
@@ -200,9 +205,27 @@ reasoning-effort siblings; for stable MTP decode numbers use `-n 512`.
## Watch list
- **GLM MTP in llama.cpp** — the single biggest pending win (primary coder ~22 → ~30+).
- **DFlash2 for Qwen3.8-27B** — `z-lab/Qwen3.8-27B-DFlash2-GGUF` drafter, upstream 1.81× vs the
MTP head's 1.33× here (b11146 has the support). Blockers: #27408 (spec + mmproj → image requests
500, so only as a text-only sibling preset) and #28158 (Vulkan OOB draft token, open). Trial =
`spec-type draft-dflash`, `spec-draft-n-max 3`, no mmproj; n-max 7 does not fit the VRAM envelope.
- **Qwen3.8-27B quant race (opened 2026-10-22, bench-only, not client-wired)** — two candidate
sections in `config.ini` after the incumbent: `Qwen3.8-27B-GSQ-RCO-IQ3_XXS` (ISTA-DASLab GSQ-RCO
file, 10.44 GB with MTP head = −0.49 GB vs unsloth v3) and its `-dflash` text-only sibling
(`z-lab/Qwen3.8-27B-DFlash2-GGUF` Q4_K_M drafter, `spec-draft-n-max 3`). Expected: +4 % decode and
+0.5 GB free from the file; +5–15 % over MTP from DFlash2 on RDNA3/Vulkan (not the paper's 1.8×).
Quality is unmeasured against our file — the user's own prompts decide. First run 10-22 03:14: both
candidates HTTP 400 = fl's config.ini not pulled yet (router: "model not found"); the incumbent in
the same run read **37.1 t/s on b11429** (see its row). Re-run 03:25, one session: incumbent 36.2 /
2.7 free, **GSQ 37.5 / 3.2 free** (+3.6 %, +0.5 GB — speed+VRAM passed, **quality call open**),
**DFlash2 15.4 / 1.9 free → dropped** (tombstoned in `config.ini`). Ruled out by the same
research: ByteShape IQ4_XS (13.1 GB), ISTA IQ3_S (12.1 GB with MTP, under the floor), the
adaptive KV-cache-streaming fork (CUDA-only, PCIe 5.0). Still open: #27408 (spec + mmproj → 500),
#28158 (Vulkan OOB draft token). Research + verdicts + download lines: the config block.
- **llama.cpp 0.7.0 (b11669) — HOLD fl at 0.6.0**: #30688, Vulkan draft-mtp decode 4× / prefill 6×
slower (target compute buffer lands in GTT = this box's freeze precursor). Every MTP preset on the
roster is exposed. When `pacman -Si llama-cpp` on fl shows 0.7.0, `IgnorePkg` it until fixed.
The 0.5.0 → 0.6.0 jump itself (Qwen3.8 15.5 → 37) is llama-side, best explained by #29856 (hybrid
graph reserve; the old 10.9 base was probably GTT-starved, not kernel-bound — see the preset block).
**ggml 0.26.0** (next `-Syu`): #30039 reports a −45 % prefill regression on a GDN hybrid — bench
Qwen3.8 before and after; downgrade ggml/ggml-vulkan from the pacman cache if prefill tanks.
- **Coder-Next 256k**: if the load log's `n_ctx_train` says 262144, ctx can likely double
again for ~1 GB (KV measured nearly flat).
> **vLLM on this box** (bench rig, docker/ROCm, never resident together with the router): see `../vllm/README.md`.
+3 -1
View File
@@ -102,7 +102,9 @@ def force_unload(base, key):
print(f" (router /models not readable: {e} — relying on sleep-idle drain)", file=sys.stderr)
return
for m in d.get("data", []):
if m.get("status", "loaded") != "loaded": # builds without 'status': try them all
st = m.get("status", "loaded") # b11146 sends {'value': 'loaded'|'sleeping'|'unloaded'}; older a string
st = st.get("value", "loaded") if isinstance(st, dict) else st
if st not in ("loaded", "loading"): # builds without 'status': try them all
continue
try:
_open(root + "/models/unload", key, {"model": m["id"]}, timeout=30).read()
+193 -1
View File
@@ -379,6 +379,36 @@ sleep-idle-seconds = 60
# stall/500) → any trial is a separate TEXT-ONLY preset; this one keeps MTP + vision.
# Open on Vulkan for MTP and DFlash alike: #28158 — intermittent draft token id 248320
# (= n_vocab) → HTTP 500 "Invalid input batch", mostly after very long prompts. If seen: that.
# ✦ 2026-10-22 QUANT RACE OPENED (reddit tip: ByteShape / ISTA 3-bit quants + DFlash2 + KV streaming):
# two bench-only candidate sections follow this preset — [Qwen3.8-27B-GSQ-RCO-IQ3_XXS] (ISTA file,
# −0.49 GB, MTP) and its -dflash text-only sibling; the research + verdicts live in that block.
# ⚠ llama.cpp 0.7.0 (b11669) = #30688, Vulkan draft decode 4× slower (compute buffer lands in GTT):
# hold fl at 0.6.0 until fixed — hits EVERY MTP preset here, not only this one.
# ✦ BUILD DATUM 2026-10-22 03:15 — fl turned out to be on 0.6.0 (b11429) + ggml-vulkan 0.25.3-2 (the
# SAME kernels as 10-02): bench 37.1 t/s / 21 pp / TTFT 3.3 / 14.4 used / 2.7 free @1.3 (-n 512).
# Server: 221 tok @ 37.07, draft acceptance 0.892 (141/158), mean len 2.78 → ~75 ms per
# speculative step vs ~173 on b11146 — a llama.cpp-side change (b11147..b11429), NOT the Vulkan
# kernels (ggml is a separate package, unchanged). A 4096-token think-run: 27.3 t/s @ acceptance
# 0.51 / len 2.02 = the same ~74 ms step; acceptance sags over long thinking. Footprint 13.1,
# unchanged. → the ≥22 gate is PASSED 1.7× over; the 'clean-desktop specialty' verdict's SPEED half
# is obsolete (the VRAM half — never on a busy desktop — still stands). Role re-rank = user's call.
# 03:25 re-run: 36.2 / 45 pp / TTFT 8.6 / 14.5 / 2.7 — reproducible.
# CAUSE (researched 10-22; a hypothesis — no PR in b11147..b11429 claims any such gain): Arch's llama-cpp
# links the SYSTEM ggml (-DLLAMA_USE_SYSTEM_GGML=ON), so every ggml-side Vulkan PR (#29476 GDN tune,
# #29280 descriptor reuse, FA work) is EXCLUDED — those ship in ggml 0.26.0, not on fl yet. Best fit:
# #29856 (10-03) "gather the recurrent states once so the reserve covers every split" — on 0.5.0 the
# hybrid graph was silently re-reserved after the first speculative step and the target compute
# buffer could land partly in GTT = the #30688 mechanism seen from the other side. The arithmetic
# agrees: the old no-MTP base (10.9 t/s = 92 ms/token) was SLOWER than today's whole MTP step (75 ms
# for ≥3 verified tokens + 2 draft passes), so the "10.9 base = the GDN kernel wall" story above is
# SUSPECT: that base was most likely GTT-starved, not kernel-bound. Cheap check whenever the card is
# free: a TEMP -nomtp alias on 0.6.0 — ~22–26 t/s confirms it, ~11 refutes it. #27694 (probabilistic
# drafter, 10-02) explains acceptance 0.84 → 0.89, not the step time. (Also learned: the 9B is hybrid
# too, 24 GDN + 8 attn per #30039 — the "regular-attention qwen35" remark further down is wrong.)
# ⚠ ggml 0.26.0 (the next -Syu brings ggml + ggml-vulkan 0.26.0-1, SOVERSION-compatible, unrebuilt):
# #30039 reports −45 % PREFILL on a GDN hybrid with b11429 binaries, TG flat → bench this preset right
# before and after that upgrade; if prefill tanks, downgrade ggml/ggml-vulkan from the pacman cache.
# REFERENCE from here: 37.1 (b11429). Every other MTP/hybrid preset needs a re-sweep on this build.
# ⚠ Needs a CURRENT llama.cpp build: first "qwen35" file HERE with ssm.*/hybrid-attn keys
# (the 9B is regular-attention qwen35). If the load fails with unknown-architecture /
# missing-tensor errors, update llama.cpp on fl before touching this preset.
@@ -418,6 +448,150 @@ min-p = 0
jinja = on
sleep-idle-seconds = 120
# ═══ QUANT RACE 2026-10-22 — Qwen3.8-27B third-party 3-bit quants + DFlash2 (bench-only, NOT client-wired) ═══
# Trigger: r/LocalLLaMA 2026-09-24 "Qwen-3.8-27B is good enough that I stopped using API": a 16 GB user
# (RTX 5060 Ti, CUDA, PCIe 5.0 x8, display on the iGPU) runs ByteShape IQ4_XS + DFlash2 + Raymond's
# KV-cache streaming; a reply says "try the ISTA IQ3_S / XXS — more speed and context, couldn't tell a
# difference in quality". What survives contact with THIS box (16 GB + display, Vulkan/RADV, PCIe 4.0 x8):
# · KV-cache streaming (troed → RaymondHuang210129/llama.cpp-adaptive-kv-streaming): OUT. CUDA-only —
# the README lists Vulkan as "memory-infrastructure tests only" (no attention path), ROCm = one
# unreviewed PR for gfx1201; both authors say PCIe 5.0 is required (PCIe 4.0 users: 2–4 t/s @192k);
# no upstream PR (discussion #28216 only). And it is a 160k+ tool — clients run this model at 24k.
# · ByteShape (byteshape/Qwen3.8-27B-GGUF, proprietary "ShapeLearn" per-tensor mixed types, the IQ*
# labels are size classes; MTP head kept, quantized IQ3/IQ4 vs Q6_K/Q8_0 elsewhere): IQ4_XS-3.84bpw
# 13.08 GB → OUT (ceiling here ≈ 11 GB file); the commenter's own file is troed's 12.25 GB
# ASCII-vocab-pruned build (248320 → 129006 tokens) → OUT twice. Same size class as the incumbent:
# IQ3_S-3.23bpw 11.035 GB (vendor blog: 98.7 % of BF16 task avg vs 93.6 % for unsloth UD-IQ3_XXS —
# vendor numbers; the one independent KLD run ranks their IQ4_XS BELOW unsloth's UD-IQ4_XS, 0.050 vs
# 0.019), IQ3_XS-3.01bpw 10.353 GB, IQ3_XXS-2.88bpw 9.919 GB. Optional third runner, see below.
# · ISTA (ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF: GSQ = learned per-coordinate grids, RCO = per-tensor
# type under a size budget, standard GGUF block types, imatrix ~90 % English — discussion #7 reports
# degraded Chinese; "-mtp" files carry the head, +0.35 GB, weights identical):
# IQ3_S-mtp 12.12 GB → footprint ≈ 14.2 → ~1.4 free @1.6 = UNDER THE 1.5 FLOOR. Plain IQ3_S 11.77
# fits only WITHOUT any drafter (~3.4 free) at ~10 t/s → no. Both reddit comments measured "more
# speed and context" against a 13 GB file, not against a 3-bit one.
# IQ3_XXS-mtp 10.443 GB = 0.49 GB under the incumbent → the one ISTA file with a case here.
# Quality evidence (nobody measured it against OUR unsloth v3 file): ISTA card wiki-ppl 7.20 vs
# BF16 7.05, AIME25/GPQA-D/LCBv6 within 1–2 pts of BF16; ByteShape's blog scores it 94.4 vs 93.6
# for UD-IQ3_XXS (= equal); Agention's KLD table puts it at 0.11, ~2× their own IQ3_XXS. Thread:
# one user "ISTA even at lower quants did better", troed "tried it, not as good for my use".
# → bench.py settles speed + VRAM only; the quality half is the user's own prompts, both files.
# · DFlash2 (z-lab/Qwen3.8-27B-DFlash2-GGUF: 1.92B block-diffusion drafter, 5 layers, 2048-token
# sliding window, no own embeddings/output head; Q4_K_M 1.143 GB; llama.cpp support since b10658 =
# already in fl's b11146): acceptance length ~5.4 vs the MTP head's 2.6 here. RDNA3/Vulkan reports
# (PR #27342 thread): 7900 XTX RADV n-max 3 = +4–12 % decode over MTP, prefill −1 to −12 %; Strix
# Halo RADV n-max 4 = 19–23 t/s vs MTP 20; R9700 n-max 4 = 52 vs 47; a net LOSS once layers spill.
# Expect +5–15 % over the incumbent (37.1 on b11429, 10-22), NOT the paper's 1.8× — every verified draft token
# re-runs the 48 GDN layers. Acceptance falls with context (5090 datum: 31–36 % at 23–63k vs MTP
# 55 %) = why the redditor says "at low context".
# VRAM: drafter 1.14 GB + (1 + n-max) × ~150 MiB recurrent-state snapshots (n-max 3 ≈ 0.6 GB) +
# drafter KV — the MTP head measured 1.5 GB against a ~0.7 estimate, so budget ~2 GB and let the
# bench.py guard decide; the ISTA file's −0.49 GB is what buys the margin. Default n-max 7 = OUT.
# Text-only: #27408 (spec + mmproj → image requests 500) still open → NO mmproj on that preset.
# #28158 (Vulkan: draft token id 248320 → HTTP 500, rare, very long prompts) still open.
# Build window: #29226 (b11132) broke drafters WITH embeddings but WITHOUT an output head (fixed by
# #30111, b11512); the z-lab drafter has neither → should not be hit. FIRST GATE regardless:
# "draft acceptance" in the server log > 0 (0.00 = that class of breakage → drop the sibling).
# ⚠ BUILD: fl = llama-cpp 0.6.0-1 (b11429) + ggml 0.25.3-2 + ggml-vulkan 0.25.3-2, CONFIRMED 10-22 (the
# b11146 → b11429 move alone took the incumbent 15.5 → 37.1, see its block). Arch extra also has
# ggml/ggml-vulkan 0.26.0 (#27952 int8 coopmat prefill, #29476 GDN tune) — the next -Syu brings them
# = another variable, bench before/after. Upstream 0.7.0 (b11669) = #30688: Vulkan
# draft-mtp decode 4× / prefill 6× SLOWER, the target compute buffer re-allocated into GTT — on this
# box that is the freeze precursor, and it hits every MTP preset on the roster. When
# `pacman -Si llama-cpp` on fl shows 0.7.0: HOLD (IgnorePkg = llama-cpp ggml ggml-vulkan) until fixed.
# Race on the current build first (one variable per run); the ggml 0.26.0 bump is a separate round.
# PLAN (clean baseline, -n 512, incumbent re-run in the SAME session — the 10-02 band was 12.5–15.5):
# 1. download (below) · 2. `sudo systemctl restart llama.service` ·
# 3. ./bench.py -n 512 -m Qwen3.8-27B-UD-IQ3_XXS,Qwen3.8-27B-GSQ-RCO-IQ3_XXS,Qwen3.8-27B-GSQ-RCO-IQ3_XXS-dflash
# 4. quality: the same 3–4 real prompts (incl. non-English) against incumbent vs GSQ.
# GATE: GSQ-mtp ≥ incumbent decode (expected ~+4 %) with free ≥ 2.5 → quality is the user's call →
# if it wins, either point the incumbent section's `model =` at the GSQ file (keeps the client id,
# accepted misnomer) or rename = the seven-client-file round. DFlash sibling: acceptance > 0 AND
# decode ≥ 1.10 × the SAME-SESSION incumbent AND free ≥ 2.5 → keep as the text-only fast sibling
# (wire into clients then); else drop. Loser files: delete from ~/software/models (10.4 + 1.1 GB).
# ✦ RACE RUN 2026-10-22 03:25 @1.3 baseline, -n 512, all three in ONE session (rows in bench-history.md):
# incumbent UD-IQ3_XXS ....... 36.2 t/s / 45 pp / TTFT 8.6 / 14.5 used / 2.7 free
# GSQ-RCO-IQ3_XXS (MTP) ...... 37.5 / 44 / 7.0 / 14.0 / 3.2 → +3.6 % decode, +0.5 GB free = exactly the
# file-size prediction. Speed + VRAM half of the gate PASSED. OPEN: the quality half (same 3–4 real
# prompts, one non-English, against both; nobody has measured ISTA vs unsloth v3 at this size).
# GSQ -dflash ................ 15.4 / 32 / 18.0 / 15.3 / 1.9 → −59 % vs its own MTP row and under the
# 2.5 target → DROPPED (tombstone below). Read the acceptance line once: `journalctl -u llama.service
# --since 03:25 | grep 'draft acceptance'` — 0.00 = the #29226-window bug (fix #30111 = b11512 sits in
# 0.7.0 behind #30688 → re-probe only on a fixed 0.7.x); > 0 = block-drafting re-runs the 48 GDN
# layers per verified block and loses on this card (the RX 9070 XT datum in #25117 said the same)
# → closed for good. Either way nothing to tune: n-max 2 only shrinks the loss.
# NEXT: quality call → if GSQ wins, point the incumbent's `model =` at the GSQ file (keeps the client
# id; the section name becomes a misnomer) or do the seven-client rename round, then delete the
# unsloth file; if not, delete the GSQ file and the section. The DFlash2 drafter can go now.
# ⬇ DOWNLOAD on fl — curl straight into the models dir (the HF cache has root-owned dirs from docker):
# cd ~/software/models
# curl -L -o Qwen3.8-27B-GSQ-RCO-IQ3_XXS-mtp.gguf https://huggingface.co/ISTA-DASLab/Qwen3.8-27B-GSQ-RCO-GGUF/resolve/main/Qwen3.8-27B-GSQ-RCO-IQ3_XXS-mtp.gguf
# (dropped 10-22, only for a re-probe) curl -L -o Qwen3.8-27B-DFlash2-Q4_K_M.gguf https://huggingface.co/z-lab/Qwen3.8-27B-DFlash2-GGUF/resolve/main/Qwen3.8-27B-DFlash2-Q4_K_M.gguf
# stat -c '%s %n' Qwen3.8-27B-GSQ-RCO-IQ3_XXS-mtp.gguf Qwen3.8-27B-DFlash2-Q4_K_M.gguf # 10442827840 · 1143006816
# (mmproj: ISTA ships the same BF16 encoder — the existing mmproj-Qwen3.8-27B-F16.gguf is reused)
[Qwen3.8-27B-GSQ-RCO-IQ3_XXS]
# ⚗ RACE CANDIDATE 2026-10-22 — bench-only, NOT in any client file. = the incumbent preset with the ISTA
# file swapped in (MTP head Q6_K, same as unsloth's). Predicted @1.6 baseline @24k: ~14.1 used /
# ~3.0 free (incumbent 14.4 / 2.7), decode ~38–39 (incumbent 37.1 on b11429) — bandwidth × file size, nothing
# else moves. Knobs deliberately identical to the incumbent (n-max 2) for a clean A/B.
# ✓ MEASURED 2026-10-22 03:25 @1.3: 37.5 t/s / 44 pp / TTFT 7.0 / 14.0 used / 3.2 free (incumbent 36.2 /
# 2.7 in the same run). Speed + VRAM passed; QUALITY CALL PENDING — not client-wired until then.
model = /home/anon/software/models/Qwen3.8-27B-GSQ-RCO-IQ3_XXS-mtp.gguf
mmproj = /home/anon/software/models/mmproj-Qwen3.8-27B-F16.gguf
spec-type = draft-mtp
spec-draft-n-max = 2
ctx-size = 24576
n-gpu-layers = 99
threads = 6
flash-attn = on
cache-type-k = q8_0
cache-type-v = q4_0
batch-size = 2048
ubatch-size = 256
cache-reuse = 256
defrag-thold = 0.1
no-mmproj-offload = true
temp = 1.0
top-p = 0.95
top-k = 20
min-p = 0
jinja = on
sleep-idle-seconds = 120
# ─── DROPPED 2026-10-22 03:25: [Qwen3.8-27B-GSQ-RCO-IQ3_XXS-dflash] (DFlash2 text-only sibling) ─────
# 15.4 t/s / 32 pp / 15.3 used / 1.9 free vs 37.5 / 3.2 for the same file with its MTP head — the
# z-lab block drafter is a net loss on this card (see the RACE RUN entry above for the two possible
# causes and the only re-probe condition: a 0.7.x build that carries #30111 without #30688).
# Removed as a loadable section on purpose: OWUI lists every router id, and 1.9 free on a busy
# desktop is the GTT freeze. File can be deleted from the models dir: Qwen3.8-27B-DFlash2-Q4_K_M.gguf
# Keys as benched (n-max 3, no mmproj — #27408):
# model = /home/anon/software/models/Qwen3.8-27B-GSQ-RCO-IQ3_XXS-mtp.gguf
# model-draft = /home/anon/software/models/Qwen3.8-27B-DFlash2-Q4_K_M.gguf
# spec-type = draft-dflash
# spec-draft-n-max = 3
# ctx-size = 24576
# n-gpu-layers = 99
# threads = 6
# flash-attn = on
# cache-type-k = q8_0
# cache-type-v = q4_0
# batch-size = 2048
# ubatch-size = 256
# cache-reuse = 256
# defrag-thold = 0.1
# temp = 1.0
# top-p = 0.95
# top-k = 20
# min-p = 0
# jinja = on
# sleep-idle-seconds = 120
# ─── optional 3rd runner (not added — copy the GSQ section if wanted): ByteShape IQ3_S-3.23bpw ────
# 11.035 GB = the incumbent's size, vendor-claimed best quality in the class, MTP head kept (IQ3-quantized).
# model = /home/anon/software/models/Qwen3.8-27B-IQ3_S-3.23bpw.gguf
# curl -L -o Qwen3.8-27B-IQ3_S-3.23bpw.gguf https://huggingface.co/byteshape/Qwen3.8-27B-GGUF/resolve/main/Qwen3.8-27B-IQ3_S-3.23bpw.gguf # 11035354496 B
# ─── RETIRED 2026-07-16: [Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ3_M] ──────────────
# Dense: 10 t/s, 2.0 GB free (under the 2.5 target) and unfixable — bandwidth-bound, smaller
# quant would cost the quality that justified it. Removed from opencode/pi/aichat.
@@ -552,6 +726,22 @@ sleep-idle-seconds = 30
# here "like the Thinking preset" silently disables the pi knob and brings back pi's
# "Response was truncated before completion." (= finish_reason length: think block + answer
# > the client's 4096 max_tokens). An instruct alias (budget 0) in a SEPARATE section is fine.
# ⚠ 2026-10-22: pi's "Compacting in OVERFLOW recovery" on this preset is NOT this window filling
# up — pi clamps each request's max_tokens to contextWindow − estimate − 4096, which on
# 24k/4096 starts exactly at its compaction threshold; the first request after a compaction
# (estimate gains the system prompt + tool schemas) can leave the thinking reply ~1024 answer
# tokens → finish_reason length → pi compacts again and retries. Arithmetic + options in
# common/.pi/CLAUDE.md > Compaction tuning > max_tokens clamp.
# APPLIED 2026-10-22 (same day, unbenched): ctx-size 24576 → 32768 here + 32768 in the seven
# client files (pi contextWindow both blocks common+lw, opencode limit.context common+lw,
# aichat max_input_tokens 30000 common+lw+wm). Threshold and clamp in pi now sit at 24576,
# above anything a fresh post-compaction context reaches. Predicted cost ≈ 0.1 GB: hybrid
# GDN 3:1 = 10 attention layers with small GQA, ~8 KB/token of q8/q4 KV → ~70 MB for +8192,
# plus a few MB of FA scratch — read the "KV self size" line in the load log to confirm.
# GATE on the first load: `sudo systemctl restart llama.service`, then
# `./bench.py -m Ornith-1.5-35B-A3B-IQ3_XXS` — free ≥ 2.5 at the 2.0 baseline and decode
# ≈ 32 t/s → keep; else back to 24576 in all eight files (the router reads presets once at
# startup, and pi at 32k against a 24k server produces REAL overflow recoveries).
# ⬇ DOWNLOAD from bartowski/Ornith-1.5-35B-A3B-GGUF (imatrix; the official repo has only Q4_K_M+
# ≥ 21.7 GB, unsloth has no 1.5 quants yet):
# Ornith-1.5-35B-A3B-IQ3_XXS.gguf 15,340,447,392 B · sha256 8918ccb9ee29abe3875efec0c3e86f0e35ef0b8a03e3f0e1c422869859a518d0
@@ -560,7 +750,9 @@ model = /home/anon/software/models/Ornith-1.5-35B-A3B-IQ3_XXS.gguf
mmproj = /home/anon/software/models/mmproj-Ornith-1.5-35B-A3B-f16.gguf
spec-type = draft-mtp
spec-draft-n-max = 2
ctx-size = 24576 # = the daily driver; n_ctx_train 262144 — raise only after a bench
ctx-size = 32768 # 24576 → 32768 on 2026-10-22 for pi's max_tokens clamp (⚠ note above);
# UNBENCHED until the next restart — gate free ≥ 2.5 @2.0, decode unchanged;
# n_ctx_train 262144, KV is cheap here (hybrid GDN, ~8 KB/token)
n-gpu-layers = 99
n-cpu-moe = 16 # ✓ KEEP (09-21: 32.1 t/s, 13.8 used / 3.3 free @2.0; moe20 was 27.9 /
# 12.4 / 4.8). = daily-driver envelope; moe14 not worth the margin.
+206
View File
@@ -0,0 +1,206 @@
# vLLM on fl (docker, ROCm) — side by side with the llama.cpp router, never at the same time
Added 2026-10-21 to answer one question: **how does vLLM compare with llama.cpp on this card, on the
same models?** It is a bench rig, not a second production endpoint. Nothing here was run on fl yet —
fl has no sshd, so every command below is for you to run there; the first run will find the rough edges.
- **Box**: the same fl as the router — RX 7600 XT 16 GB (gfx1102, RDNA3), 5600X, 48 GB RAM. The card
drives the display. llama.cpp runs the Arch Vulkan build; vLLM needs ROCm, which comes **inside the
docker image** — nothing ROCm is installed on the host.
- **Files**: `vllm.ini` (presets, same `key = --flag` convention as `../llamacpp/config.ini`),
`vllmctl.py` (start/stop with the VRAM guards), `bench-vs.py` (one table for both engines).
- **Port 11348**. Not in the router, not in any client list (opencode/pi/aichat/OWUI) on purpose.
## ⚠ The one rule: one engine resident at a time
vLLM pre-allocates `gpu-memory-utilization` × 16 GiB at startup and keeps it, idle or not. The router
loads models lazily on request. So while vLLM is up, **any client request to :11343** makes the router
load a model on top of it → overcommit → GTT spill → frozen PC (the same failure `--models-max 1`
guards against). Unloading resident models is not enough; the clients on the LAN are the problem.
`vllmctl.py up` therefore refuses while `llama.service` is active. The sequence is always:
```fish
~/.config/vllm/vllmctl.py up <preset> --stop-router # sudo systemctl stop llama.service, then start vLLM
# … use / bench vLLM …
~/.config/vllm/vllmctl.py down # stop container, wait for VRAM to drain
sudo systemctl start llama.service # router back for the LAN
```
The other order is the only one that should fail safely: vLLM compares its requested budget against
free VRAM at startup and aborts when a router model is resident ("free memory … is less than desired
GPU memory utilization" — reported by users, not something verified on this card). Everything else freezes.
## What "same models" means here, and the size surprise
llama.cpp runs GGUF; vLLM runs Hugging Face safetensors (bf16 / AWQ / GPTQ / compressed-tensors).
The GGUF files cannot be loaded natively, so the pairs below are **the same model at a vLLM-native
quant**, not the same files (same-file GGUF loading is phase 2, see the end).
Sizes from the HF API, 2026-10-21 — the "4-bit" vLLM checkpoints are **10–11.5 GB** because embeddings,
lm_head and the vision tower stay bf16. They are bigger than the Q6/Q8 GGUFs they pair with, so on
16 GB they are **clean-desktop only**, at `gpu-memory-utilization` ≈ 0.78 and 16k context:
| pair | llama.cpp preset (ledger) | vLLM preset (`vllm.ini`) | vLLM weights | fits? |
|---|---|---|---|---|
| smoke | — | `Qwen3-0.6B-smoke` = `Qwen/Qwen3-0.6B` | 1.5 GB | yes — proves docker+GPU+image |
| gemma-4-E4B | `gemma-4-E4B-it-UD-Q8_K_XL` — 57 t/s **with MTP**, 96k ctx, ~7 GB | ~~`google/gemma-4-E4B-it-qat-w4a16-ct`~~ | 11.5 GB | **✗ no** (2026-10-21): vLLM needs ~16 GiB before any KV — the bf16 per-layer embedding table (262144 × 42 × 256 × 2 B = 5.6 GB) is held twice. Tombstone in the ini. |
| Qwen3.5-9B | `Qwen3.5-9B-UD-Q6_K_XL` — 32 t/s **with MTP**, 32k ctx, 9 GB | `Qwen3.5-9B-w4a16` = `RedHatAI/Qwen3.5-9B-quantized.w4a16` | 11.0 GB | the last viable pair: 0.75 on a 2.1 GB desktop, 0.78 clean; prefill batch 512 |
**Cannot be compared on this card at all**: every 30B+ MoE (Coder-30B, GLM-4.7-Flash, Qwen3.6-35B,
Ornith, gemma-4-26B), Coder-Next 80B and Qwen3.8-27B — they only fit llama.cpp through `n-cpu-moe`
expert offload into RAM, which vLLM does not have (its MoE CPU-offload RFC died stale; the only
option is whole-weight `cpu_offload_gb`, which is slow). gpt-oss-20b (MXFP4, ~13 GB) is marginal and
its RDNA3 kernel status is unknown — not in the ini.
**Why the "docker should just work" expectation does not hold here**: of the three first-day failures,
one was the image (no gfx1102 kernels → the override, solved) and two were memory. vLLM keeps
everything in VRAM — bf16 embeddings, fp32 logits during profiling, CUDA-graph memory — on a card that
also drives the desktop. No image changes a model's memory need; the ROCm images on Docker Hub are the
right ones.
So the comparison is **not apples to apples** and the table says so: llama.cpp rows run Q8/Q6 with a
speculative MTP draft at a bigger context; vLLM rows run int4 without speculation at 16k. Read it as
"what each engine delivers on this card for this model", not as a kernel shoot-out.
## Setup on fl (once)
```fish
sudo pacman -S docker # ~20 GB free disk for the ROCm image + 25 GB for two models
sudo systemctl enable --now docker
sudo usermod -aG docker anon # then log out / in (or `newgrp docker`)
cd ~/.dots && ./install.sh # links ~/.config/vllm/* (new files are not live until re-stow)
~/.config/vllm/vllmctl.py pull # vllm/vllm-openai-rocm:v0.32.0 (ROCm 10.0; 11.6 GB download, ~25 GB on disk)
~/.config/vllm/vllmctl.py gpu-check # torch sees the card? prints device name + gfx arch
```
`gpu-check` must print `cuda available: True`, a `gfx1100` device (that is the override at work) and
`torch kernel: ok`. **The override is required** (found 2026-10-21): `v0.32.0` carries no gfx1102 code
objects, and a kernel launch without one aborts the process *silently* — the engine core vanishes, the
API server only says `Failed core proc(s): {}`, and even the `print`s before the abort are lost to
stdout buffering. `[global] env.HSA_OVERRIDE_GFX_VERSION = 11.0.0` presents the card as gfx1100 (RX 7900),
whose RDNA3 code runs on this chip. Nothing else on the host changes.
Then the smoke test (tiny model, 30 % of the card, proves the whole path before any big download —
✓ passed 2026-10-21: 154.9 t/s decode, 1462 t/s prefill, TTFT 0.02 s, 7.7 GB used incl. a 2.9 GB desktop):
```fish
~/.config/vllm/vllmctl.py up Qwen3-0.6B-smoke --stop-router
curl -s localhost:11348/v1/chat/completions -H 'content-type: application/json' \
-d '{"model":"Qwen3-0.6B-smoke","messages":[{"role":"user","content":"hi"}],"max_tokens":20}'
~/.config/vllm/vllmctl.py down; sudo systemctl start llama.service
```
First start of any model compiles Triton kernels: minutes, with `vllmctl.py up` echoing the log tail
every 30 s.
**Tags and updates.** `<tag>` is the part after the colon: `latest` = newest stable release (was
`v0.32.0` on 2026-10-18, ROCm 10.0), `nightly` = today's main branch (18 GB), `latest-rocm72` /
`nightly-rocm72` = the same on the older ROCm 7.2 stack (smaller, 12 GB), `…-base` = without vLLM
itself. The ini pins `v0.32.0` so a re-pull cannot silently change the engine under a bench run. To
update: pick the new `vX.Y.Z` on hub.docker.com/r/vllm/vllm-openai-rocm/tags, put it in `[global] image`,
`vllmctl.py pull`, then `vllmctl.py down` / `up` — the container is recreated from the image on every
`up`. Afterwards `docker image prune -f` drops the superseded layers (`docker images` shows what is kept).
`HF_TOKEN` is only needed for gated repos; none of the three presets is gated. If one ever is,
`hf auth login` on fl writes `~/.cache/huggingface/token`, which the mounted cache hands to the container.
## Running the benchmark
Two phases, because the engines cannot be resident together. Do both on a **clean desktop** (close the
browser; `vramx` must show ≤ ~1.5 GB used) — the vLLM presets leave ~2 GB free at 0.78 and an evening
desktop does not fit.
```fish
# phase 1 — vLLM (router stopped by --stop-router; first run also downloads the model)
~/.config/vllm/vllmctl.py up gemma-4-E4B-it-w4a16 --stop-router
~/.config/vllm/bench-vs.py -t vllm:gemma-4-E4B-it-w4a16 --ctx 8000 --concurrency 4
~/.config/vllm/vllmctl.py down
~/.config/vllm/vllmctl.py up Qwen3.5-9B-w4a16 # router is still stopped
~/.config/vllm/bench-vs.py -t vllm:Qwen3.5-9B-w4a16 --ctx 8000 --concurrency 4
~/.config/vllm/vllmctl.py down
# phase 2 — llama.cpp (vLLM down, router back)
sudo systemctl start llama.service
~/.config/vllm/bench-vs.py -t llamacpp:gemma-4-E4B-it-UD-Q8_K_XL -t llamacpp:Qwen3.5-9B-UD-Q6_K_XL --ctx 8000 --concurrency 4
```
Every row lands in `bench-vs-history.md` (append) and the latest run in `bench-vs-results.md`, same
folder. The guards: a `llamacpp` target is skipped while vLLM answers `/health`; a `vllm` target is
skipped while the router has a model loaded or `llama.service` is active (`--force` overrides both).
**How to read it**
- `decode t/s` is usage tokens over wall time after the first token, measured the same way on both
engines; llama.cpp's own `timings` number is in parentheses so rows match `../llamacpp/bench-results.md`.
- Thinking is **off** on both (`chat_template_kwargs.enable_thinking=false`) and a reasoning delta counts
as first token, so TTFT compares the engines, not the preamble lengths. `--think` reverts that.
- `spec` shows the MTP draft on the llama.cpp side; vLLM rows have none unless the preset sets
`speculative-config`. Expect the gemma/Qwen llama.cpp rows to carry a 1.5–2× draft bonus.
- `agg t/s @N` is continuous batching's home turf — and llama-server still won it (85 vs 61 at 4
streams): its default slot count on b11146 is evidently > 1, so no `parallel` key is needed.
- `--ctx 8000` adds decode at an 8k prompt — the number that matters for agent loops.
## Results
**2026-10-21, Qwen3.5-9B, same evening, same script, thinking off, 256 tok, 0.6 GB desktop:**
| engine · checkpoint | spec | decode t/s | prefill t/s | TTFT s | decode @8k prompt | 4 streams agg t/s | VRAM used |
|---|---|---|---|---|---|---|---|
| vLLM · RedHatAI w4a16 @0.80, 16k, batch 512 | — | 29.4 | **181** | **0.17** | 16.0 | 60.6 | 12.5 GB |
| llama.cpp · UD-Q6_K_XL, 32k | MTP draft | **61.3** | 114 | 0.27 | **51.1** | **85.4** | 10.9 GB |
What it says, for this card:
- **Decode**: llama.cpp wins 2× single-stream and 3× at an 8k prompt. Most of the 2× is the MTP draft
(the ledger's pre-MTP Q8 number was 17 t/s in July; a no-draft Q6 today is probably ~30, i.e. parity
with vLLM). The 8k gap is not MTP: vLLM's decode halves with context here (16.0 vs 29.4) while
llama.cpp loses 17 % — on RDNA3 vLLM's attention path is the Triton generic one, llama.cpp's is a
tuned Vulkan kernel.
- **Prefill**: vLLM +59 % even with its batch capped at 512 for memory. This is its real strength on
the box, and it shows in TTFT.
- **Concurrency**: the column that was supposed to be vLLM's — it batched 4 streams to 2× its single
(60.6), but llama-server served the same 4 at 85.4. Its default slot count is evidently > 1 on this
build, so the README's "llama.cpp serializes" caveat does not hold; it does not need a `parallel` key.
- **Memory**: vLLM holds 1.6 GB more for a model with 2 GB less context, and only runs on a ≤ 0.6 GB
desktop. 0.75 was 0.11 GiB short of KV room; 0.80 is the setting.
**Verdict**: no vLLM model earns a roster slot on fl; the bench rig stays, the router stays, llama-swap
is not needed. The one follow-up that could move the decode column is MTP in vLLM (the knob is in
the ini) — but it cannot touch the memory column or the MoE roster, so it is curiosity, not a decision.
## When it does not start
- **`No available memory for the cache blocks`** (seen 2026-10-21, gemma @0.73/8k on a 2.1 GB desktop):
the model loaded, but budget − weights − activation reservations left nothing for KV. What vLLM
reserved is in the log: `docker logs vllm 2>&1 | grep -iE 'profil|kv cache|weights take|peak'`.
Cheapest fixes first: text-only (`limit-mm-per-prompt` all zero — skips the image/audio encoder
warmup, now in both presets), smaller `max-num-batched-tokens` (2048, now in both presets), lower
`max-num-seqs`; then a cleaner desktop so the projection allows 0.78–0.82; last resort the smaller
GPTQ checkpoint. `--set key=val` tries any of these without editing the ini.
- **`ABORT: projected free …`**: the desktop is using more than the preset assumes. Close GPU apps or
take the suggested utilization it prints. `--force` exists; the freeze is real.
- **compressed-tensors / "no kernel" for int4 on ROCm**: both real presets use compressed-tensors
pack-quantized. If this image lacks a ROCm kernel for it, swap in the GPTQ alternative noted in the
ini (`Vishva007/gemma-4-E4B-it-W4A16-AutoRound-GPTQ`, 10.0 GB) — different kernel path.
- **Hangs / garbage output**: RDNA3 attention backend. Set `env.VLLM_ATTENTION_BACKEND = TRITON_ATTN`
explicitly; option names moved between versions — `vllmctl.py exec vllm serve --help` shows this image's.
- **GTT growing during a run** (`vramx`, the GTT line): the budget is leaking into system RAM — stop,
lower the utilization. Do not wait for the desktop to stutter.
- **Root-owned files in `~/.cache/huggingface`**: the container runs as root, so its downloads land
root-owned in anon's HF cache. Harmless for the rig; `sudo chown -R anon: ~/.cache/huggingface` if
the host-side `hf` tooling complains.
- **Container died**: `docker logs vllm` (the container is kept after a crash; `vllmctl.py up` shows
the tail and removes it on the next start).
## Phase 2 — if the numbers justify more
1. **Same-file GGUF in vLLM**: GGUF loading moved to an out-of-tree `vllm-gguf-plugin`; it is
"highly experimental and under-optimized" per vLLM's docs and needs a custom image layer
(`pip install vllm-gguf-plugin`), plus `--tokenizer <base repo>`. Untested on ROCm. It would make the
weights identical — and show vLLM's unoptimized path, so interpret carefully.
2. **MTP in vLLM**: `RedHatAI/Qwen3.5-9B-quantized.w4a16` ships `model_mtp.safetensors`; the
commented `speculative-config` in the ini is the knob (syntax per this image's `--help`).
3. **Permanent coexistence**: replace the router's `--models-preset` mode with llama-swap (AUR
`llama-swap-bin`), which spawns llama-server *or* the vLLM container per model id, one at a time,
with a `ttl` idle unload. Same host/port/model ids for every client. Only worth it if a vLLM model
earns a place in the daily roster.
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
bench-vs — the SAME prompt through llama.cpp (router :11343) and vLLM (:11348), one table.
Measures per target: decode t/s, prefill t/s, TTFT, VRAM used/free (amdgpu sysfs, no root needed),
optionally decode at a long prompt (--ctx) and aggregate throughput under N parallel requests
(--concurrency). Method = ../llamacpp/bench.py (streamed chat completion, usage.completion_tokens
over wall time after the first token), applied identically to both engines; llama.cpp's own
`timings` figure is shown in parentheses so rows stay comparable with bench-results.md.
Thinking is OFF by default on both engines (chat_template_kwargs.enable_thinking=false) and the
first reasoning_content delta counts as the first token anyway — a reasoning preamble of different
length per engine would otherwise poison TTFT. --think re-enables it.
GUARDS (the card drives the display; two resident engines = GTT spill = frozen PC):
- a llamacpp target is refused while vLLM answers /health (stop it: ./vllmctl.py down)
- a vllm target is refused while the router reports a loaded model or llama.service is active
./bench-vs.py -t vllm:gemma-4-E4B-it-w4a16 # vLLM up (vllmctl.py up …), router stopped
./bench-vs.py -t llamacpp:gemma-4-E4B-it-UD-Q8_K_XL # vLLM down, router started
./bench-vs.py -t llamacpp:Qwen3.5-9B-UD-Q6_K_XL --ctx 8000 --concurrency 4
Output: bench-vs-results.md (latest) + bench-vs-history.md (append), next to this script. Stdlib only.
"""
import argparse, json, os, re, subprocess, sys, threading, time, urllib.request, urllib.error
from datetime import datetime
HERE = os.path.dirname(os.path.abspath(__file__))
DRM = "/sys/class/drm/renderD128/device"
URLS = {"llamacpp": "http://127.0.0.1:11343/v1", "vllm": "http://127.0.0.1:11348/v1"}
TASK = "Write a Python function that merges two sorted lists, with a short docstring and one example."
def sysfs(name):
try:
return int(open(os.path.join(DRM, name)).read().strip())
except (OSError, ValueError):
return None
def gb(b):
return f"{b / 1e9:.1f} GB" if b is not None else "-"
def http(url, key, payload=None, timeout=600):
data = json.dumps(payload).encode() if payload is not None else None
hdrs = {"Authorization": f"Bearer {key}"}
if data:
hdrs["Content-Type"] = "application/json"
return urllib.request.urlopen(urllib.request.Request(url, data=data, headers=hdrs), timeout=timeout)
def parse_ini(path):
sections, sec = {}, None
try:
lines = open(path, encoding="utf-8").read().splitlines()
except OSError:
return {}
for ln in lines:
s = ln.strip()
if not s or s.startswith("#"):
continue
if s.startswith("[") and s.endswith("]"):
sec = s[1:-1]; sections[sec] = {}
elif sec and "=" in s:
k, v = s.split("=", 1)
sections[sec][k.strip()] = v.split(" #", 1)[0].split("\t#", 1)[0].strip()
return sections
def run(base, key, model, prompt, n, temp, timeout, think):
payload = {"model": model, "messages": [{"role": "user", "content": prompt}],
"max_tokens": n, "temperature": temp, "stream": True,
"stream_options": {"include_usage": True},
"timings_per_token": True} # llama.cpp extension, ignored by vLLM
if not think:
payload["chat_template_kwargs"] = {"enable_thinking": False} # both engines accept it; templates without it ignore it
t0 = time.perf_counter(); ttft = None; ntok = 0; usage = None; timings = None
for raw in http(base.rstrip("/") + "/chat/completions", key, payload, timeout):
line = raw.decode("utf-8", "ignore").strip()
if not line.startswith("data:"):
continue
body = line[5:].strip()
if body == "[DONE]":
break
try:
c = json.loads(body)
except ValueError:
continue
ch = c.get("choices") or [{}]
d = ch[0].get("delta", {}) if ch else {}
if d.get("content") or d.get("reasoning_content") or d.get("reasoning"):
if ttft is None:
ttft = time.perf_counter() - t0
ntok += 1
if c.get("usage"):
usage = c["usage"]
if c.get("timings"):
timings = c["timings"]
total = time.perf_counter() - t0
comp = (usage or {}).get("completion_tokens") or ntok
ptok = (usage or {}).get("prompt_tokens")
gen_s = total - (ttft or total)
tg = comp / gen_s if gen_s > 0 else 0.0
pp = (ptok / ttft) if (ptok and ttft) else None
srv = timings.get("predicted_per_second") if timings else None # llama.cpp's own number, for the ledger
return {"tg": tg, "pp": pp, "ttft": ttft or 0.0, "ptok": ptok or 0, "ctok": comp, "wall": total, "srv": srv,
"t0": t0, "t1": t0 + total}
def with_retries(fn, retries, wait, label=""):
last = None
for i in range(retries + 1):
try:
return fn()
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", "ignore")[:300]
if e.code < 500 or "failed to load" in body.lower():
raise
last = e
except (urllib.error.URLError, ConnectionError, TimeoutError) as e:
last = e
if i < retries:
print(f" … {label}: not ready ({last}); retry in {wait}s", file=sys.stderr)
time.sleep(wait)
raise last
def vllm_up(url):
try:
return http(re.sub(r"/v1/?$", "", url) + "/health", "x", timeout=3).status == 200
except Exception:
return False
def _status(m):
"""Router /models status: newer builds send {'value': 'loaded'|'sleeping'|'unloaded'|…}, older a string."""
st = m.get("status", "loaded")
return st.get("value", "loaded") if isinstance(st, dict) else st
def router_loaded(url):
try:
d = json.load(http(re.sub(r"/v1/?$", "", url) + "/models", "x", timeout=3))
return [m["id"] for m in d.get("data", []) if _status(m) in ("loaded", "loading")]
except Exception:
return []
def router_active():
try:
r = subprocess.run(["systemctl", "is-active", "llama.service"], capture_output=True, text=True, timeout=10)
except (OSError, subprocess.TimeoutExpired):
return False
return r.stdout.strip() == "active"
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("-t", "--target", action="append", required=True,
help="engine:model, engine = llamacpp | vllm (repeatable)")
ap.add_argument("--url", action="append", default=[], help="engine=URL override, e.g. vllm=http://host:11348/v1")
ap.add_argument("-k", "--key", default="no-key-required")
ap.add_argument("-n", "--tokens", type=int, default=256)
ap.add_argument("--temp", type=float, default=0.3)
ap.add_argument("--ctx", type=int, default=0, help="also record decode at ~this many prompt tokens")
ap.add_argument("--concurrency", type=int, default=0, help="also record aggregate t/s with N parallel requests")
ap.add_argument("--think", action="store_true", help="leave the model's thinking ON (default: off on both engines)")
ap.add_argument("--timeout", type=int, default=600)
ap.add_argument("--retries", type=int, default=4); ap.add_argument("--retry-wait", type=int, default=15)
ap.add_argument("--force", action="store_true", help="skip the two-engines-resident guards")
ap.add_argument("--out", default=os.path.join(HERE, "bench-vs-results.md"))
ap.add_argument("--history", default=os.path.join(HERE, "bench-vs-history.md"))
a = ap.parse_args()
urls = dict(URLS)
for u in a.url:
k, v = u.split("=", 1); urls[k] = v
llcfg = parse_ini(os.path.join(HERE, "..", "llamacpp", "config.ini"))
vlcfg = parse_ini(os.path.join(HERE, "vllm.ini"))
total = sysfs("mem_info_vram_total")
if total is None:
print(" (amdgpu sysfs not readable here — VRAM columns are OFF; run this ON fl for them)", file=sys.stderr)
filler = ("The quick brown fox jumps over the lazy dog. " * max(1, a.ctx // 9)) if a.ctx else None
cols = ["engine", "model", "cfg ctx", "spec", "decode t/s", "prefill t/s", "TTFT s", "VRAM used", "VRAM free"]
if a.ctx:
cols.append(f"decode@{a.ctx // 1000}k")
if a.concurrency:
cols.append(f"agg t/s @{a.concurrency}")
latest = open(a.out, "w", encoding="utf-8"); hist = open(a.history, "a", encoding="utf-8")
def emit(line):
print(line); latest.write(line + "\n"); latest.flush(); hist.write(line + "\n"); hist.flush()
latest.write("# llama.cpp vs vLLM (latest run)\n\n"); hist.write(f"\n## run {datetime.now():%Y-%m-%d %H:%M}\n\n")
emit(f"_Run {datetime.now():%Y-%m-%d %H:%M} · gen {a.tokens} tok · temp {a.temp} · thinking "
f"{'ON' if a.think else 'OFF'} · VRAM total {gb(total)} · baseline used {gb(sysfs('mem_info_vram_used'))} · "
f"decode = usage tokens / wall after first token on BOTH engines (llama.cpp's own timings in parentheses)_\n")
emit("| " + " | ".join(cols) + " |"); emit("|" + "|".join(["---"] * len(cols)) + "|")
for t in a.target:
eng, _, model = t.partition(":")
if eng not in urls or not model:
emit(f"| {eng} | {model} | bad target — use engine:model with engine in {list(urls)} |"); continue
base = urls[eng]
# guards
if not a.force:
if eng == "llamacpp" and vllm_up(urls["vllm"]):
emit(f"| llamacpp | {model} | ⚠ SKIPPED: vLLM is up on {urls['vllm']} — loading a router model on top = freeze. "
f"`vllmctl.py down` first |"); continue
if eng == "vllm":
loaded = router_loaded(urls["llamacpp"])
if loaded or router_active():
emit(f"| vllm | {model} | ⚠ SKIPPED: router loaded={loaded or '-'} active={router_active()} — "
f"stop llama.service before benching vLLM |"); continue
if eng == "llamacpp":
c = llcfg.get(model, {})
cfgctx, spec = c.get("ctx-size", "-"), (c.get("spec-type") or "-")
else:
c = vlcfg.get(model, {})
cfgctx = c.get("max-model-len", "-")
spec = "mtp" if c.get("speculative-config") else "-"
cells = [eng, model, cfgctx, spec]
try:
with_retries(lambda: run(base, a.key, model, "hi", 8, a.temp, a.timeout, a.think), a.retries, a.retry_wait, model)
used = sysfs("mem_info_vram_used")
r = with_retries(lambda: run(base, a.key, model, TASK, a.tokens, a.temp, a.timeout, a.think), a.retries, a.retry_wait, model)
dec = f"{r['tg']:.1f}" + (f" ({r['srv']:.1f})" if r["srv"] else "")
cells += [dec, f"{r['pp']:.0f}" if r["pp"] else "-", f"{r['ttft']:.2f}", gb(used),
gb(total - used) if (total and used is not None) else "-"]
if a.ctx:
rc = run(base, a.key, model, filler + "\n\n" + TASK, a.tokens, a.temp, a.timeout, a.think)
cells.append(f"{rc['tg']:.1f}")
if a.concurrency:
res = [None] * a.concurrency
def worker(i):
res[i] = run(base, a.key, model, TASK + f" (variant {i})", a.tokens, a.temp, a.timeout, a.think)
th = [threading.Thread(target=worker, args=(i,)) for i in range(a.concurrency)]
for x in th: x.start()
for x in th: x.join()
ok = [x for x in res if x]
wall = max(x["t1"] for x in ok) - min(x["t0"] for x in ok)
cells.append(f"{sum(x['ctok'] for x in ok) / wall:.1f}" if wall > 0 else "-")
except Exception as e:
err = f"HTTP {e.code} {e.read().decode('utf-8', 'ignore')[:120]!r}" if isinstance(e, urllib.error.HTTPError) else f"ERROR {e}"
cells = cells[:4] + [err] + [""] * (len(cols) - 5) # same width whether it died early or in the --ctx/--concurrency pass
emit("| " + " | ".join(str(x) for x in cells) + " |")
latest.write("\n_llama.cpp rows run with the preset's MTP draft when `spec` says so; vLLM rows have no speculation "
"unless `spec` = mtp. `agg t/s @N` = N parallel requests, completion tokens over the wall time of "
"the whole batch (llama-server's default slot count serves them in parallel too)._\n")
latest.close(); hist.close()
print(f"\nwrote {a.out} (+ appended {a.history})")
if __name__ == "__main__":
main()
+92
View File
@@ -0,0 +1,92 @@
# vLLM presets for fl — AMD RX 7600 XT · 16 GB · docker image vllm/vllm-openai-rocm
# Same convention as ../llamacpp/config.ini: section name = served model id, `key = value` = `--key value`.
#
# ⚠ ONE ENGINE AT A TIME. vLLM pre-allocates `gpu-memory-utilization` × 16 GiB at startup and KEEPS it.
# Any llama.cpp load on top (a client hitting :11343 is enough) overcommits → GTT spill → PC freeze.
# `vllmctl.py up` refuses to start while llama.service is active — stop the router first
# (`vllmctl.py up <preset> --stop-router`), start it again after `vllmctl.py down`.
#
# Sections:
# [global] container/runtime knobs (NOT vllm flags) — see vllmctl.py
# [<preset>] one `vllmctl.py up <preset>`:
# model HF repo id, or /models/<dir> (models-dir is mounted at /models, read-only)
# any other key → `--key value` for `vllm serve`; `true` → bare `--key`; `false` → omitted
# served-model-name defaults to the section name (what clients and bench-vs.py use)
# x-* vllmctl-only metadata (x-weights-gb feeds the VRAM projection)
# env.NAME = value container environment (per preset; [global] env.* apply to all)
#
# VRAM MATH (vllmctl prints it before every start, aborts under reserve-gb unless --force):
# budget = gpu-memory-utilization × 17.16 GB (vLLM's total = the card's total, same as sysfs)
# budget must hold weights + activations + KV cache; everything above it + the desktop must stay
# under 16 GB with ≥ reserve-gb free. 0.75 → 12.9 GB budget → ~2.9 GB free on a 1.3 GB clean desktop.
# A busy evening desktop (3+ GB) does NOT fit any of the real models below — clean desktop only.
# Sizes measured 2026-10-21 from the HF API: the 4-bit vLLM checkpoints are 10-11.5 GB because
# embeddings, lm_head and the vision tower stay bf16 — bigger than the Q6/Q8 GGUFs they pair with.
[global]
image = vllm/vllm-openai-rocm:v0.32.0 # = `latest` on 2026-10-18 (ROCm 10.0, 11.6 GB download); update = bump this + `vllmctl.py pull`
container = vllm
port = 11348 # whisper = 11345/11346, kokoro = 11347
models-dir = /home/anon/software/models # mounted at /models (ro) — share the GGUF dir
hf-cache = /home/anon/.cache/huggingface # mounted at /root/.cache/huggingface (downloads + token)
router = http://127.0.0.1:11343 # llama.cpp router root (for the resident-model gate)
reserve-gb = 2.0 # abort if projected free VRAM < this (bench.py floor 1.5, target 2.5)
drain-gb = 4.0 # "card is clean" threshold before starting (same as bench.py)
health-timeout = 1800 # s — first start compiles Triton kernels, be patient
gpu-memory-utilization = 0.75 # default; presets override
# REQUIRED on this card (✓ verified 2026-10-21): v0.32.0 ships no gfx1102 code objects — without the
# override every GPU kernel launch aborts the process SILENTLY (the engine core just vanishes, the
# API server reports "Failed core proc(s): {}"). gfx1100 (RX 7900) code runs on gfx1102, same RDNA3 ISA.
env.HSA_OVERRIDE_GFX_VERSION = 11.0.0
# Attention backend knobs (ROCm; names moved between versions — `vllmctl.py exec vllm serve --help`):
# env.VLLM_ATTENTION_BACKEND = TRITON_ATTN # the default on RDNA; set explicitly if a hang points at attention
# ─── 1. smoke test — tiny, certain to be supported; proves docker + GPU + image + override ────
# ✓ 2026-10-21: 154.9 t/s decode · 1462 t/s prefill · TTFT 0.02 s · 7.7 GB used incl. a 2.9 GB desktop.
[Qwen3-0.6B-smoke]
model = Qwen/Qwen3-0.6B
gpu-memory-utilization = 0.30
max-model-len = 4096
x-weights-gb = 1.5
# ─── 2. gemma-4-E4B — ✗ DOES NOT FIT vLLM ON THIS CARD (2026-10-21), tombstone kept for the math ──
# Pairs with llama.cpp [gemma-4-E4B-it-UD-Q8_K_XL] (57 t/s with MTP, 96k ctx). google/gemma-4-E4B-it-qat-w4a16-ct
# is 11.5 GB, and the second try (text-only, batch 2048, @0.73 on a 2.1 GB desktop) ended with
# "Available KV cache memory: -4.44 GiB" after an OOM on a 5,637,144,576-byte allocation. That is exactly
# Gemma 4's per-layer embedding table: 262144 vocab × 42 layers × 256 × 2 bytes (bf16). vLLM holds it
# in bf16 AND needs a second buffer of the same size → ~16 GiB before any KV block. No util, batch or
# desktop fixes that; the GPTQ/AWQ alternatives carry the same bf16 table. llama.cpp fits because the
# GGUF stores the table quantized, once. A different image changes nothing here.
# [gemma-4-E4B-it-w4a16]
# model = google/gemma-4-E4B-it-qat-w4a16-ct
# gpu-memory-utilization = 0.78
# max-model-len = 16384
# max-num-seqs = 4
# max-num-batched-tokens = 2048
# limit-mm-per-prompt = {"image": 0, "audio": 0, "video": 0}
# x-weights-gb = 11.5
# ─── 3. Qwen3.5-9B — pairs with llama.cpp [Qwen3.5-9B-UD-Q6_K_XL] (32 t/s with MTP, 32k ctx) ──
# Red Hat's llm-compressor int4 (compressed-tensors). 10.95 GB + a 0.49 GB MTP head file.
# Alternative (real AWQ, Triton kernels on ROCm): QuantTrio/Qwen3.5-9B-AWQ — 12.4 GB, too big for 0.78.
# The last viable "same model" pair: no per-layer table, but a 248k vocab → the fp32 logits buffer
# during profiling is max-num-batched-tokens × 248k × 4 B (2048 → 2.0 GB, 512 → 0.5 GB). Start small.
# On a 2.1 GB desktop the projection allows 0.75 (`--set gpu-memory-utilization=0.75`) → ~1.9 GB for
# activations + graphs + KV. If "No available memory for the cache blocks" again: `--set enforce-eager=true`
# (drops the CUDA-graph memory, costs decode speed), then a cleaner desktop for 0.78.
# ✓ RUNS 2026-10-21 @0.80 (0.75 was 0.11 GiB short of KV room): 29.4 t/s decode · 181 prefill · TTFT 0.17 ·
# decode@8k 16.0 · 4-stream aggregate 60.6 · 12.5 GB used on a 0.6 GB desktop. Same evening, llama.cpp
# [Qwen3.5-9B-UD-Q6_K_XL] with its MTP draft: 61.3 · 114 · 0.27 · 51.1 · 85.4 · 10.9 GB. See README.
[Qwen3.5-9B-w4a16]
model = RedHatAI/Qwen3.5-9B-quantized.w4a16
gpu-memory-utilization = 0.80 # needs a ≤ 0.6 GB desktop (projection: 2.9 GB free); 0.75 fails on KV room
max-model-len = 16384
max-num-seqs = 4
max-num-batched-tokens = 512 # caps the profiling logits buffer (prefill t/s cost — note it in the ledger)
limit-mm-per-prompt = {"image": 0, "video": 0} # text-only bench (Qwen3.5 has no audio input)
x-weights-gb = 11.0
# NEXT (the one knob that could change the verdict): MTP speculative decoding — the repo ships
# model_mtp.safetensors and llama.cpp's 2× comes from exactly this. Costs ~0.5 GB weights + draft
# state → try @0.82 on a clean desktop. Syntax per `vllmctl.py exec vllm serve --help`:
# ./vllmctl.py up Qwen3.5-9B-w4a16 --set gpu-memory-utilization=0.82 --set 'speculative-config={"method": "mtp", "num_speculative_tokens": 2}'
# speculative-config = {"method": "mtp", "num_speculative_tokens": 2}
+398
View File
@@ -0,0 +1,398 @@
#!/usr/bin/env python3
"""
vllmctl — run vLLM (docker, ROCm) on fl WITHOUT freezing the box.
The RX 7600 XT drives the display; VRAM overcommit spills to GTT and freezes the PC. vLLM grabs
`gpu-memory-utilization` × total VRAM at startup and keeps it, so this script gates every start:
1. llama.service must be STOPPED (or --stop-router): a LAN client hitting :11343 would make the
router load a model on top of vLLM = the freeze. --unsafe-keep-router skips this gate.
2. the card must be clean (VRAM used ≤ drain-gb; resident router models are unloaded via its API).
3. projected free VRAM = total − used − budget must be ≥ reserve-gb (else abort, --force overrides).
Presets live in vllm.ini next to this file (same key = --flag convention as llamacpp/config.ini).
./vllmctl.py up <preset> [--stop-router] [--force] [--dry-run] start, wait for /health
./vllmctl.py down stop, wait for VRAM to drain
./vllmctl.py status | logs | pull | gpu-check | exec <cmd...>
Stdlib only. Run ON fl.
"""
import argparse, grp, json, os, re, shlex, subprocess, sys, time, urllib.request, urllib.error
HERE = os.path.dirname(os.path.abspath(__file__))
INI = os.path.join(HERE, "vllm.ini")
DRM = "/sys/class/drm/renderD128/device"
# ─── ini ────────────────────────────────────────────────────────────────────────────────────
def parse_ini(path):
"""{section: {key: value}} — inline `# comments` stripped, order kept."""
sections, sec = {}, None
for ln in open(path, encoding="utf-8").read().splitlines():
s = ln.strip()
if not s or s.startswith("#"):
continue
if s.startswith("[") and s.endswith("]"):
sec = s[1:-1]; sections[sec] = {}
elif sec and "=" in s:
k, v = s.split("=", 1)
sections[sec][k.strip()] = v.split(" #", 1)[0].split("\t#", 1)[0].strip()
return sections
def build_args(preset):
"""vllm serve args from a preset section (x-* and env.* are not flags)."""
args = []
for k, v in preset.items():
if k.startswith("x-") or k.startswith("env."):
continue
if v.lower() == "false":
continue
if v.lower() == "true":
args.append(f"--{k}")
else:
args += [f"--{k}", v]
return args
# ─── gpu / router ───────────────────────────────────────────────────────────────────────────
def sysfs(name):
try:
return int(open(os.path.join(DRM, name)).read().strip())
except (OSError, ValueError):
return None
def vram():
"""(used, total, gtt_used) bytes from amdgpu sysfs; None where unreadable."""
return sysfs("mem_info_vram_used"), sysfs("mem_info_vram_total"), sysfs("mem_info_gtt_used")
def gb(b):
return f"{b / 1e9:.1f} GB" if b is not None else "-"
def http(url, payload=None, timeout=10):
data = json.dumps(payload).encode() if payload is not None else None
hdrs = {"Content-Type": "application/json"} if data else {}
return urllib.request.urlopen(urllib.request.Request(url, data=data, headers=hdrs), timeout=timeout)
def _status(m):
"""Router /models status: newer builds send {'value': 'loaded'|'sleeping'|'unloaded'|…}, older a string."""
st = m.get("status", "loaded")
return st.get("value", "loaded") if isinstance(st, dict) else st
def router_loaded(root):
"""ids the llama.cpp router reports as loaded ([] if unreachable)."""
try:
d = json.load(http(root.rstrip("/") + "/models", timeout=5))
except Exception:
return []
return [m["id"] for m in d.get("data", []) if _status(m) in ("loaded", "loading")]
def router_unload(root):
for m in router_loaded(root):
try:
http(root.rstrip("/") + "/models/unload", {"model": m}, timeout=30).read()
except Exception:
pass
def router_active():
try:
r = subprocess.run(["systemctl", "is-active", "llama.service"], capture_output=True, text=True, timeout=10)
except (OSError, subprocess.TimeoutExpired):
return False
return r.stdout.strip() == "active"
def router_hint():
return "" if router_active() else "\n (llama.service is STOPPED → sudo systemctl start llama.service to bring the router back)"
def wait_drain(drain_gb, window_s, label):
used, _, _ = vram()
end = time.time() + window_s
said = False
while used is not None and used > drain_gb * 1e9 and time.time() < end:
if not said:
print(f" … waiting for VRAM to drain {label} ({gb(used)} used)"); said = True
time.sleep(5)
used, _, _ = vram()
return used
# ─── docker ─────────────────────────────────────────────────────────────────────────────────
def docker(*args, check=True, capture=True):
return subprocess.run(["docker", *args], check=check, capture_output=capture, text=True)
def container_state(name):
r = docker("inspect", "-f", "{{.State.Status}}", name, check=False)
return r.stdout.strip() if r.returncode == 0 else None
def gpu_flags():
flags = ["--device", "/dev/kfd", "--device", "/dev/dri", "--group-add", "video",
"--cap-add", "SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--ipc=host"]
try:
flags += ["--group-add", str(grp.getgrnam("render").gr_gid)] # Arch: /dev/dri/renderD128 is group render
except KeyError:
pass
return flags
def run_cmd(g, name, preset, envs):
cmd = ["docker", "run", "-d", "--name", g["container"], *gpu_flags(),
"-p", f'{g["port"]}:8000',
"-v", f'{g["models-dir"]}:/models:ro',
"-v", f'{g["hf-cache"]}:/root/.cache/huggingface']
for k, v in envs.items():
cmd += ["-e", f"{k}={v}"]
cmd.append(g["image"])
cmd += ["--host", "0.0.0.0", "--port", "8000", "--served-model-name", preset.get("served-model-name", name)]
cmd += build_args({k: v for k, v in preset.items() if k != "served-model-name"})
return cmd
ROOT_CAUSE = re.compile(r"hipError|HSA_STATUS|code object|gfx\d+|no kernel image|EngineCore.*(ERROR|Error|error)"
r"|^\S*(ERROR|CRITICAL)|RuntimeError|ValueError|NotImplementedError|OutOfMemory|Killed|Segmentation"
r"|free memory|not supported|Unsupported|Traceback|memory profiling|kv cache memory|weights take|peak", re.I)
def root_cause(lines):
"""The lines that explain a dead engine core: known ROCm/vLLM failure signatures, the FIRST
traceback's error line, and the last 8 lines — not the parent's 40-line stack trace."""
out, seen = [], set()
for l in lines:
if ROOT_CAUSE.search(l) and "api_server" not in l and "core_client" not in l and "launch_core_engines" not in l:
key = l.strip()[:120]
if key not in seen:
seen.add(key); out.append(l.rstrip())
if len(out) >= 30:
break
return out + ["…"] + [l.rstrip() for l in lines[-8:]]
# ─── commands ───────────────────────────────────────────────────────────────────────────────
def cmd_up(a, cfg):
g = cfg["global"]
if a.preset not in cfg or a.preset == "global":
sys.exit(f"unknown preset {a.preset!r}; have: {', '.join(k for k in cfg if k != 'global')}")
preset = dict(cfg[a.preset])
preset.update(dict(kv.split("=", 1) for kv in a.set)) # --set gpu-memory-utilization=0.73 --set max-model-len=8192
preset.setdefault("gpu-memory-utilization", g.get("gpu-memory-utilization", "0.75"))
envs = {k[4:]: v for k, v in g.items() if k.startswith("env.")}
envs.update({k[4:]: v for k, v in preset.items() if k.startswith("env.")})
envs.update(dict(e.split("=", 1) for e in a.env))
util = float(preset["gpu-memory-utilization"])
reserve = float(a.reserve if a.reserve is not None else g.get("reserve-gb", 2.0))
drain = float(g.get("drain-gb", 4.0))
cmd = run_cmd(g, a.preset, preset, envs)
print("docker command:\n " + " ".join(shlex.quote(c) for c in cmd) + "\n")
if a.dry_run:
return
st = container_state(g["container"])
if st == "running":
sys.exit(f"container {g['container']} is already running — `vllmctl.py down` first")
if st: # leftover from a crash: its log is the evidence, show the tail, then clear the name
r = docker("logs", "--tail", "15", g["container"], check=False)
print(f"removing old container ({st}); its last lines were:\n" + "\n".join(" | " + l for l in (r.stdout + r.stderr).strip().splitlines()[-15:]))
docker("rm", g["container"], check=False)
# 1. clean card — unload resident router models through its API while it is still up, then drain
if router_active():
router_unload(g["router"])
used = wait_drain(drain, 180, "before start")
used, total, gtt = vram()
if used is None or total is None:
if not a.force:
sys.exit(f"ABORT: cannot read {DRM}/mem_info_vram_* — no VRAM guard, not starting blind (--force overrides)")
print("⚠ FORCED without a VRAM guard (sysfs unreadable)"); used, total = 0, 1
if used > drain * 1e9 and not a.force:
sys.exit(f"ABORT: card not clean — {gb(used)} VRAM used (> drain-gb {drain}). Leftover model / GPU apps?")
# 2. projection — abort here costs nothing, llama.service is still running
budget = util * total
free_after = total - used - budget
weights = float(preset.get("x-weights-gb", 0) or 0) * 1e9
print(f"VRAM: total {gb(total)} · used now {gb(used)} (GTT {gb(gtt)}) · vLLM budget {util:.2f} × total = {gb(budget)}"
f"\n → projected free {gb(free_after)} (reserve {reserve} GB)")
if weights:
room = budget - weights
print(f" budget − weights {gb(weights)} = {gb(room)} for activations + KV cache"
+ (" ⚠ < 1 GB: expect 'no memory for KV cache' → raise util or lower max-model-len" if room < 1e9 else ""))
if free_after < reserve * 1e9:
suggest = max(0.0, (total - used - reserve * 1e9) / total)
msg = (f"projected free {gb(free_after)} < reserve {reserve} GB. Max safe gpu-memory-utilization right now "
f"= {suggest:.2f} (desktop is using {gb(used)}).")
if not a.force:
sys.exit("ABORT: " + msg + f"\n → free the desktop (vramx) or `--set gpu-memory-utilization={suggest:.2f}` "
f"(+ `--set max-model-len=8192` if weights leave < 1 GB); --force to start anyway.")
print("⚠ FORCED: " + msg)
# 3. router gate — the only thing that stops a LAN client from loading a model on top of vLLM
if router_active():
if a.stop_router:
print("stopping llama.service (sudo) …")
subprocess.run(["sudo", "systemctl", "stop", "llama.service"], check=True)
elif a.unsafe_keep_router:
print("⚠ llama.service stays ACTIVE: any client request to :11343 while vLLM runs = GTT freeze.")
else:
sys.exit("ABORT: llama.service is active. Any client request to :11343 would load a model on top "
"of vLLM and freeze the PC.\n → re-run with --stop-router (sudo systemctl stop llama.service), "
"or --unsafe-keep-router if you really know nothing will call the router.")
used2, _, _ = vram()
if used2 is not None and used2 > used + 1e9: # a client loaded something between projection and stop
print(f"⚠ VRAM grew to {gb(used2)} meanwhile — vLLM's own free-memory check should refuse; watch the start")
# 4. start + wait
cid = subprocess.run(cmd, check=True, capture_output=True, text=True).stdout.strip()
print(f"started {cid[:12]} — waiting for http://127.0.0.1:{g['port']}/health (timeout {g.get('health-timeout', 1800)} s; "
f"first start compiles kernels)")
end = time.time() + int(g.get("health-timeout", 1800))
last_log, shown = 0, set()
while time.time() < end:
st = container_state(g["container"])
if st != "running":
r = docker("logs", g["container"], check=False)
print("root cause candidates (full log: docker logs " + g["container"] + "):")
for l in root_cause((r.stdout + r.stderr).splitlines()):
print(" | " + l[:200])
sys.exit(f"ABORT: container is {st or 'gone'}. If a line names gfx1102 / 'code object' / hipErrorNoBinaryForGpu: "
f"uncomment env.HSA_OVERRIDE_GFX_VERSION = 11.0.0 in [global] and retry." + router_hint())
try:
if http(f"http://127.0.0.1:{g['port']}/health", timeout=3).status == 200:
break
except Exception:
pass
if time.time() - last_log > 30:
r = docker("logs", "--tail", "3", g["container"], check=False)
tail = (r.stdout + r.stderr).strip().splitlines()[-3:]
for t in tail:
if t not in shown: # only lines not printed before (compile phases are quiet for minutes)
print(" | " + t[:160]); shown.add(t)
last_log = time.time()
time.sleep(3)
else:
sys.exit("ABORT: no /health within the timeout — `vllmctl.py logs`, then `vllmctl.py down`" + router_hint())
used, total, gtt = vram()
print(f"UP. VRAM used {gb(used)} / free {gb(total - used)} · GTT {gb(gtt)}")
try:
ids = [m["id"] for m in json.load(http(f"http://127.0.0.1:{g['port']}/v1/models")).get("data", [])]
print(f"models: {', '.join(ids)} → http://192.168.0.204:{g['port']}/v1")
except Exception as e:
print(f"(/v1/models not readable yet: {e})")
print(f"next: ./bench-vs.py -t vllm:{preset.get('served-model-name', a.preset)} · when done: ./vllmctl.py down")
def cmd_down(a, cfg):
g = cfg["global"]
st = container_state(g["container"])
if st:
print(f"stopping {g['container']} ({st}) …")
docker("stop", "-t", "60", g["container"], check=False)
docker("rm", g["container"], check=False)
else:
print("container not running")
used = wait_drain(float(g.get("drain-gb", 4.0)), 120, "after stop")
used, total, gtt = vram()
print(f"VRAM used {gb(used)} / free {gb(total - used) if total and used is not None else '-'} · GTT {gb(gtt)}")
if not router_active():
print("llama.service is stopped → `sudo systemctl start llama.service` to bring the router back")
def cmd_status(a, cfg):
g = cfg["global"]
used, total, gtt = vram()
print(f"container {g['container']}: {container_state(g['container']) or 'not running'}")
print(f"VRAM used {gb(used)} / total {gb(total)} · GTT {gb(gtt)}")
print(f"llama.service: {'ACTIVE' if router_active() else 'stopped'} · loaded: {router_loaded(g['router']) or '-'}")
try:
ids = [m["id"] for m in json.load(http(f"http://127.0.0.1:{g['port']}/v1/models", timeout=3)).get("data", [])]
print(f"vLLM :{g['port']} serving: {', '.join(ids)}")
except Exception:
print(f"vLLM :{g['port']}: not answering")
def cmd_logs(a, cfg):
os.execvp("docker", ["docker", "logs", "-f", "--tail", "100", cfg["global"]["container"]])
def cmd_pull(a, cfg):
os.execvp("docker", ["docker", "pull", cfg["global"]["image"]])
GPU_CHECK = r"""
import os, torch
print('HSA_OVERRIDE_GFX_VERSION =', os.environ.get('HSA_OVERRIDE_GFX_VERSION', '(unset)'))
print('cuda available:', torch.cuda.is_available())
p = torch.cuda.get_device_properties(0)
print('device:', torch.cuda.get_device_name(0) or '(no name)', p.gcnArchName, f'{p.total_memory / 1e9:.1f} GB')
print('torch kernel …', end=' ', flush=True) # a hard abort here (no code object for this arch) prints nothing more
x = torch.ones(1024, device='cuda'); torch.cuda.synchronize(); print('ok', x.sum().item())
import vllm._C # noqa
ops = sorted({s.name.split('::', 1)[1] for s in torch._C._jit_get_all_schemas() if s.name.startswith('_C::')})
print(f'vllm _C ops registered: {len(ops)}')
f16 = dict(device='cuda', dtype=torch.float16)
probes = {'silu_and_mul': lambda: torch.ops._C.silu_and_mul(torch.empty(4, 8, **f16), torch.randn(4, 16, **f16)),
'gelu_fast': lambda: torch.ops._C.gelu_fast(torch.empty(4, 8, **f16), torch.randn(4, 8, **f16)),
'rms_norm': lambda: torch.ops._C.rms_norm(torch.empty(4, 8, **f16), torch.randn(4, 8, **f16), torch.ones(8, **f16), 1e-6)}
for name, fn in probes.items():
if name in ops:
print(f'vllm custom op {name} …', end=' ', flush=True); fn(); torch.cuda.synchronize(); print('ok -> the image has kernels for this arch')
break
else:
print('none of the probe ops exist in this build; first ops:', ops[:15])
"""
def cmd_gpu_check(a, cfg):
"""Does the image see the card? Prints torch's device name + arch (needs no model)."""
g = cfg["global"]
envs = [x for k, v in g.items() if k.startswith("env.") for x in ("-e", f"{k[4:]}={v}")]
envs += [x for e in a.env for x in ("-e", e)]
code = GPU_CHECK
cmd = ["docker", "run", "--rm", *gpu_flags(), *envs, "--entrypoint", "python3", g["image"], "-u", "-c", code]
print(" ".join(shlex.quote(c) for c in cmd))
os.execvp("docker", cmd)
def cmd_exec(a, cfg):
g = cfg["global"]
if container_state(g["container"]) == "running":
os.execvp("docker", ["docker", "exec", "-it", g["container"], *a.cmd])
envs = [x for k, v in g.items() if k.startswith("env.") for x in ("-e", f"{k[4:]}={v}")]
os.execvp("docker", ["docker", "run", "--rm", "-it", *gpu_flags(), *envs, "--entrypoint", a.cmd[0], g["image"], *a.cmd[1:]])
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--ini", default=INI)
sub = ap.add_subparsers(dest="cmd", required=True)
up = sub.add_parser("up"); up.add_argument("preset")
up.add_argument("--stop-router", action="store_true", help="sudo systemctl stop llama.service first")
up.add_argument("--unsafe-keep-router", action="store_true", help="leave llama.service running (freeze risk)")
up.add_argument("--force", action="store_true", help="ignore the VRAM projection / clean-card checks")
up.add_argument("--reserve", type=float, help="override reserve-gb")
up.add_argument("--dry-run", action="store_true", help="print the docker command only")
up.add_argument("--env", action="append", default=[], metavar="KEY=VAL", help="extra container env (e.g. HSA_OVERRIDE_GFX_VERSION=11.0.0)")
up.add_argument("--set", action="append", default=[], metavar="KEY=VAL", help="override a preset key for this start (e.g. gpu-memory-utilization=0.73)")
sub.add_parser("down"); sub.add_parser("status"); sub.add_parser("logs"); sub.add_parser("pull")
gc = sub.add_parser("gpu-check"); gc.add_argument("--env", action="append", default=[], metavar="KEY=VAL")
ex = sub.add_parser("exec"); ex.add_argument("cmd", nargs=argparse.REMAINDER)
a = ap.parse_args()
cfg = parse_ini(a.ini)
if "global" not in cfg:
sys.exit(f"{a.ini}: no [global] section")
{"up": cmd_up, "down": cmd_down, "status": cmd_status, "logs": cmd_logs, "pull": cmd_pull,
"gpu-check": cmd_gpu_check, "exec": cmd_exec}[a.cmd](a, cfg)
if __name__ == "__main__":
main()
+6 -6
View File
@@ -28,7 +28,7 @@ WHISPER_LANG=auto
which expands to:
```
```sh
whisper-server --model $WHISPER_MODEL \
--host 0.0.0.0 --port $WHISPER_PORT \
--inference-path /v1/audio/transcriptions \
@@ -56,7 +56,7 @@ with auth in front — whisper-server has no `--api-key`.
`git pull` edit is on disk immediately — but systemd caches unit files, so it still needs a reload.
(Copying into `/etc` instead would work, but then every repo edit silently drifts.) On the server:
```
```sh
dotsync # nudges you if this dir changed
sudo systemctl daemon-reload
sudo systemctl restart whisper@turbo whisper@small
@@ -66,7 +66,7 @@ Editing only an `.env` needs no `daemon-reload` — just restart that instance.
First install (stow does not install units):
```
```sh
yay -S whisper-cpp ffmpeg
curl -L -o ~/software/models/ggml-large-v3-turbo-q5_0.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q5_0.bin
@@ -99,7 +99,7 @@ transcribing it, and the `gemma-4-E4B` preset samples at temp 1.0. Whisper is pu
**Still available as a fallback** for wav/mp3/flac (no config change needed), if these services are down:
```
```sh
curl -F file=@clip.wav -F model=gemma-4-E4B-it-UD-Q8_K_XL \
http://$SERVER:11343/v1/audio/transcriptions
```
@@ -122,7 +122,7 @@ with line breaks mid-sentence.
## Verifying
```
```sh
systemctl status whisper@turbo whisper@small
rocm-smi --showmeminfo vram # must be UNCHANGED vs before the services started
@@ -159,7 +159,7 @@ have no server integration, so **model size is the only real dial**.
A/B any candidate without touching a service:
```
```sh
curl -LO https://github.com/ggml-org/whisper.cpp/raw/master/samples/jfk.wav # known text
time whisper-cli -m ~/software/models/ggml-small.en-q5_1.bin -f jfk.wav -t 6 -nt
time whisper-cli -m ~/software/models/ggml-base.en-q5_1.bin -f jfk.wav -t 6 -nt
+2 -1
View File
@@ -1,4 +1,5 @@
# Synthwave '84 — matches ../kitty/theme.conf
# Colours from Robb Owen's SynthWave '84 palette, arranged for this repo (yellow and magenta
# swapped). Background/foreground match kitty.conf's own colours.
[colors.primary]
background = "#262335"
+1
View File
@@ -0,0 +1 @@
user-db:hyde_hyprland
@@ -0,0 +1,12 @@
# gui package — put HyDE's script dir on the systemd *user manager* PATH, so
# user units can resolve bare hyde script names. Nothing else on the system ever
# adds it: HyDE only self-bootstraps PATH inside its own wrappers
# (hyde-shell/app.sh), so anything a unit execs bare is "command not found" —
# hypridle's lock_cmd was the incident (TROUBLESHOOTING.md symptom 8).
# Lifecycle: the environment generator reads this at user-manager start, but the
# session-start PATH import overwrites the manager's PATH afterwards — daemon-reload
# won't re-apply this over an imported PATH; only relogin does. The fish twin
# gui/.config/fish/conf.d/hyde-path.fish is what makes that import *carry* the dir
# (it feeds the session PATH the import copies from), so both files are needed and
# together they hold in either order.
PATH=$HOME/.local/lib/hyde:$PATH
+23 -17
View File
@@ -1,26 +1,32 @@
## gui package — pin which config Hyprland starts with.
##
## HyDE 26.x configures Hyprland in Lua and points HYPRLAND_CONFIG at
## ~/.local/share/hypr/hyde.lua. Once that is set, Hyprland never reads
## ~/.config/hypr/hyprland.conf, so the entire dots hypr layer (theme, keybinds,
## window rules, monitors, userprefs) is bypassed and the session comes up as
## stock HyDE — silently, with `hyprctl configerrors` empty.
## Points HYPRLAND_CONFIG at HyDE's Lua entry point, explicitly. The Lua chain
## loads HyDE's lua/ modules and then the user override
## ~/.config/hypr/hyprland.lua (repo: gui/.config/hypr/hyprland.lua, which pulls
## in the per-host host.lua). None of the old hyprlang .conf files are read in
## Lua mode. See LUA-PORT.md.
##
## Why it is set from *fish* of all places: the update dropped a real
## ~/.config/fish/conf.d/hyde.fish (untracked, HyDE-owned) which exports it, and
## fish sources every conf.d/*.fish at startup. SDDM launches the session through
## the login shell (/usr/bin/start-hyprland, the `hyprland.desktop` entry — not
## Why it is set from *fish* of all places: HyDE's untracked
## ~/.config/fish/conf.d/hyde.fish exports it, and fish sources every
## conf.d/*.fish at startup. SDDM launches the session through the login shell
## (/usr/bin/start-hyprland, the `hyprland.desktop` entry — not
## `hyprland-uwsm.desktop`), so the compositor inherits whatever fish exported.
## HyDE's snippet only assigns `if test -z "$HYPRLAND_CONFIG"`, and conf.d files
## load in alphabetical order, so this file's `00-` prefix means ours is already
## set by the time hyde.fish looks — do not rename it above `hyde.fish`.
##
## The uwsm twin of this lives in gui/.config/uwsm/env-hyprland.d/10-dots.sh and
## covers the `hyprland-uwsm.desktop` session entry, which sources uwsm env files
## instead of the shell. Both are needed; neither is sufficient alone.
## The uwsm twin lives in gui/.config/uwsm/env-hyprland.d/10-dots.sh and covers
## the `hyprland-uwsm.desktop` session entry. Both are needed; neither is
## sufficient alone. Keeping the pins alive (instead of trusting HyDE's default)
## preserves the invariant that the dots repo decides the config, and makes any
## rollback a one-line edit. Takes effect at session start — relogin, not
## `hyprctl reload`.
##
## STOPGAP: this pins ~/.local/share/hyde/hyprland.conf, which current HyDE no
## longer ships (ours is a leftover of the pre-update install). The supported
## path is porting the hypr layer to ~/.config/hypr/hyprland.lua. See
## HYDE-UPDATE.md. Takes effect at session start — relogin, not `hyprctl reload`.
set -gx HYPRLAND_CONFIG "$HOME/.config/hypr/hyprland.conf"
## ROLLBACK to hyprlang (the chain was retired at LUA-PORT.md step 8,
## 2026-09-10): restore gui/.config/hypr, wm/.config/hypr, lw/.config/hypr and
## gui/.local/share/hyde from bddbbf3 (the last pre-step-8 commit), ./install.sh, point both
## pins back at "$HOME/.config/hypr/hyprland.conf" and relogin. d851213 is the
## last hyprlang `main` (the port merged 2026-11-09).
set -l hypr_data "$XDG_DATA_HOME"
test -n "$hypr_data"; or set hypr_data "$HOME/.local/share"
set -gx HYPRLAND_CONFIG "$hypr_data/hypr/hyde.lua"
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"diagnostics": {
"globals": ["hl", "hyde", "check_require"]
},
"workspace": {
"library": ["/usr/share/hypr/stubs", "~/.local/share/hypr/lua"]
},
"runtime": {
"version": "Lua 5.5"
}
}
-12
View File
@@ -1,12 +0,0 @@
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# See https://wiki.hypr.land/Configuring/Animations/
# Edit or add presets in ./animations/, then run 'animations.sh --select' to switch.
# (noanim layerrules for hyprpicker/selection live in userprefs.conf)
$ANIMATION=theme
# Absolute, not ./ — HyDE re-sources $ANIMATION_PATH from ~/.local/share/hyde/hyprland.conf,
# where a relative path resolves against that directory instead of this one.
$ANIMATION_PATH=~/.config/hypr/animations/theme.conf
source = $ANIMATION_PATH
-18
View File
@@ -1,18 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Classic"
# credit https://github.com/mylinuxforwork/dotfiles
# -----------------------------------------------------
animations {
enabled = true
bezier = myBezier, 0.05, 0.9, 0.1, 1.05
animation = windows, 1, 7, myBezier
animation = windowsOut, 1, 7, default, popin 80%
animation = border, 1, 10, default
animation = borderangle, 1, 8, default
animation = fade, 1, 7, default
animation = workspaces, 1, 6, default
}
-35
View File
@@ -1,35 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Diablo-1"
# credit https://github.com/Itz-Abhishek-Tiwari
# -----------------------------------------------------
animations {
enabled = 1
bezier = default, 0.05, 0.9, 0.1, 1.05
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = overshot, 0.13, 0.99, 0.29, 1.08
bezier = liner, 1, 1, 1, 1
bezier = bounce, 0.4, 0.9, 0.6, 1.0
bezier = snappyReturn, 0.4, 0.9, 0.6, 1.0
bezier = slideInFromRight, 0.5, 0.0, 0.5, 1.0
animation = windows, 1, 5, snappyReturn, slidevert
animation = windowsIn, 1, 5, snappyReturn, slidevert right
animation = windowsOut, 1, 5, snappyReturn, slide
animation = windowsMove, 1, 6, bounce, slide
animation = layersOut, 1, 5, bounce, slidevert right
animation = fadeIn, 1, 10, default
animation = fadeOut, 1, 10, default
animation = fadeSwitch, 1, 10, default
animation = fadeShadow, 1, 10, default
animation = fadeDim, 1, 10, default
animation = fadeLayers, 1, 10, default
animation = workspaces, 1, 7, overshot, slidevert
animation = border, 1, 1, liner
animation = layers, 1, 4, bounce, slidevert right
animation = borderangle, 1, 30, liner, loop
}
-29
View File
@@ -1,29 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Diablo-2"
# credit https://github.com/Itz-Abhishek-Tiwari
# -----------------------------------------------------
animations {
enabled = 1
bezier = default, 0.05, 0.9, 0.1, 1.05
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = overshot, 0.13, 0.99, 0.29, 1.08
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 7, wind, popin
animation = windowsIn, 1, 7, overshot, popin
animation = windowsOut, 1, 5, overshot, popin
animation = windowsMove, 1, 6, overshot, slide
animation = layers, 1, 5, default, popin
animation = fadeIn, 1, 10, default
animation = fadeOut, 1, 10, default
animation = fadeSwitch, 1, 10, default
animation = fadeShadow, 1, 10, default
animation = fadeDim, 1, 10, default
animation = fadeLayers, 1, 10, default
animation = workspaces, 1, 7, overshot, slidevert
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
}
-9
View File
@@ -1,9 +0,0 @@
# // ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# // █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
# See https://wiki.hyprland.org/Configuring/Animations/
# credits: https://github.com/prasanthrangan/hyprdots
animations:enabled=false
-23
View File
@@ -1,23 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Dynamic"
# credit https://github.com/mylinuxforwork/dotfiles
# -----------------------------------------------------
animations {
enabled = true
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}
-43
View File
@@ -1,43 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "End4"
# credit https://github.com/end-4/dots-hyprland
# -----------------------------------------------------
animations {
enabled = true
# Animation curves
bezier = linear, 0, 0, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = menu_decel, 0.1, 1, 0, 1
bezier = menu_accel, 0.38, 0.04, 1, 0.07
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
bezier = softAcDecel, 0.26, 0.26, 0.15, 1
bezier = md2, 0.4, 0, 0.2, 1 # use with .2s duration
# Animation configs
animation = windows, 1, 3, md3_decel, popin 60%
animation = windowsIn, 1, 3, md3_decel, popin 60%
animation = windowsOut, 1, 3, md3_accel, popin 60%
animation = border, 1, 10, default
animation = fade, 1, 3, md3_decel
# animation = layers, 1, 2, md3_decel, slide
animation = layersIn, 1, 3, menu_decel, slide
animation = layersOut, 1, 1.6, menu_accel
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
# animation = workspaces, 1, 2.5, softAcDecel, slide
# animation = workspaces, 1, 7, menu_decel, slidefade 15%
# animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}
-26
View File
@@ -1,26 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Fast"
# credit https://github.com/mylinuxforwork/dotfiles
# -----------------------------------------------------
animations {
enabled = true
bezier = linear, 0, 0, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = fluent_decel, 0.1, 1, 0, 1
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
animation = windows, 1, 3, md3_decel, popin 60%
animation = border, 1, 10, default
animation = fade, 1, 2.5, md3_decel
animation = workspaces, 1, 3.5, easeOutExpo, slide
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}
-23
View File
@@ -1,23 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "High"
# credit https://github.com/mylinuxforwork/dotfiles
# -----------------------------------------------------
animations {
enabled = true
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}
-32
View File
@@ -1,32 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Ja"
# credit https://github.com/JaKooLit/Hyprland-Dots
# -----------------------------------------------------
animations {
enabled = yes
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = overshot, 0.05, 0.9, 0.1, 1.05
bezier = smoothOut, 0.5, 0, 0.99, 0.99
bezier = smoothIn, 0.5, -0.5, 0.68, 1.5
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 5, winIn, slide
animation = windowsOut, 1, 3, smoothOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
#animation = borderangle, 1, 180, liner, loop #used by rainbow borders and rotating colors
animation = fade, 1, 3, smoothOut
animation = workspaces, 1, 5, overshot
# animations for -git or version >0.42.0
animation = workspacesIn, 1, 5, winIn, slide
animation = workspacesOut, 1, 5, winOut, slide
}
-58
View File
@@ -1,58 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Me-1"
# credit https://github.com/mahaveergurjar
# -----------------------------------------------------
animations {
enabled = true
# Animation curves
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
# bezier = linear, 0, 0, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = menu_decel, 0.1, 1, 0, 1
bezier = menu_accel, 0.38, 0.04, 1, 0.07
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
bezier = softAcDecel, 0.26, 0.26, 0.15, 1
bezier = md2, 0.4, 0, 0.2, 1 # use with .2s duration
# Animation configs
# animation = windows, 1, 3, md3_decel, popin 60%
# animation = windowsIn, 1, 3, md3_decel, popin 60%
# animation = windowsOut, 1, 3, md3_accel, popin 60%
# animation = windows, 1, 6, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
# animation = border, 1, 10, default
animation = fade, 1, 3, md3_decel
# animation = layers, 1, 2, md3_decel, slide
animation = layersIn, 1, 3, menu_decel, slide
animation = layersOut, 1, 1.6, menu_accel
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
animation = workspaces, 1, 5, wind
# animation = workspaces, 1, 2.5, softAcDecel, slide
# animation = workspaces, 1, 7, menu_decel, slidefade 15%
animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}
-50
View File
@@ -1,50 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Me-2"
# credit https://github.com/mahaveergurjar
# -----------------------------------------------------
animations {
enabled = true
# Animation curves
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = md3_standard, 0.2, 0, 0, 1
bezier = md3_decel, 0.05, 0.7, 0.1, 1
bezier = md3_accel, 0.3, 0, 0.8, 0.15
bezier = overshot, 0.05, 0.9, 0.1, 1.1
bezier = crazyshot, 0.1, 1.5, 0.76, 0.92
bezier = hyprnostretch, 0.05, 0.9, 0.1, 1.0
bezier = menu_decel, 0.1, 1, 0, 1
bezier = menu_accel, 0.38, 0.04, 1, 0.07
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutExpo, 0.16, 1, 0.3, 1
bezier = softAcDecel, 0.26, 0.26, 0.15, 1
bezier = md2, 0.4, 0, 0.2, 1 # use with .2s duration
bezier = OutBack, 0.34, 1.56, 0.64, 1
bezier = easeInOutCirc, 0.85, 0, 0.15, 1
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
# animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
# animation = windowsOut, 1, 5, winOut, slide
animation = windows, 1, 5, easeInOutCirc
animation = windowsOut, 1, 5, OutBack
animation = windowsMove, 1, 5, wind, slide
animation = fade, 1, 3, md3_decel
animation = layersIn, 1, 3, menu_decel, slide
animation = layersOut, 1, 1.6, menu_accel
animation = fadeLayersIn, 1, 2, menu_decel
animation = fadeLayersOut, 1, 4.5, menu_accel
animation = workspaces, 1, 7, menu_decel, slide
animation = workspaces, 1, 5, wind
animation = specialWorkspace, 1, 3, md3_decel, slidefadevert 15%
animation = specialWorkspace, 1, 3, md3_decel, slidevert
}
@@ -1,29 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Minimal-1"
# -----------------------------------------------------
animations {
enabled = true
# █▄▄ █▀▀ ▀█ █ █▀▀ █▀█   █▀▀ █░█ █▀█ █░█ █▀▀
# █▄█ ██▄ █▄ █ ██▄ █▀▄   █▄▄ █▄█ █▀▄ ▀▄▀ ██▄
bezier = wind, 0.05, 0.9, 0.1, 1.05
bezier = winIn, 0.1, 1.1, 0.1, 1.1
bezier = winOut, 0.3, -0.3, 0, 1
bezier = liner, 1, 1, 1, 1
#▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
#█▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
animation = windows, 1, 6, wind, slide
animation = windowsIn, 1, 6, winIn, slide
animation = windowsOut, 1, 5, winOut, slide
animation = windowsMove, 1, 5, wind, slide
animation = border, 1, 1, liner
animation = borderangle, 1, 30, liner, loop
animation = fade, 1, 10, default
animation = workspaces, 1, 5, wind
}
@@ -1,18 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Minimal-2"
# -----------------------------------------------------
animations {
enabled = yes
bezier = quart, 0.25, 1, 0.5, 1
animation = windows, 1, 6, quart, slide
animation = border, 1, 6, quart
animation = borderangle, 1, 6, quart
animation = fade, 1, 6, quart
animation = workspaces, 1, 6, quart
}
-22
View File
@@ -1,22 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Moving"
# credit https://github.com/mylinuxforwork/dotfiles
# -----------------------------------------------------
animations {
enabled = true
bezier = overshot, 0.05, 0.9, 0.1, 1.05
bezier = smoothOut, 0.5, 0, 0.99, 0.99
bezier = smoothIn, 0.5, -0.5, 0.68, 1.5
animation = windows, 1, 5, overshot, slide
animation = windowsOut, 1, 3, smoothOut
animation = windowsIn, 1, 3, smoothOut
animation = windowsMove, 1, 4, smoothIn, slide
animation = border, 1, 5, default
animation = fade, 1, 5, smoothIn
animation = fadeDim, 1, 5, smoothIn
animation = workspaces, 1, 6, default
}
@@ -1,43 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Optimized"
# -----------------------------------------------------
animations {
enabled = true
bezier = wind, 0.05, 0.85, 0.03, 0.97
bezier = winIn, 0.07, 0.88, 0.04, 0.99
bezier = winOut, 0.20, -0.15, 0, 1
bezier = liner, 1, 1, 1, 1
bezier = md3_standard, 0.12, 0, 0, 1
bezier = md3_decel, 0.05, 0.80, 0.10, 0.97
bezier = md3_accel, 0.20, 0, 0.80, 0.08
bezier = overshot, 0.05, 0.85, 0.07, 1.04
bezier = crazyshot, 0.1, 1.22, 0.68, 0.98
bezier = hyprnostretch, 0.05, 0.82, 0.03, 0.94
bezier = menu_decel, 0.05, 0.82, 0, 1
bezier = menu_accel, 0.20, 0, 0.82, 0.10
bezier = easeInOutCirc, 0.75, 0, 0.15, 1
bezier = easeOutCirc, 0, 0.48, 0.38, 1
bezier = easeOutExpo, 0.10, 0.94, 0.23, 0.98
bezier = softAcDecel, 0.20, 0.20, 0.15, 1
bezier = md2, 0.30, 0, 0.15, 1
bezier = OutBack, 0.28, 1.40, 0.58, 1
bezier = easeInOutCirc, 0.78, 0, 0.15, 1
animation = border, 1, 1.6, liner
animation = borderangle, 1, 82, liner, loop
animation = windowsIn, 1, 3.2, winIn, slide
animation = windowsOut, 1, 2.8, easeOutCirc
animation = windowsMove, 1, 3.0, wind, slide
animation = fade, 1, 1.8, md3_decel
animation = layersIn, 1, 1.8, menu_decel, slide
animation = layersOut, 1, 1.5, menu_accel
animation = fadeLayersIn, 1, 1.6, menu_decel
animation = fadeLayersOut, 1, 1.8, menu_accel
animation = workspaces, 1, 4.0, menu_decel, slide
animation = specialWorkspace, 1, 2.3, md3_decel, slidefadevert 15%
}
-18
View File
@@ -1,18 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Standard"
# credit https://github.com/mylinuxforwork/dotfiles
# -----------------------------------------------------
animations {
enabled = true
bezier = myBezier, 0.05, 0.9, 0.1, 1.05
animation = windows, 1, 7, myBezier
animation = windowsOut, 1, 7, default, popin 80%
animation = border, 1, 10, default
animation = borderangle, 1, 8, default
animation = fade, 1, 7, default
animation = workspaces, 1, 6, default
}
-34
View File
@@ -1,34 +0,0 @@
# -----------------------------------------------------
# ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█
# █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█
#
# name "Vertical"
# -----------------------------------------------------
animations {
bezier = fluent_decel, 0, 0.2, 0.4, 1
bezier = easeOutCirc, 0, 0.55, 0.45, 1
bezier = easeOutCubic, 0.33, 1, 0.68, 1
bezier = easeinoutsine, 0.37, 0, 0.63, 1
# Windows
animation = windowsIn, 1, 1.5, easeinoutsine, popin 60% # window open
animation = windowsOut, 1, 1.5, easeOutCubic, popin 60% # window close.
animation = windowsMove, 1, 1.5, easeinoutsine, slide # everything in between, moving, dragging, resizing.
# Fading
animation = fade, 1, 2.5, fluent_decel
animation = fadeLayersIn, 0
animation = border, 0
# Layers
animation = layers, 1, 1.5, easeinoutsine, popin
# Workspaces
#animation = workspaces, 1, 3, fluent_decel, slidefade 30% # styles: slide, slidevert, fade, slidefade, slidefadevert
animation = workspaces, 1, 3, fluent_decel, slidefadevert 30% # styles: slide, slidevert, fade, slidefade, slidefadevert
animation = specialWorkspace, 1, 2, fluent_decel, slidefade 10%
}
+263
View File
@@ -0,0 +1,263 @@
-- gui package — the dots keybinds, split out of ./hyprland.lua. Required there
-- after the app/start overrides (the launchers read hc.app.*) and before
-- ./host.lua.
local hc = hyde.config
local MOD = hc.modifiers.main
local home = os.getenv("HOME") or ""
-- ── Keybinds ─────────────────────────────────────────────────────────────────
-- Policy: preserve what the dots keybindings.conf did live until the port —
-- where a dots bind and a HyDE bind share a combo, the dots action wins and
-- the displaced HyDE action is noted. Binds identical in both are not
-- repeated here (volume/media/brightness, screenshots, Super+Q/W,
-- rofi menus, Super+L lock, Super+Shift+F pin, Super+S scratchpad family,
-- Super+Z/X + mouse drag/resize, Super+J togglesplit, Super+T/E/B via the
-- app.* overrides in hyprland.lua, Super+Alt+G gamemode, Ctrl+Shift+Esc
-- sysmon).
-- Removes every live bind on a combo regardless of flags. hl.bind's dedup only
-- replaces a bind whose flags match exactly; HyDE binds with locked/transparent/
-- repeating flags would otherwise stay live next to ours and double-fire.
local function unbind_all(combo)
-- _active/canonicalize are HyDE-private; if an update renames them, degrade
-- to "the displaced HyDE binds double-fire" instead of erroring here and
-- silently dropping every bind registered below this line.
if not (hyde.binds and hyde.binds._active and hyde.binds.canonicalize) then
return
end
local canon = hyde.binds.canonicalize(combo)
for id, registered in pairs(hyde.binds._active) do
if id:sub(1, #canon + 1) == canon .. "|" then
hl.unbind(registered)
hyde.binds._active[id] = nil
end
end
end
local _F
-- Launchers (dots-only combos, no conflict)
_F = { description = "[Launcher|Apps] work session (kitty c2)" }
hl.bind(MOD .. " + CTRL + I", hl.dsp.exec_cmd("kitty --session " .. home .. "/.config/kitty/c2.conf"), _F)
_F = { description = "[Launcher|Apps] work session (kitty runners)" }
hl.bind(MOD .. " + CTRL + O", hl.dsp.exec_cmd("kitty --session " .. home .. "/.config/kitty/runners.conf"), _F)
_F = { description = "[Launcher|Apps] pyradio (kitty music)" }
hl.bind(MOD .. " + R", hl.dsp.exec_cmd("kitty --session " .. home .. "/.config/kitty/music.conf"), _F)
_F = { description = "[Launcher|Apps] ferdium" }
hl.bind(MOD .. " + Y", hl.dsp.exec_cmd("ferdium"), _F)
_F = { description = "[Launcher|Apps] betterbird" }
hl.bind(MOD .. " + M", hl.dsp.exec_cmd("betterbird"), _F)
_F = { description = "[Launcher|Apps] freetube" }
hl.bind(MOD .. " + F", hl.dsp.exec_cmd("freetube"), _F)
_F = { description = "[Launcher|Apps] web browser (librewolf)" }
hl.bind(MOD .. " + SHIFT + B", hl.dsp.exec_cmd("librewolf"), _F)
_F = { description = "[Launcher|Apps] terminal emulator (st)" }
hl.bind(MOD .. " + CTRL + T", hl.dsp.exec_cmd("st"), _F)
_F = { description = "[Launcher|Apps] terminal emulator" }
hl.bind("CTRL + ALT + T", hl.dsp.exec_cmd(hc.app.terminal), _F)
-- Rebound fresh so the hc.app.* overrides actually apply: HyDE's binds
-- evaluated _apps.browser/_apps.explorer when key_binds.lua ran, baking in the
-- hyde-shell open wrappers.
_F = { description = "[Launcher|Apps] web browser" }
hl.bind(MOD .. " + B", hl.dsp.exec_cmd("zen-browser"), _F)
_F = { description = "[Launcher|Apps] file explorer" }
hl.bind(MOD .. " + E", hl.dsp.exec_cmd("dolphin"), _F)
-- Conflict resolutions (dots action wins; displaced HyDE action noted)
_F = { description = "[Launcher|Apps] web browser (firefox)" } -- displaces HyDE: hide waybar
hl.bind(MOD .. " + CTRL + B", hl.dsp.exec_cmd("firefox"), _F)
_F = { description = "[Launcher|Apps] xmpp (gajim)" } -- displaces HyDE: toggle group (dots keeps it on Super+Shift+G)
hl.bind(MOD .. " + G", hl.dsp.exec_cmd("gajim"), _F)
_F = { description = "[Window Management] toggle group" } -- displaces HyDE: game launcher
hl.bind(MOD .. " + SHIFT + G", hl.dsp.group.toggle(), _F)
_F = { description = "[Launcher|Apps] terminal emulator (alacritty)" } -- displaces HyDE: theme select menu
hl.bind(MOD .. " + SHIFT + T", hl.dsp.exec_cmd("alacritty"), _F)
-- Through `wbar`, never killall: under supervision a killed waybar is back in
-- seconds, and a raw relaunch next to it means two bars.
_F = { description = "[Theming and Wallpaper] reload hyprland + waybar" } -- displaces HyDE: wallbash mode menu
hl.bind(MOD .. " + SHIFT + R", hl.dsp.exec_cmd("hyprctl reload; " .. home .. "/.local/bin/wbar restart"), _F)
_F = { description = "[Launcher|Apps] neovim" } -- displaces HyDE: code editor
hl.bind(MOD .. " + C", hl.dsp.exec_cmd("kitty nvim"), _F)
-- Super+K keepassxc — HyDE's keyboard-layout switch carries locked=true, so a
-- plain rebind would leave both live (flags are part of a bind's identity).
unbind_all(MOD .. " + K") -- displaces HyDE: keyboard layout switch (locked)
_F = { description = "[Launcher|Apps] password manager" }
hl.bind(MOD .. " + K", hl.dsp.exec_cmd("keepassxc"), _F)
-- Alt+Tab focus-down — HyDE ships a 4-bind alt-tab switcher (transparent/
-- release flags); remove the whole cluster, half a switcher is worse than none.
unbind_all("ALT + TAB") -- displaces HyDE: alt-tab switcher (cycle next)
unbind_all("ALT + SHIFT + TAB")
unbind_all("ALT + ALT_L")
unbind_all("ALT + ALT_R")
_F = { description = "[Window Management|Change focus] focus down" }
hl.bind("ALT + TAB", hl.dsp.focus({ direction = "down" }), _F)
-- Waybar toggle (custom layout; the legacy Ctrl+Alt+W). systemctl stop is a
-- deliberate stop, so Restart=always does not undo it.
_F = { description = "[Window Management] toggle waybar" }
hl.bind("CTRL + ALT + W", hl.dsp.exec_cmd(home .. "/.local/bin/wbar toggle"), _F)
-- Fullscreen on Alt+Return (Shift+F11 stays HyDE's cycle-fullscreen).
-- Native dispatcher, not exec "hyprctl dispatch fullscreen": in Lua mode that
-- CLI arg is evaluated as hl.dispatch(fullscreen) — a nil global, dead bind.
_F = { description = "[Window Management] toggle fullscreen" }
hl.bind("ALT + RETURN", hl.dsp.window.fullscreen(), _F)
-- No Super+Delete: HyDE's exit() ends the session on the spot, no confirm, and
-- the combo is one slipped finger from Super+Alt+Delete. Ctrl+Alt+Delete (the
-- logout menu) stays the way out.
unbind_all(MOD .. " + Delete") -- displaces HyDE: exit hyprland session
-- ── Focus (arrows + hjkl) ────────────────────────────────────────────────────
-- Super+Ctrl+Up/Down: movefocus — displaces HyDE: nearest-empty (Down), which
-- moves to Super+Ctrl+E below. Super+Ctrl+Left/Right move the window to r±1
-- (Workspaces below); r±1 nav is on Super+Left/Right and Super+Ctrl+Shift.
for dir, key in pairs({ left = "LEFT", right = "RIGHT", up = "UP", down = "DOWN" }) do
_F = { description = "[Window Management|Change focus] focus " .. dir }
if key == "UP" or key == "DOWN" then
hl.bind(MOD .. " + CTRL + " .. key, hl.dsp.focus({ direction = dir }), _F)
end
hl.bind("ALT + CTRL + " .. key, hl.dsp.focus({ direction = dir }), _F)
end
for dir, key in pairs({ left = "H", right = "L", up = "K", down = "J" }) do
_F = { description = "[Window Management|Change focus] focus " .. dir }
hl.bind("ALT + CTRL + " .. key, hl.dsp.focus({ direction = dir }), _F)
end
-- Super+Ctrl+H/L carried a deliberate double action in the hyprlang chain:
-- changegroupactive (a no-op outside a group) AND movefocus fired together.
-- Lua dedup would collapse the pair, so one function does both.
-- (Replaces HyDE's plain group-nav binds on the same combos.) Unlike the
-- arrows, they do not move the window to r±1 — that is Super+Ctrl+Alt+H/L.
_F = { description = "[Window Management|Change focus] group prev + focus left" }
hl.bind(MOD .. " + CTRL + H", function()
hl.dispatch(hl.dsp.group.prev())
hl.dispatch(hl.dsp.focus({ direction = "left" }))
end, _F)
_F = { description = "[Window Management|Change focus] group next + focus right" }
hl.bind(MOD .. " + CTRL + L", function()
hl.dispatch(hl.dsp.group.next())
hl.dispatch(hl.dsp.focus({ direction = "right" }))
end, _F)
_F = { description = "[Window Management|Change focus] focus up" }
hl.bind(MOD .. " + CTRL + K", hl.dsp.focus({ direction = "up" }), _F)
_F = { description = "[Window Management|Change focus] focus down" }
hl.bind(MOD .. " + CTRL + J", hl.dsp.focus({ direction = "down" }), _F)
-- ── Resize (add hjkl to HyDE's arrow set) ────────────────────────────────────
-- HyDE puts a flagless calculator menu on Super+Shift+K; our repeating resize
-- would double-fire next to it (flag mismatch), so drop the calculator.
unbind_all(MOD .. " + SHIFT + K") -- displaces HyDE: calculator menu
for _, m in ipairs({ { "L", 30, 0 }, { "H", -30, 0 }, { "K", 0, -30 }, { "J", 0, 30 } }) do
_F = { description = "[Window Management|Resize Active Window] resize window", repeating = true }
hl.bind(MOD .. " + SHIFT + " .. m[1], hl.dsp.window.resize({ x = m[2], y = m[3], relative = true }), _F)
end
-- ── Move active window (Super+Alt, float-aware: moveactive / swapwindow) ─────
-- Displaces HyDE: wallpaper next/prev (Alt+Right/Left) and waybar layout
-- next/prev (Alt+Up/Down); wallpaper moves to Super+Alt+Ctrl+Shift below,
-- as in the dots chain. HyDE's binds carry no flags but ours are repeating,
-- so an explicit unbind is needed to keep them from double-firing.
unbind_all(MOD .. " + ALT + RIGHT")
unbind_all(MOD .. " + ALT + LEFT")
unbind_all(MOD .. " + ALT + UP")
unbind_all(MOD .. " + ALT + DOWN")
local function dots_move_window(dir, pix)
local lut = { l = { -1, 0 }, r = { 1, 0 }, u = { 0, -1 }, d = { 0, 1 } }
local m = lut[dir]
return function()
local w = hl.get_active_window()
if w and w.floating then
hl.dispatch(hl.dsp.window.move({ x = m[1] * pix, y = m[2] * pix, relative = true }))
else
local names = { l = "left", r = "right", u = "up", d = "down" }
hl.dispatch(hl.dsp.window.swap({ direction = names[dir] }))
end
end
end
for _, m in ipairs({
{ "LEFT", "l" },
{ "RIGHT", "r" },
{ "UP", "u" },
{ "DOWN", "d" },
{ "H", "l" },
{ "L", "r" },
{ "K", "u" },
{ "J", "d" },
}) do
_F = { description = "[Window Management|Move active window] move " .. m[2], repeating = true }
hl.bind(MOD .. " + ALT + " .. m[1], dots_move_window(m[2], 30), _F)
end
-- ── Wallpaper (moved off Super+Alt by the dots chain) ────────────────────────
_F = { description = "[Theming and Wallpaper] next global wallpaper" }
hl.bind(MOD .. " + ALT + CTRL + SHIFT + RIGHT", hl.dsp.exec_cmd(hyde.sh.wallpaper("--next")), _F)
_F = { description = "[Theming and Wallpaper] previous global wallpaper" }
hl.bind(MOD .. " + ALT + CTRL + SHIFT + LEFT", hl.dsp.exec_cmd(hyde.sh.wallpaper("--prev")), _F)
-- ── Workspaces ───────────────────────────────────────────────────────────────
-- r~N = Nth workspace of the FOCUSED monitor — load-bearing on the 3-monitor
-- workspace sets (1-10 / 11-20 / 21-30 pinned per monitor in host.lua).
-- HyDE's own binds pass "r+1"/"empty" strings the same way; r~N is the one
-- thing to verify first in the live session.
for i = 1, 10 do
local key = (i == 10) and 0 or i
_F = { description = "[Workspaces|Navigation] navigate to workspace " .. i .. " (monitor-relative)" }
hl.bind(MOD .. " + " .. key, hl.dsp.focus({ workspace = "r~" .. i }), _F) -- displaces HyDE: absolute workspace N
_F = { description = "[Workspaces|Move window to workspace] move to workspace " .. i .. " (monitor-relative)" }
hl.bind(MOD .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = "r~" .. i }), _F)
_F = { description = "[Workspaces|Move window silently] move to workspace " .. i .. " (monitor-relative)" }
hl.bind(MOD .. " + ALT + " .. key, hl.dsp.window.move({ workspace = "r~" .. i, follow = false }), _F) -- displaces HyDE: absolute silent move
end
-- Relative workspace nav on Super+Ctrl+Shift (dots), displacing HyDE's
-- move-window-in-workspace family there (repeating flags -> explicit unbind;
-- dots does window movement on Super+Alt instead).
unbind_all(MOD .. " + SHIFT + CTRL + LEFT")
unbind_all(MOD .. " + SHIFT + CTRL + RIGHT")
unbind_all(MOD .. " + SHIFT + CTRL + UP")
unbind_all(MOD .. " + SHIFT + CTRL + DOWN")
_F = { description = "[Workspaces|Navigation|Relative workspace] change active workspace forwards" }
hl.bind(MOD .. " + CTRL + SHIFT + RIGHT", hl.dsp.focus({ workspace = "r+1" }), _F)
hl.bind(MOD .. " + CTRL + SHIFT + L", hl.dsp.focus({ workspace = "r+1" }), _F)
_F = { description = "[Workspaces|Navigation|Relative workspace] change active workspace backwards" }
hl.bind(MOD .. " + CTRL + SHIFT + LEFT", hl.dsp.focus({ workspace = "r-1" }), _F)
hl.bind(MOD .. " + CTRL + SHIFT + H", hl.dsp.focus({ workspace = "r-1" }), _F)
_F = { description = "[Workspaces|Navigation|Relative workspace] change active workspace forwards" }
hl.bind(MOD .. " + RIGHT", hl.dsp.focus({ workspace = "r+1" }), _F)
_F = { description = "[Workspaces|Navigation|Relative workspace] change active workspace backwards" }
hl.bind(MOD .. " + LEFT", hl.dsp.focus({ workspace = "r-1" }), _F)
_F = { description = "[Workspaces|Move window to workspace|Relative workspace] move focused window to next workspace" }
hl.bind(MOD .. " + CTRL + RIGHT", hl.dsp.window.move({ workspace = "r+1" }), _F) -- displaces HyDE: workspace r+1
_F = {
description = "[Workspaces|Move window to workspace|Relative workspace] move focused window to previous workspace",
}
hl.bind(MOD .. " + CTRL + LEFT", hl.dsp.window.move({ workspace = "r-1" }), _F) -- displaces HyDE: workspace r-1
-- E for "empty" — Super+Ctrl+Down is taken by movefocus above
_F = { description = "[Workspaces|Navigation] navigate to the nearest empty workspace" }
hl.bind(MOD .. " + CTRL + E", hl.dsp.focus({ workspace = "empty" }), _F)
-- Move window to relative workspace: HyDE ships the arrow pair; add hjkl.
_F = { description = "[Workspaces|Move window to workspace|Relative workspace] move focused window to next workspace" }
hl.bind(MOD .. " + CTRL + ALT + L", hl.dsp.window.move({ workspace = "r+1" }), _F)
_F = {
description = "[Workspaces|Move window to workspace|Relative workspace] move focused window to previous workspace",
}
hl.bind(MOD .. " + CTRL + ALT + H", hl.dsp.window.move({ workspace = "r-1" }), _F)
-- ── Hardware controls ────────────────────────────────────────────────────────
-- HyDE binds bare F10/F11/F12 to mute / volume down / volume up (key_binds.lua,
-- locked + repeating -- a plain rebind would leave both live). That is a laptop
-- F-row assumption: the Cantor has no F-row, so F1-F12 only ever arrive from its
-- Vial layer 5 and have to reach applications instead of the mixer. The
-- XF86Audio* binds below them keep the media keys working, so nothing is lost,
-- and Shift+F11 (cycle fullscreen) canonicalizes separately and survives.
unbind_all("F10") -- displaces HyDE: un/mute output
unbind_all("F11") -- displaces HyDE: decrease volume
unbind_all("F12") -- displaces HyDE: increase volume
+122
View File
@@ -0,0 +1,122 @@
-- gui package — the dots layer + window rules, split out of ./hyprland.lua
-- (required there in their original position). Plain hl.* registrations;
-- nothing here depends on the rest of the layer.
-- ── Layer rules ──────────────────────────────────────────────────────────────
-- waybar blur ships upstream (layer_rules.lua), but its ignore_alpha uses the
-- boolean form (default threshold), which drops the blur on low-alpha regions
-- — the cavabar background is rgba(0,0,0,0.2) and went flat. The legacy chain
-- set the threshold to 0 (blur everything) on both bars; restore that exactly.
hl.layer_rule({
name = "dots_cavabar_blur",
match = { namespace = "cavabar" },
blur = true,
ignore_alpha = 0,
})
hl.layer_rule({
name = "dots_waybar_ignore_alpha",
match = { namespace = "waybar" },
ignore_alpha = 0,
})
-- ── Window rules ─────────────────────────────────────────────────────────────
-- Upstream's window_rules.lua consolidates most floats (dialogs, portals,
-- dolphin, nm-*, qt*ct, vlc, pavucontrol, …) and the PiP/modal handling.
-- Ported here: everything the repo's windowrules.conf had that upstream
-- dropped when it went global-opacity — per-app opacity, idle_inhibit,
-- jetbrains popups — plus the floats upstream does not cover.
-- idle_inhibit while fullscreen (media players, browsers)
hl.window_rule({
name = "dots_idle_inhibit_media",
match = { class = "^(.*celluloid.*)$|^(.*mpv.*)$|^(.*vlc.*)$|^(.*[Ss]potify.*)$" },
idle_inhibit = "fullscreen",
})
hl.window_rule({
name = "dots_idle_inhibit_browsers",
match = {
class = "^(.*LibreWolf.*)$|^(.*floorp.*)$|^(.*brave-browser.*)$|^(.*firefox.*)$|^(.*chromium.*)$|^(.*zen.*)$|^(.*vivaldi.*)$",
},
idle_inhibit = "fullscreen",
})
-- per-app opacity ("active inactive fullscreen", hyprlang-style value string --
-- the Lua rule's opacity field is string-typed and takes the whole triple)
hl.window_rule({
name = "dots_opacity_browsers",
match = { class = "^(firefox)$|^(zen)$|^(brave-browser)$" },
opacity = "0.90 0.90 1",
})
hl.window_rule({
name = "dots_opacity_editors_terms",
match = {
class = "^(code-oss)$|^([Cc]ode)$|^(code-url-handler)$|^(code-insiders-url-handler)$|^(kitty)$|^(org.kde.dolphin)$|^(org.kde.ark)$|^(nwg-look)$|^(qt5ct)$|^(qt6ct)$|^(kvantummanager)$",
},
opacity = "0.80 0.80 1",
})
hl.window_rule({
name = "dots_opacity_applets",
match = {
class = "^(org.pulseaudio.pavucontrol)$|^(blueman-manager)$|^(nm-applet)$|^(nm-connection-editor)$|^(hyprpolkitagent)$|^(org.freedesktop.impl.portal.desktop.gtk)$|^(org.freedesktop.impl.portal.desktop.hyprland)$",
},
opacity = "0.80 0.70 1",
})
hl.window_rule({
name = "dots_opacity_spotify_class",
match = { class = "^([Ss]potify)$" },
opacity = "0.70 0.70 1",
})
hl.window_rule({
name = "dots_opacity_spotify_title",
match = { initial_title = "^(Spotify Free)$|^(Spotify Premium)$" },
opacity = "0.70 0.70 1",
})
hl.window_rule({
name = "dots_opacity_blender",
match = { class = "^(blender)$" },
opacity = "1.00 1.00 1",
})
hl.window_rule({
name = "dots_opacity_gtk_apps",
match = { class = "^(com.github.rafostar.Clapper)$" },
opacity = "0.90 0.90",
})
hl.window_rule({
name = "dots_opacity_misc_apps",
match = {
class = "^(com.github.tchx84.Flatseal)$|^(hu.kramo.Cartridges)$|^(com.obsproject.Studio)$|^(gnome-boxes)$|^(vesktop)$|^(discord)$|^(WebCord)$|^(ArmCord)$|^(app.drey.Warp)$|^(net.davidotek.pupgui2)$|^(yad)$|^(Signal)$|^(io.github.alainm23.planify)$|^(io.gitlab.theevilskeleton.Upscaler)$|^(com.github.unrud.VideoDownloader)$|^(io.gitlab.adhami3310.Impression)$|^(io.missioncenter.MissionCenter)$|^(io.github.flattool.Warehouse)$",
},
opacity = "0.80 0.80",
})
-- floats upstream's consolidated list does not cover
hl.window_rule({
name = "dots_float_class",
match = {
class = "^(Signal)$|^(com.github.rafostar.Clapper)$|^(app.drey.Warp)$|^(net.davidotek.pupgui2)$|^(yad)$|^(eog)$|^(io.github.alainm23.planify)$|^(io.gitlab.theevilskeleton.Upscaler)$|^(com.github.unrud.VideoDownloader)$|^(io.gitlab.adhami3310.Impression)$|^(io.missioncenter.MissionCenter)$",
},
float = true,
})
hl.window_rule({
name = "dots_float_title",
match = { title = "^(About Mozilla Firefox)$|^(Ghidra: NO ACTIVE PROJECT.*)$" },
float = true,
})
hl.window_rule({
name = "dots_float_kitty_monitors",
match = { class = "^(kitty)$", title = "^(top)$|^(btop)$|^(htop)$" },
float = true,
})
hl.window_rule({
name = "dots_blender_image_editor",
match = { class = "^(blender)$", initial_title = "^(Image Editor)$" },
float = true,
size = "(monitor_w*0.5) (monitor_h*0.5)",
})
-- jetbrains dropdowns/popups flicker workaround
hl.window_rule({
name = "dots_jetbrains_popups",
match = { class = "^(.*jetbrains.*)$", title = "^(win[0-9]+)$" },
no_initial_focus = true,
})
-4
View File
@@ -1,4 +0,0 @@
# Default (empty) per-host extras — fallback shared by all GUI hosts.
# Real host.conf comes from the per-host overlay (e.g. wm/ sources nvidia.conf here).
# Present so `source = ./host.conf` in hyprland.conf always resolves, even mid-install
# before the host overlay is stowed (avoids the transient "source globbing error").
-154
View File
@@ -1,154 +0,0 @@
#!   ░▒▒▒░░░▓▓          ___________
#! ░░▒▒▒░░░░░▓▓        //___________/
#! ░░▒▒▒░░░░░▓▓     _   _ _    _ _____
#! ░░▒▒░░░░░▓▓▓▓▓▓ | | | | |  | |  __/
#! ░▒▒░░░░▓▓   ▓▓ | |_| | |_/ /| |___
#!  ░▒▒░░▓▓   ▓▓   \__  |____/ |____/
#!    ░▒▓▓   ▓▓  //____/
# Use this for reference to override the default HyDE' hyprland configuration
# ! Never source ~/.config/hypr/hyde.conf directly, it is sourced by the main configuration file
# This file acts as an override configuration for the user to set their environment variables and startup commands
# Static variable declaration in hyde.conf will be prioritized over the default and dynamic configuration
#  NOTE 
# Leaving the variable empty will unset the variable
# Commenting out the variable will use the default value
# For updated configuration options, see https://github.com/HyDE-Project/HyDE/blob/master/Configs/.config/hypr/hyde.conf
# For simplicity, ./hyde.conf ONLY accepts $ for variables and # for comments, will sanitize the file to remove any other characters
# $start.VAR , $env.VAR are ONLY HyDE specific conventions for consistency.
# Keyboard modifier
# $mainMod = SUPER # windows key
# ▄▀█ █▀█ █▀█ █▀
# █▀█ █▀▀ █▀▀ ▄█
# $QUICKAPPS = # used for quick app launcher
# Leaving this to HyDE means Super+B runs its `$default.BROWSER`, which is
# `hyde-launch.sh --fall firefox web-browser` — and the 26.x update deprecated that script, so
# every press fired a "hyde-launch.sh is deprecated, please use hyde-shell open instead"
# notification. Naming the browser directly skips the wrapper entirely. This is the primary
# browser only; Super+Shift+B (librewolf) and Super+Ctrl+B (firefox) are set in keybindings.conf.
# `hyde-shell open web-browser` is the modern HyDE equivalent if the xdg default should decide.
$BROWSER = zen-browser # matches the xdg default (zen.desktop)
# $EDITOR = code # default editor, if commented out , will use the default editor
# Same deprecation as $BROWSER above: HyDE's default is `hyde-launch.sh --fall dolphin
# file-manager`, and Super+E in keybindings.conf uses it. $EDITOR is deliberately left to HyDE —
# no active bind reads it (Super+C runs `$TERMINAL nvim`), so there is nothing to fix there.
$EXPLORER = dolphin # matches the xdg default (org.kde.dolphin.desktop)
# $TERMINAL = kitty # default terminal, if commented out , will use the org.gnome.desktop.default-applications.terminal
# $LOCKSCREEN=hyprlock # default lockscreen, you can use any lockscreen you want, eg swaylock
# $IDLE=hypridle # default idle manager, you can use any idle manager you want,eg swayidle
# // █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█
# // █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█
# See https://wiki.hyprland.org/Configuring/Keywords/
# Override the default startup commands
# $start.XDG_PORTAL_RESET=$scrPath/resetxdgportal.sh
# $start.DBUS_SHARE_PICKER=dbus-update-activation-environment --systemd --all # for XDPH
# $start.SYSTEMD_SHARE_PICKER=systemctl --user import-environment QT_QPA_PLATFORMTHEME WAYLAND_DISPLAY XDG_CURRENT_DESKTOP # for XDPH
# Empty = unset, so HyDE's default `exec-once = $start.BAR` (a bare `waybar`, using HyDE's
# generated config.jsonc/style.css) does not launch a second bar. The bar we actually want is
# launched from userprefs.conf with the custom layout/style.
$start.BAR=
# $start.NOTIFICATIONS=swaync # dunst
# $start.APPTRAY_BLUETOOTH=blueman-applet
# $start.WALLPAPER=$scrPath/wallpaper.sh --global # 26.x: backend-agnostic (awww); swwwallpaper.sh is gone
# $start.TEXT_CLIPBOARD=wl-paste --type text --watch cliphist store
# $start.IMAGE_CLIPBOARD=wl-paste --type image --watch cliphist store
# $start.BATTERY_NOTIFY=$scrPath/batterynotify.sh
# Kept explicitly at the default: nm-applet's tray icon is the one place with a quick
# connect/disconnect MENU (wifi list, VPN) — the bar's `network` module only shows state
# and bandwidth. Tela renders it as a blue circle (its scalable/devices icon); that's the
# theme working, not a bug. Was briefly unset 2026-08-14 — wrong call, the menu was wanted.
$start.NETWORK_MANAGER=nm-applet --indicator
# $start.REMOVABLE_MEDIA=udiskie --no-automount --smart-tray
# $start.AUTH_DIALOGUE=$scrPath/polkitkdeauth.sh
# $start.IDLE_DAEMON=$IDLE
# // █▀▀ █▄░█ █░█
# // ██▄ █░▀█ ▀▄▀
# See https://wiki.hyprland.org/Configuring/Environment-variables/
# Override the default environment variables
# # Toolkit Backend Variables - https://wiki.hyprland.org/Configuring/Environment-variables/#toolkit-backend-variables
# $env.GDK_BACKEND = wayland,x11,* #s GTK: Use wayland if available. If not: try x11, then any other GDK backend.
# # $env.QT_QPA_PLATFORM = wayland;xcb #Qt: Use wayland if available, fall back to x11 if not.
# $env.SDL_VIDEODRIVER = wayland #s Run SDL2 applications on Wayland. Remove or set to x11 if games that provide older versions of SDL cause compatibility issues
# $env.CLUTTER_BACKEND = wayland #s Clutter package already has wayland enabled, this variable will force Clutter applications to try and use the Wayland backend
# # XDG Specifications - https://wiki.hyprland.org/Configuring/Environment-variables/#xdg-specifications
# $env.XDG_CURRENT_DESKTOP = Hyprland
# $env.XDG_SESSION_TYPE = wayland
# $env.XDG_SESSION_DESKTOP = Hyprland
# # Qt Variables - https://wiki.hyprland.org/Configuring/Environment-variables/#qt-variables
# $env.QT_AUTO_SCREEN_SCALE_FACTOR = 1 # (From the Qt documentation) enables automatic scaling, based on the monitor’s pixel density
# $env.QT_QPA_PLATFORM=wayland;xcb # Tell Qt applications to use the Wayland backend, and fall back to x11 if Wayland is unavailable
# $env.QT_WAYLAND_DISABLE_WINDOWDECORATION = 1 # Disables window decorations on Qt applications
# $env.QT_QPA_PLATFORMTHEME = qt6ct # Tells Qt based applications to pick your theme from qt5ct, use with Kvantum.
# # HyDE Environment Variables -
# $env.PATH =
# $env.MOZ_ENABLE_WAYLAND=1 # Enable Wayland for Firefox
# $env.GDK_SCALE=1 # Set GDK scale to 1 // For Xwayland on HiDPI
# $env.ELECTRON_OZONE_PLATFORM_HINT=auto # Set Electron Ozone Platform Hint to auto // For Electron apps on Wayland
# # XDG-DIRS
# $env.XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR
# $env.XDG_CONFIG_HOME=$HOME/.config
# $env.XDG_CACHE_HOME=$HOME/.cache
# $env.XDG_DATA_HOME=$HOME/.local/share
# These variable will override the default and the theme configuration
# Do not uncomment the variables if you want want HyDE do the theme configuration for you
# // █▀▀ ▀█▀ █▄▀
# // █▄█ ░█░ █░█
#$GTK_THEME=Wallbash-Gtk
# dots-icons = repo-tracked overlay theme (gui/.local/share/icons/dots-icons) that inherits
# Tela-circle-dracula and only swaps the tray/status icons for Tela's monochrome panel set at
# every size — without it, waybar on a fractionally-scaled monitor requests 2x sizes and gets
# the colored-circle variants (the blue nm-applet blob). Everything else still comes from Tela.
$ICON_THEME=dots-icons
#$COLOR_SCHEME=prefer-dark
# // █▀▀ █░█ █▀█ █▀ █▀█ █▀█
# // █▄▄ █▄█ █▀▄ ▄█ █▄█ █▀▄
#$CURSOR_THEME=Bibata-Modern-Ice
#$CURSOR_SIZE=30
# // █▀▀ █▀█ █▄░█ ▀█▀
# // █▀░ █▄█ █░▀█ ░█░
#$FONT=Canterell
#$FONT_SIZE=10
#$DOCUMENT_FONT=Cantarell
#$DOCUMENT_FONT_SIZE=10
#$MONOSPACE_FONT=CaskaydiaCove Nerd Font Mono
#$MONOSPACE_FONT_SIZE=9
#$FONT_ANTIALIASING=rgba
#$FONT_HINTING=full
# // █░░ █▀█ █▀▀ █▄▀ █▀ █▀▀ █▀█ █▀▀ █▀▀ █▄░█
# // █▄▄ █▄█ █▄▄ █░█ ▄█ █▄▄ █▀▄ ██▄ ██▄ █░▀█
#Setting the Hyprlock layout will override any layout set in the ./hypr/hyprlock.sh
# Dynamic Hyprlock layout should be set in the ./hypr/hyprlock.sh file
# $LAYOUT_PATH=/path/to/hyprlock/layout.conf
+14 -3
View File
@@ -7,7 +7,14 @@
#!    ░▒▓▓   ▓▓  //____/
# $LOCKSCREEN = hyprlock # preferred LOCKSCREEN e.g swaylock
$LOCKSCREEN = lockscreen.sh # Calls $LOCKSCREEN set from hyprland
# Absolute path required: the Lua chain starts hypridle as a systemd user unit
# (hyde-Hyprland-idle.service), and on the SDDM/fish session path the session PATH
# never reaches the user manager (start_up.lua's unconditional `uwsm finalize` runs
# but errors outside a uwsm session, so the import never happens) — a bare
# `lockscreen.sh` is "command not found" there — Super+L, the 240s idle lock and
# before_sleep all silently no-op. The script self-bootstraps its own env
# (hyde-shell init), so the full path alone is sufficient.
$LOCKSCREEN = ~/.local/lib/hyde/lockscreen.sh
general {
lock_cmd = $LOCKSCREEN
@@ -31,10 +38,14 @@ listener {
}
# DPMS off
# Lua-chain form: in Lua mode `hyprctl dispatch` evaluates its argument as Lua, so the hyprlang
# `dpms off` is a syntax error and this listener silently never ran. The table form is the one
# hypridle's own sample uses (/usr/share/hypr/hypridle.conf); a bare bool/string argument acts
# like a toggle — `hl.dsp.dpms("on")` turned the display OFF. main (hyprlang) keeps `dpms off/on`.
listener {
timeout = 2300
on-timeout = hyprctl dispatch dpms off #do not turn off display while media is playing
on-resume = hyprctl dispatch dpms on
on-timeout = hyprctl dispatch 'hl.dsp.dpms({action = "off"})' #do not turn off display while media is playing
on-resume = hyprctl dispatch 'hl.dsp.dpms({action = "on"})'
}
# Suspend
-117
View File
@@ -1,117 +0,0 @@
$HYDE_HYPRLAND=set #! Do not remove - HyDE marker to prevent file overwrite
#!   ░▒▒▒░░░▓▓          ___________
#! ░░▒▒▒░░░░░▓▓        //___________/
#! ░░▒▒▒░░░░░▓▓     _   _ _    _ _____
#! ░░▒▒░░░░░▓▓▓▓▓▓ | | | | |  | |  __/
#! ░▒▒░░░░▓▓   ▓▓ | |_| | |_/ /| |___
#!  ░▒▒░░▓▓   ▓▓   \__  |____/ |____/
#!    ░▒▓▓   ▓▓  //____/
#?  HyDE's Hyprland Configuration 
# First, let's understand the structure of the configuration file
# It is divided into 3 sections: Boilerplate, Overrides, and Users
#* 1. Boilerplate
# - This section is for the default configuration of HyDE
# - It is recommended to not change this section
# - Filepath: $XDG_DATA_HOME/share/hyde/hyprland.conf
#* 2. Overrides
# - This section is for overriding the default configuration of HyDE
# - Change this section if you intended to:
# - Change the startup and env variables
# - Stop a app/service from starting
# - Override HyDE specific variables
# - Filepath: $XDG_CONFIG_HOME/hypr/hyde.conf
#* 3. Users
# - This section is for user configuration
# - It is recommended to change this section to your liking
# - Filepath: ./keybindings.conf, ./windowrules.conf, ./monitors.conf, ./userprefs.conf
#?  Boilerplate 
source = ~/.local/share/hyde/hyprland.conf
#?  Overrides 
# Overriding startup and env variables
# To avoid breaking and conflicts, HyDE will use variables to store the env and startup variables
# See ~/.config/hypr/hyde.conf for overriding the default env and startup variables
#* startup:
# - The startup variables are the variables that are set when the shell starts
# - $start.BAR=waybar will launch waybar on startup (default waybar)
# - $start.BAR= will unset the variable
# - $start.BAR=other_bar will launch other_bar on startup, overriding the default (waybar)
#* env:
# - The env variables are the variables that are set when the shell starts
# - $env.QT_QPA_PLATFORMTHEME= will unset the variable
# - $env.QT_QPA_PLATFORMTHEME=qt6ct will set the variable to qt6ct
# for updated configuration, see the https://github.com/HyDE-Project/HyDE/blob/master/Configs/.config/hypr/hyde.conf
#?  Users 
# These files are for user configuration
# Edit the files to your liking
# Refer to https://wiki.hyprland.org
source = ./keybindings.conf # Keyboard shortcuts
source = ./windowrules.conf # Window rules
source = ./monitors.conf # Monitor configuration (from per-host overlay)
source = ./userprefs.conf # Personal hyprland configuration
source = ./host.conf # Per-host extras: nvidia, etc. (from per-host overlay)
#!▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄
#? █▀█ █▀▀ █▀▀ █▀▀ █▀█ █▀▀ █▄░█ █▀▀ █▀▀
#? █▀▄ ██▄ █▀░ ██▄ █▀▄ ██▄ █░▀█ █▄▄ ██▄
#!▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄▄
#?  Reference 
# Configurations below are for reference ONLY
# They are not essential for HyDE to work but can be used to enhance QOL
# Copy the config line and paste it in the Users section to use it
# This is to prevent bloating users configuration files when not needed
# // █▀▀ █▄░█ █░█
# // ██▄ █░▀█ ▀▄▀
# env = MANGOHUD,1
# env = MANGOHUD_DLSYM,1
# // █▄▀ █▀▀ █▄█ █▄▄ █ █▄░█ █▀▄ █ █▄░█ █▀▀ █▀
# // █░█ ██▄ ░█░ █▄█ █ █░▀█ █▄▀ █ █░▀█ █▄█ ▄█
# bind = Ctrl+Alt+Shift, R, pass, ^(com\.obsproject\.Studio)$ # toggle obs screen recording // install obs flatpak
# bind = $mainMod+Shift, G, exec, pkill -x rofi || $scrPath/gamelauncher.sh # launch steam game launcher // install steam
# bind = $mainMod, Period, exec, emote # launch emoji selector // install emote
# bind = $mainMod+Alt, XF86MonBrightnessDown, exec, hyprshade on blue-light-filter # enable blue light filter // install hyprshade
# bind = $mainMod+Alt, XF86MonBrightnessUp, exec, hyprshade off # disable blue light filter // install hyprshade
# // █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█
# // █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█
# exec-once = swayidle -w timeout 600 'swaylock' timeout 900 'hyprctl dispatch dpms off' resume 'hyprctl dispatch dpms on' # lock after 10 mins, sleep after 15 mins // install swayidle
# exec-once = swayidle -w timeout 1200 'swaylock; hyprctl dispatch dpms off' resume 'hyprctl dispatch dpms on' timeout 1800 'systemctl suspend' # lock and sleep after 20 mins, suspend after 30 mins // install swayidle
# exec-once = libinput-gestures // install libinput-gestures
# exec-once = swayosd-server # enable swayosd service
# exec-once = `pkexec swayosd-libinput-backend` # swayosd service for keyboard input (requires to be run in a subshell)
# // █░█░█ █ █▄░█ █▀▄ █▀█ █░█░█   █▀█ █░█ █░░ █▀▀ █▀
# // ▀▄▀▄▀ █ █░▀█ █▄▀ █▄█ ▀▄▀▄▀   █▀▄ █▄█ █▄▄ ██▄ ▄█
# // █░█ █▀▄▀█
# // ▀▄▀ █░▀░█
# bind = Ctrl+Alt_L, V, submap, passthrough
# submap = passthrough
# bind = Ctrl+Alt_L, V, submap, reset
# submap = reset
+128
View File
@@ -0,0 +1,128 @@
-- gui package — the dots Hyprland layer, ported from the hyprlang chain
-- (hyde.conf / userprefs.conf / keybindings.conf / windowrules.conf) to
-- HyDE 26.x's Lua config. Loaded by ~/.local/share/hypr/hyde.lua second-to-last
-- (after every HyDE module, before lua_state.workflows), so plain assignments
-- here override HyDE's defaults. HyDE never overwrites this file.
--
-- The rules and keybinds sections live in ./dots_rules.lua and
-- ./dots_binds.lua, required in place below. Host-specific config (monitors,
-- workspace pins, gaps/rounding, gpu extras) lives in ./host.lua, loaded at
-- the bottom — same layering the hyprlang chain had with
-- `source = ./host.conf`.
-- Bootstrap: Hyprland prefers hyprland.lua as the entry point when started
-- without a config; without this block that path yields a bare cursor. hyde.lua
-- sets `hyde` on its first line, so this runs only when HyDE has not loaded.
-- The `return` matters: dofile(hyde.lua) re-enters this file through
-- check_require("hyprland"), so by the time dofile returns the body below has
-- already run once — falling through would register everything twice.
if not hyde then
local share = os.getenv("XDG_DATA_HOME") or (os.getenv("HOME") .. "/.local/share")
local entry = share .. "/hypr/hyde.lua"
local handle = io.open(entry, "r")
if not handle then
error(
"HyDE is not installed at " .. entry .. ". Run HyDE's install.sh -r, or point Hyprland at your own config."
)
end
handle:close()
dofile(entry)
return
end
local hc = hyde.config
local home = os.getenv("HOME") or ""
-- ── Apps ─────────────────────────────────────────────────────────────────────
-- Named directly (not hyde-shell open --fall …) for the same reason hyde.conf
-- did: skip the wrapper, match the xdg defaults. The assignments cover lazy
-- readers of hc.app.*, but key_binds.lua already baked the defaults into its
-- Super+B/E dispatchers at bind time — hence the explicit rebinds in
-- dots_binds.lua.
hc.app.browser = "zen-browser"
hc.app.explorer = "dolphin"
-- ── Startup ──────────────────────────────────────────────────────────────────
-- One knob suppresses HyDE's plain waybar AND launches the cava rig (was
-- $start.BAR= + a userprefs exec-once). start_up.lua captures hc.start by
-- reference and the hyprland.start event fires after this file loads, so the
-- override lands in time. `wbar` (gui/.local/bin) runs the rig as the
-- dots-waybar.service transient unit with Restart=always: waybar 0.15.0 dies on
-- every DP-2 link drop/return, and as a plain scope nothing relaunched it.
-- Absolute path: the exec env's PATH is not guaranteed to hold ~/.local/bin.
hc.start.bar = home .. "/.local/bin/wbar start"
-- Not part of the legacy session — the Lua defaults would newly launch these
-- (both binaries are installed). hyprsunset would visibly tint the screen.
-- Delete a line to adopt the HyDE default.
hc.start.blue_light_filter_daemon = ""
hc.start.clipboard_persist = ""
-- nm-applet --indicator is already the Lua default (kept deliberately: the
-- tray icon is the one place with a quick connect/disconnect menu).
-- ── UI / icon theme ──────────────────────────────────────────────────────────
-- dots-icons = repo-tracked overlay theme (gui/.local/share/icons/dots-icons)
-- that inherits Tela-circle-dracula and swaps the tray/status icons for the
-- monochrome panel set — without it, waybar on a fractionally-scaled monitor
-- gets the colored-circle variants (the blue nm-applet blob).
hc.ui.icon_theme = "dots-icons"
-- The theme-switch pipeline regenerates lua_state/ui.lua from the theme's
-- hypr.theme and writes THAT to dconf (color/hypr.sh -> color/dconf.lua), so
-- the assignment above is not enough on its own. Top-level code re-runs on
-- every `hyprctl reload`, and both theme-switch paths end in a reload, so this
-- write lands last — the Lua-mode equivalent of the legacy $exec.dConf.
-- Upstream-sanctioned alternative if this ever races: add
-- [hyprland]\n icon_theme = "dots-icons"
-- to ~/.config/hyde/config.toml (state/hyde/hyprland.conf then overrides the
-- theme vars in theme.switch.sh:151).
hl.exec_cmd([[dconf write /org/gnome/desktop/interface/icon-theme "'dots-icons'"]])
-- ── Env (was userprefs.conf) ─────────────────────────────────────────────────
hl.env("SSH_AUTH_SOCK", (os.getenv("XDG_RUNTIME_DIR") or "") .. "/ssh-agent.socket")
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "x11") -- disable after hyprland fixes electron flickering
-- ── Layer + window rules (dots_rules.lua) ────────────────────────────────────
-- Plain require, not check_require: stow --no-folding links new files one by
-- one, so a host that pulled without re-running install.sh fails loudly in
-- `hyprctl configerrors` instead of booting silently without its rules/binds.
-- The dots_ prefix matters: hyde.lua searches HyDE's own lua/ before
-- ~/.config/hypr, so a bare window_rules / key_binds would load HyDE's module.
require("dots_rules")
-- ── Animations ───────────────────────────────────────────────────────────────
-- The legacy chain ran $ANIMATION=theme — the hyprdots set from the theme's
-- animations.dcol. The Lua chain cannot inherit it: hyq's hyprland-lua.json
-- schema dumps decoration/borders into lua_state/hypr_theme.lua but NOT the
-- animations block (LUA-PORT.md risk 6, confirmed live), so without this the
-- session falls back to Hyprland's slower built-in defaults. Ported verbatim
-- from the generated ~/.config/hypr/animations/theme.conf.
-- Conditional so `hyde-shell animations --select` still works: a selector-
-- chosen lua_state/animations.lua loads earlier (dynamic.lua) and this block
-- steps aside for it.
local anim_state = (os.getenv("XDG_STATE_HOME") or home .. "/.local/state") .. "/hyde/lua_state/animations.lua"
local anim_f = io.open(anim_state, "r")
if anim_f then
anim_f:close()
else
hl.curve("wind", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.05 } } })
hl.curve("winIn", { type = "bezier", points = { { 0.1, 1.1 }, { 0.1, 1.1 } } })
hl.curve("winOut", { type = "bezier", points = { { 0.3, -0.3 }, { 0, 1 } } })
hl.curve("liner", { type = "bezier", points = { { 1, 1 }, { 1, 1 } } })
hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "wind", style = "slide" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "winIn", style = "slide" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "winOut", style = "slide" })
hl.animation({ leaf = "windowsMove", enabled = true, speed = 5, bezier = "wind", style = "slide" })
hl.animation({ leaf = "border", enabled = true, speed = 1, bezier = "liner" })
hl.animation({ leaf = "borderangle", enabled = true, speed = 30, bezier = "liner", style = "once" })
hl.animation({ leaf = "fade", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 5, bezier = "wind" })
end
-- ── Keybinds (dots_binds.lua) ────────────────────────────────────────────────
-- After the hc.app.* overrides above: the launcher binds read them.
require("dots_binds")
-- ── Host layer ───────────────────────────────────────────────────────────────
-- Resolves via hyde.lua's package.path (~/.config/hypr/?.lua); each host
-- overlay stows its own host.lua (wm/, lw/). check_require is HyDE's
-- missing-tolerant require — a host without one just skips it.
check_require("host")
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more