diff --git a/.gitignore b/.gitignore index 479dfb1..6e410d0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index ca66a8e..3190d17 100644 --- a/README.md +++ b/README.md @@ -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 | `/.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 | `/.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 `, +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--bar.service` (auto-restarting) — `systemctl --user stop ; systemctl --user mask `. -- 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. diff --git a/bin/dots-icons-link.sh b/bin/dots-icons-link.sh new file mode 100755 index 0000000..e0f3015 --- /dev/null +++ b/bin/dots-icons-link.sh @@ -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 ] diff --git a/bin/dots-themes-fetch.sh b/bin/dots-themes-fetch.sh new file mode 100755 index 0000000..c2d4ed6 --- /dev/null +++ b/bin/dots-themes-fetch.sh @@ -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 | 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 /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 ] diff --git a/common/.bashrc b/common/.bashrc index ae25f49..9540a7c 100644 --- a/common/.bashrc +++ b/common/.bashrc @@ -115,6 +115,7 @@ __prompt() { PROMPT_COMMAND=__prompt set -o vi +eval "$(fzf --bash)" # eval "$(zoxide init --cmd cd bash)" # colorized --help diff --git a/common/.config/aichat/config.yaml b/common/.config/aichat/config.yaml index dd4f275..49e8d6e 100644 --- a/common/.config/aichat/config.yaml +++ b/common/.config/aichat/config.yaml @@ -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 diff --git a/common/.config/aichat/dark.tmTheme b/common/.config/aichat/dark.tmTheme deleted file mode 100644 index b16050c..0000000 --- a/common/.config/aichat/dark.tmTheme +++ /dev/null @@ -1,663 +0,0 @@ - - - - - - name - OneHalfLight - semanticClass - theme.dark.one_half_dark - uuid - - colorSpaceName - sRGB - author - Son A. Pham <sp@sonpham.me> - comment - A dark iTerm 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 (Neo)Vim and Sublime Text. - settings - - - settings - - fontStyle - - foreground - #dcdfe4 - background - #282c34 - bracketsOptions - underline - caret - #a3b3cc - gutter - #282c34 - gutterForeground - #919baa - invisibles - #5c6370 - lineHighlight - #313640 - selection - #474e5d - selectionBorder - #474e5d - tagsForeground - - tagsOptions - stippled_underline - bracketContentsOptions - underline - - - - name - Comments - scope - comment - settings - - foreground - #5c6370 - - - - name - Text - scope - variable.parameter.function - settings - - foreground - #dcdfe4 - - - - name - Delimiters - scope - none - settings - - foreground - - - - - name - Operators - scope - keyword.operator - settings - - foreground - - - - - name - Keywords - scope - keyword - settings - - foreground - #c678dd - - - - name - Variables - scope - variable - settings - - foreground - #e06c75 - - - - name - Functions - scope - entity.name.function, meta.require, support.function.any-method - settings - - foreground - #61afef - - - - name - Classes - scope - support.class, entity.name.class, entity.name.type.class - settings - - foreground - #e5c07b - - - - name - Classes - scope - meta.class - settings - - foreground - #e5c07b - - - - name - Methods - scope - keyword.other.special-method - settings - - foreground - #61afef - - - - name - Storage - scope - storage - settings - - foreground - #c678dd - - - - name - Support - scope - support.function - settings - - foreground - #61afef - - - - name - Strings, Inherited Class - scope - string - settings - - foreground - #98c379 - - - - name - Integers - scope - constant.numeric - settings - - foreground - #e5c07b - - - - name - Floats - scope - none - settings - - foreground - #e5c07b - - - - name - Boolean - scope - none - settings - - foreground - #e5c07b - - - - name - Constants - scope - constant - settings - - foreground - #e5c07b - - - - name - HTML: Tags - scope - entity.name.tag - settings - - foreground - #e06c75 - - - - name - HTML: Tag attributes - scope - entity.other.attribute-name - settings - - foreground - #e5c07b - - - - name - Attribute IDs - scope - entity.other.attribute-name.id, punctuation.definition.entity - settings - - foreground - #e5c07b - - - - name - Selector - scope - meta.selector - settings - - foreground - #c678dd - - - - name - Markdown: Headings - scope - markup.heading punctuation.definition.heading, entity.name.section - settings - - fontStyle - - foreground - #61afef - - - - name - Markdown: Bold - scope - markup.bold, punctuation.definition.bold - settings - - foreground - #c678dd - - - - name - Markdown: Italic - scope - markup.italic, punctuation.definition.italic - settings - - foreground - #c678dd - - - - name - Markdown: Code - scope - markup.raw.inline - settings - - foreground - #98c379 - - - - name - Markdown: Link Text - scope - string.other.link, punctuation.definition.string.end.markdown - settings - - foreground - - - - - name - Markdown: Link Url - scope - meta.link - settings - - foreground - #98c379 - - - - name - Markdown: Lists - scope - markup.list - settings - - foreground - - - - - name - Markdown: Quotes - scope - markup.quote - settings - - foreground - #98c379 - - - - name - Java Source - scope - source.java meta.class.java meta.method.java - settings - - foreground - #dcdfe4 - - - - name - Java Class Body - scope - source.java meta.class.java meta.class.body.java - settings - - foreground - #dcdfe4 - - - - name - Javascript: Function Arguments - scope - source.js meta.function.js variable.parameter.function.js - settings - - foreground - #e06c75 - - - - name - Javascript: New Variables - scope - source.js variable.other.readwrite.js - settings - - foreground - #e06c75 - - - - name - Javascript: Variables - scope - source.js variable.other.object.js - settings - - foreground - #dcdfe4 - - - - name - Javascript: Variables in Function Calls - scope - source.js meta.function-call.method.js variable.other.readwrite.js - settings - - foreground - #e06c75 - - - - name - Javascript: New Block Variables - scope - source.js meta.block.js variable.other.readwrite.js - settings - - foreground - #e06c75 - - - - name - Javascript: Block Variables - scope - source.js meta.block.js variable.other.object.js - settings - - foreground - #dcdfe4 - - - - name - Javascript: Block Variables in Function Calls - scope - source.js meta.block.js meta.function-call.method.js variable.other.readwrite.js - settings - - foreground - #dcdfe4 - - - - name - Javascript: Function Calls - scope - source.js meta.function-call.method.js variable.function.js - settings - - foreground - #dcdfe4 - - - - name - Javascript: Properties - scope - source.js meta.property.object.js entity.name.function.js - settings - - foreground - #61afef - - - - name - Javascript: Prototypes - scope - source.js support.constant.prototype.js - settings - - foreground - #dcdfe4 - - - - name - Separator - scope - meta.separator - settings - - background - - foreground - - - - - name - Inserted - scope - markup.inserted - settings - - foreground - #98c379 - - - - name - Deleted - scope - markup.deleted - settings - - foreground - #e06c75 - - - - name - Changed - scope - markup.changed - settings - - foreground - #e5c07b - - - - name - Regular Expressions - scope - string.regexp - settings - - foreground - #98c379 - - - - name - Escape Characters - scope - constant.character.escape - settings - - foreground - #56b6c2 - - - - name - Embedded - scope - punctuation.section.embedded, variable.interpolation - settings - - foreground - - - - - name - Illegal - scope - invalid.illegal - settings - - background - #e06c75 - foreground - #dcdfe4 - - - - name - Broken - scope - invalid.broken - settings - - background - #e5c07b - foreground - #dcdfe4 - - - - name - Deprecated - scope - invalid.deprecated - settings - - background - #e5c07b - foreground - #dcdfe4 - - - - name - Unimplemented - scope - invalid.unimplemented - settings - - background - #c678dd - foreground - #dcdfe4 - - - - - \ No newline at end of file diff --git a/common/.config/bat/config b/common/.config/bat/config index 9a5d3a9..0f447ad 100644 --- a/common/.config/bat/config +++ b/common/.config/bat/config @@ -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" diff --git a/common/.config/bat/themes/onedark.tmTheme b/common/.config/bat/themes/onedark.tmTheme deleted file mode 100644 index aa14d99..0000000 --- a/common/.config/bat/themes/onedark.tmTheme +++ /dev/null @@ -1,260 +0,0 @@ - - - - - name - One Dark (NvChad base46) - uuid - 3c1e2b84-9d5f-4a7c-8b2e-6f4a1d9c7e5b - settings - - - settings - - background - #1e222a - foreground - #abb2bf - caret - #abb2bf - selection - #3e4451 - lineHighlight - #252931 - gutterForeground - #545862 - - - - name - Comment - scope - comment, punctuation.definition.comment - settings - - foreground - #6f737b - fontStyle - italic - - - - name - String - scope - string - settings - - foreground - #98c379 - - - - name - Number, constant - scope - constant.numeric, constant.language, constant.other - settings - - foreground - #d19a66 - - - - name - Escape character - scope - constant.character.escape - settings - - foreground - #a3b8ef - - - - name - Keyword, storage - scope - keyword, storage - settings - - foreground - #c882e7 - - - - name - Operator - scope - keyword.operator - settings - - foreground - #abb2bf - - - - name - Function - scope - entity.name.function, support.function, meta.function-call.generic - settings - - foreground - #61afef - - - - name - Class, type - scope - entity.name.class, entity.name.type, entity.name.struct, entity.name.enum, support.type, support.class - settings - - foreground - #e7c787 - - - - name - Variable - scope - variable, variable.other - settings - - foreground - #e06c75 - - - - name - Function parameter - scope - variable.parameter - settings - - foreground - #e06c75 - fontStyle - italic - - - - name - Attribute - scope - entity.other.attribute-name - settings - - foreground - #d19a66 - - - - name - Tag - scope - entity.name.tag, meta.tag - settings - - foreground - #e06c75 - - - - name - Markup heading - scope - markup.heading, entity.name.section - settings - - foreground - #e06c75 - fontStyle - bold - - - - name - Markup bold - scope - markup.bold - settings - - fontStyle - bold - - - - name - Markup italic - scope - markup.italic - settings - - fontStyle - italic - - - - name - Markup link - scope - markup.underline.link, string.other.link - settings - - foreground - #61afef - - - - name - Markup quote - scope - markup.quote - settings - - foreground - #6f737b - fontStyle - italic - - - - name - Diff inserted - scope - markup.inserted - settings - - foreground - #98c379 - - - - name - Diff deleted - scope - markup.deleted - settings - - foreground - #e06c75 - - - - name - Invalid - scope - invalid, invalid.illegal - settings - - foreground - #ffffff - background - #be5046 - - - - - diff --git a/common/.config/bat/themes/synthwave84.tmTheme b/common/.config/bat/themes/synthwave84.tmTheme deleted file mode 100644 index 30ee7b7..0000000 --- a/common/.config/bat/themes/synthwave84.tmTheme +++ /dev/null @@ -1,262 +0,0 @@ - - - - - name - Synthwave 84 - uuid - 7b2c3a10-4b6e-4d8f-9c1a-2e5f8d7a9b3c - settings - - - settings - - background - #262335 - foreground - #ffffff - caret - #f97e72 - selection - #463465 - lineHighlight - #2a2139 - gutterForeground - #848bbd - - - - name - Comment - scope - comment, punctuation.definition.comment - settings - - foreground - #848bbd - fontStyle - italic - - - - name - String - scope - string - settings - - foreground - #ff8b39 - - - - name - Number, constant - scope - constant.numeric, constant.language, constant.other - settings - - foreground - #f97e72 - - - - name - Escape character - scope - constant.character.escape - settings - - foreground - #36f9f6 - - - - name - Keyword, storage - scope - keyword, storage, keyword.operator - settings - - foreground - #fede5d - - - - name - Storage type - scope - storage.type, storage.modifier - settings - - foreground - #fede5d - fontStyle - italic - - - - name - Function - scope - entity.name.function, support.function, meta.function-call.generic - settings - - foreground - #36f9f6 - - - - name - Class, type - scope - entity.name.class, entity.name.type, entity.name.struct, entity.name.enum, support.type, support.class - settings - - foreground - #fe4450 - - - - name - Variable - scope - variable, variable.other - settings - - foreground - #ff7edb - - - - name - Function parameter - scope - variable.parameter - settings - - foreground - #ff7edb - fontStyle - italic - - - - name - Attribute - scope - entity.other.attribute-name - settings - - foreground - #fede5d - - - - name - Tag - scope - entity.name.tag, meta.tag - settings - - foreground - #72f1b8 - - - - name - Markup heading - scope - markup.heading, entity.name.section - settings - - foreground - #fe4450 - fontStyle - bold - - - - name - Markup bold - scope - markup.bold - settings - - fontStyle - bold - - - - name - Markup italic - scope - markup.italic - settings - - fontStyle - italic - - - - name - Markup link - scope - markup.underline.link, string.other.link - settings - - foreground - #36f9f6 - - - - name - Markup quote - scope - markup.quote - settings - - foreground - #848bbd - fontStyle - italic - - - - name - Diff inserted - scope - markup.inserted - settings - - foreground - #72f1b8 - - - - name - Diff deleted - scope - markup.deleted - settings - - foreground - #fe4450 - - - - name - Invalid - scope - invalid, invalid.illegal - settings - - foreground - #ffffff - background - #fe4450 - - - - - diff --git a/common/.config/cava/config b/common/.config/cava/config index 33e05f8..558c987 100644 --- a/common/.config/cava/config +++ b/common/.config/cava/config @@ -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 diff --git a/common/.config/cava/shaders/bar_spectrum.frag b/common/.config/cava/shaders/bar_spectrum.frag deleted file mode 100644 index b078913..0000000 --- a/common/.config/cava/shaders/bar_spectrum.frag +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/common/.config/cava/shaders/eye_of_phi.frag b/common/.config/cava/shaders/eye_of_phi.frag deleted file mode 100644 index e499ee7..0000000 --- a/common/.config/cava/shaders/eye_of_phi.frag +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/common/.config/cava/shaders/northern_lights.frag b/common/.config/cava/shaders/northern_lights.frag deleted file mode 100644 index ecd859a..0000000 --- a/common/.config/cava/shaders/northern_lights.frag +++ /dev/null @@ -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; -} diff --git a/common/.config/cava/shaders/pass_through.vert b/common/.config/cava/shaders/pass_through.vert deleted file mode 100644 index a4f20e5..0000000 --- a/common/.config/cava/shaders/pass_through.vert +++ /dev/null @@ -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; -} diff --git a/common/.config/cava/shaders/spectrogram.frag b/common/.config/cava/shaders/spectrogram.frag deleted file mode 100644 index ccb79ae..0000000 --- a/common/.config/cava/shaders/spectrogram.frag +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/common/.config/cava/shaders/winamp_line_style_spectrum.frag b/common/.config/cava/shaders/winamp_line_style_spectrum.frag deleted file mode 100644 index 375ff27..0000000 --- a/common/.config/cava/shaders/winamp_line_style_spectrum.frag +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/common/.config/cava/themes/solarized_dark b/common/.config/cava/themes/solarized_dark deleted file mode 100644 index 200057c..0000000 --- a/common/.config/cava/themes/solarized_dark +++ /dev/null @@ -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' \ No newline at end of file diff --git a/common/.config/cava/themes/tricolor b/common/.config/cava/themes/tricolor deleted file mode 100644 index b908137..0000000 --- a/common/.config/cava/themes/tricolor +++ /dev/null @@ -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' \ No newline at end of file diff --git a/common/.config/claude/README.md b/common/.config/claude/README.md index 87059de..e0c2020 100644 --- a/common/.config/claude/README.md +++ b/common/.config/claude/README.md @@ -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 `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 -- `, `reset HEAD ` stay allowed) / `restore ` (`--staged ` 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 diff --git a/common/.config/claude/StatusBar.md b/common/.config/claude/StatusBar.md index 228f6d9..7f2cdb7 100644 --- a/common/.config/claude/StatusBar.md +++ b/common/.config/claude/StatusBar.md @@ -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..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..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..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. diff --git a/common/.config/claude/hooks/git-guard-test.sh b/common/.config/claude/hooks/git-guard-test.sh index f430e24..52ad051 100755 --- a/common/.config/claude/hooks/git-guard-test.sh +++ b/common/.config/claude/hooks/git-guard-test.sh @@ -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 ` 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 - < : 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 +} diff --git a/common/.config/claude/hooks/git-guard.py b/common/.config/claude/hooks/git-guard.py index fa3bfa1..07cb613 100755 --- a/common/.config/claude/hooks/git-guard.py +++ b/common/.config/claude/hooks/git-guard.py @@ -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 -- `, `reset HEAD ` and `reset ` are fine) | restore (--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 ''`: 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 ) 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) diff --git a/common/.config/claude/keybindings.json b/common/.config/claude/keybindings.json index ddc88f3..8a89cc3 100644 --- a/common/.config/claude/keybindings.json +++ b/common/.config/claude/keybindings.json @@ -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" } }, { diff --git a/common/.config/claude/link.sh b/common/.config/claude/link.sh index 1ac91fa..ec442e0 100755 --- a/common/.config/claude/link.sh +++ b/common/.config/claude/link.sh @@ -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 diff --git a/common/.config/claude/settings.json b/common/.config/claude/settings.json index 91f532a..945d3e2 100644 --- a/common/.config/claude/settings.json +++ b/common/.config/claude/settings.json @@ -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, diff --git a/common/.config/claude/skills/commit-msg/SKILL.md b/common/.config/claude/skills/commit-msg/SKILL.md index 5d2eef4..033278b 100644 --- a/common/.config/claude/skills/commit-msg/SKILL.md +++ b/common/.config/claude/skills/commit-msg/SKILL.md @@ -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 < `. - 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. diff --git a/common/.config/claude/skills/pr-loop/SKILL.md b/common/.config/claude/skills/pr-loop/SKILL.md index 105ed35..7b61a11 100644 --- a/common/.config/claude/skills/pr-loop/SKILL.md +++ b/common/.config/claude/skills/pr-loop/SKILL.md @@ -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 < `. - - 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. diff --git a/common/.config/claude/statusline.py b/common/.config/claude/statusline.py index b8ac001..ed26d76 100755 --- a/common/.config/claude/statusline.py +++ b/common/.config/claude/statusline.py @@ -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..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() - end = chunk.rfind(b"\n") + 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..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 ( diff --git a/common/.config/fastfetch/config.jsonc b/common/.config/fastfetch/config.jsonc index 696b540..0b6784b 100644 --- a/common/.config/fastfetch/config.jsonc +++ b/common/.config/fastfetch/config.jsonc @@ -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": { diff --git a/common/.config/fastfetch/pngs/anon.png b/common/.config/fastfetch/pngs/anon.png deleted file mode 100644 index 3a839af..0000000 Binary files a/common/.config/fastfetch/pngs/anon.png and /dev/null differ diff --git a/common/.config/fish/conf.d/agent-state.fish b/common/.config/fish/conf.d/agent-state.fish new file mode 100644 index 0000000..dec8419 --- /dev/null +++ b/common/.config/fish/conf.d/agent-state.fish @@ -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 diff --git a/common/.config/fish/conf.d/kitty-public-key.fish b/common/.config/fish/conf.d/kitty-public-key.fish new file mode 100644 index 0000000..8132bb7 --- /dev/null +++ b/common/.config/fish/conf.d/kitty-public-key.fish @@ -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 diff --git a/common/.config/fish/config.fish b/common/.config/fish/config.fish index a2b7393..921b530 100644 --- a/common/.config/fish/config.fish +++ b/common/.config/fish/config.fish @@ -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" diff --git a/common/.config/fish/functions/glow.fish b/common/.config/fish/functions/glow.fish new file mode 100644 index 0000000..64e174b --- /dev/null +++ b/common/.config/fish/functions/glow.fish @@ -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 diff --git a/common/.config/fish/functions/md2pdf.fish b/common/.config/fish/functions/md2pdf.fish new file mode 100644 index 0000000..7639c3d --- /dev/null +++ b/common/.config/fish/functions/md2pdf.fish @@ -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 '' + 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 diff --git a/common/.config/misc/clis.txt b/common/.config/misc/clis.txt index 82f909f..86e82b8 100644 --- a/common/.config/misc/clis.txt +++ b/common/.config/misc/clis.txt @@ -35,4 +35,4 @@ traceroute fuck atool aria2c - +cmark-gfm diff --git a/common/.config/nvim/lazy-lock.json b/common/.config/nvim/lazy-lock.json index a4f6a3c..531110c 100644 --- a/common/.config/nvim/lazy-lock.json +++ b/common/.config/nvim/lazy-lock.json @@ -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" } } diff --git a/common/.config/opencode/README.md b/common/.config/opencode/README.md index f608fe9..e44101a 100644 --- a/common/.config/opencode/README.md +++ b/common/.config/opencode/README.md @@ -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: `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. | diff --git a/common/.config/opencode/opencode.json b/common/.config/opencode/opencode.json index 52b69e6..ab2f601 100644 --- a/common/.config/opencode/opencode.json +++ b/common/.config/opencode/opencode.json @@ -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 } } } diff --git a/common/.config/opencode/plugins/agent-state.ts b/common/.config/opencode/plugins/agent-state.ts new file mode 100644 index 0000000..3e19b4a --- /dev/null +++ b/common/.config/opencode/plugins/agent-state.ts @@ -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() + 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 }).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 + } + }, + } +} diff --git a/common/.config/tmux/scripts/agent-next.sh b/common/.config/tmux/scripts/agent-next.sh new file mode 100755 index 0000000..7c1a902 --- /dev/null +++ b/common/.config/tmux/scripts/agent-next.sh @@ -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 diff --git a/common/.config/tmux/scripts/agent-state.sh b/common/.config/tmux/scripts/agent-state.sh new file mode 100755 index 0000000..b12d762 --- /dev/null +++ b/common/.config/tmux/scripts/agent-state.sh @@ -0,0 +1,69 @@ +#!/bin/sh +# agent-state.sh +# +# 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/.json, and the pane is the one whose +# title Claude set to that session's 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 diff --git a/common/.config/tmux/scripts/cpu.sh b/common/.config/tmux/scripts/cpu.sh deleted file mode 100755 index 5de0b0f..0000000 --- a/common/.config/tmux/scripts/cpu.sh +++ /dev/null @@ -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 "$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 diff --git a/common/.config/tmux/scripts/mem.sh b/common/.config/tmux/scripts/mem.sh deleted file mode 100755 index 5ecd761..0000000 --- a/common/.config/tmux/scripts/mem.sh +++ /dev/null @@ -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 diff --git a/common/.config/tmux/scripts/metrics.sh b/common/.config/tmux/scripts/metrics.sh new file mode 100755 index 0000000..e9bc907 --- /dev/null +++ b/common/.config/tmux/scripts/metrics.sh @@ -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 "$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 diff --git a/common/.config/tmux/scripts/net.sh b/common/.config/tmux/scripts/net.sh deleted file mode 100755 index 7cbbd31..0000000 --- a/common/.config/tmux/scripts/net.sh +++ /dev/null @@ -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 - 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 diff --git a/common/.config/tmux/scripts/temp.sh b/common/.config/tmux/scripts/temp.sh deleted file mode 100755 index 9aef90d..0000000 --- a/common/.config/tmux/scripts/temp.sh +++ /dev/null @@ -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)) diff --git a/common/.config/tmux/tmux.conf b/common/.config/tmux/tmux.conf index 7bd1d8f..3627f8a 100644 --- a/common/.config/tmux/tmux.conf +++ b/common/.config/tmux/tmux.conf @@ -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 } { - switch-client -t = + 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 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" diff --git a/common/.pi/README.md b/common/.pi/README.md index 2f36c04..e4c9b1f 100644 --- a/common/.pi/README.md +++ b/common/.pi/README.md @@ -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 `` 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 diff --git a/common/.pi/agent/extensions/agent-state.ts b/common/.pi/agent/extensions/agent-state.ts new file mode 100644 index 0000000..fe3df11 --- /dev/null +++ b/common/.pi/agent/extensions/agent-state.ts @@ -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")); +} diff --git a/common/.pi/agent/extensions/compaction-watch.ts b/common/.pi/agent/extensions/compaction-watch.ts index c5e3a18..0adab9c 100644 --- a/common/.pi/agent/extensions/compaction-watch.ts +++ b/common/.pi/agent/extensions/compaction-watch.ts @@ -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. diff --git a/common/.pi/agent/extensions/statusbar.ts b/common/.pi/agent/extensions/statusbar.ts index d1e75cc..3394f34 100644 --- a/common/.pi/agent/extensions/statusbar.ts +++ b/common/.pi/agent/extensions/statusbar.ts @@ -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, onTimings: (t: ServerTimings) => void): Promise { + 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; + 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[0], init?: Parameters[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; - const secs = genFirstTokenAt ? (Date.now() - genFirstTokenAt) / 1000 : 0; - if (secs > 0 && output > 0) lastTps = output / secs; + // 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); }); diff --git a/common/.pi/agent/models.json b/common/.pi/agent/models.json index dde1a9b..f548f56 100644 --- a/common/.pi/agent/models.json +++ b/common/.pi/agent/models.json @@ -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 } }, diff --git a/common/.pi/agent/settings.json b/common/.pi/agent/settings.json index acf5d68..3119dd8 100644 --- a/common/.pi/agent/settings.json +++ b/common/.pi/agent/settings.json @@ -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" ] diff --git a/fl/.config/hypr/animations.conf b/fl/.config/hypr/animations.conf deleted file mode 100644 index 4e33fd3..0000000 --- a/fl/.config/hypr/animations.conf +++ /dev/null @@ -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 -} diff --git a/fl/.config/hypr/hyprland.conf b/fl/.config/hypr/hyprland.conf deleted file mode 100644 index f6f36ee..0000000 --- a/fl/.config/hypr/hyprland.conf +++ /dev/null @@ -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 diff --git a/fl/.config/hypr/keybindings.conf b/fl/.config/hypr/keybindings.conf deleted file mode 100644 index 95b0b0b..0000000 --- a/fl/.config/hypr/keybindings.conf +++ /dev/null @@ -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 - diff --git a/fl/.config/hypr/monitors.conf b/fl/.config/hypr/monitors.conf deleted file mode 100644 index 8da85ed..0000000 --- a/fl/.config/hypr/monitors.conf +++ /dev/null @@ -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 - diff --git a/fl/.config/hypr/userprefs.conf b/fl/.config/hypr/userprefs.conf deleted file mode 100644 index 9ad1750..0000000 --- a/fl/.config/hypr/userprefs.conf +++ /dev/null @@ -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 - diff --git a/fl/.config/hypr/windowrules.conf b/fl/.config/hypr/windowrules.conf deleted file mode 100644 index f3f112c..0000000 --- a/fl/.config/hypr/windowrules.conf +++ /dev/null @@ -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 diff --git a/fl/.config/kglobalshortcutsrc b/fl/.config/kglobalshortcutsrc index 83d32ae..742c69a 100644 --- a/fl/.config/kglobalshortcutsrc +++ b/fl/.config/kglobalshortcutsrc @@ -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 diff --git a/fl/.config/llamacpp/README.md b/fl/.config/llamacpp/README.md index 63cd8f1..d066898 100644 --- a/fl/.config/llamacpp/README.md +++ b/fl/.config/llamacpp/README.md @@ -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 `` 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`. diff --git a/fl/.config/llamacpp/bench.py b/fl/.config/llamacpp/bench.py index 0c8b910..00594cf 100755 --- a/fl/.config/llamacpp/bench.py +++ b/fl/.config/llamacpp/bench.py @@ -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() diff --git a/fl/.config/llamacpp/config.ini b/fl/.config/llamacpp/config.ini index b70ea7e..45870ec 100644 --- a/fl/.config/llamacpp/config.ini +++ b/fl/.config/llamacpp/config.ini @@ -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. diff --git a/fl/.config/vllm/README.md b/fl/.config/vllm/README.md new file mode 100644 index 0000000..3fa4673 --- /dev/null +++ b/fl/.config/vllm/README.md @@ -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 --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.** `` 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 `. 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. diff --git a/fl/.config/vllm/bench-vs.py b/fl/.config/vllm/bench-vs.py new file mode 100755 index 0000000..2ed0c75 --- /dev/null +++ b/fl/.config/vllm/bench-vs.py @@ -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() diff --git a/fl/.config/vllm/vllm.ini b/fl/.config/vllm/vllm.ini new file mode 100644 index 0000000..ae67c6a --- /dev/null +++ b/fl/.config/vllm/vllm.ini @@ -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 --stop-router`), start it again after `vllmctl.py down`. +# +# Sections: +# [global] container/runtime knobs (NOT vllm flags) — see vllmctl.py +# [] one `vllmctl.py up `: +# model HF repo id, or /models/ (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} diff --git a/fl/.config/vllm/vllmctl.py b/fl/.config/vllm/vllmctl.py new file mode 100755 index 0000000..5183bc5 --- /dev/null +++ b/fl/.config/vllm/vllmctl.py @@ -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 [--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 +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() diff --git a/fl/.config/whisper/README.md b/fl/.config/whisper/README.md index 87a3dcc..22f2d28 100644 --- a/fl/.config/whisper/README.md +++ b/fl/.config/whisper/README.md @@ -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 diff --git a/gui/.config/alacritty/theme.toml b/gui/.config/alacritty/theme.toml index 1d958bd..dc79cce 100644 --- a/gui/.config/alacritty/theme.toml +++ b/gui/.config/alacritty/theme.toml @@ -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" diff --git a/gui/.config/dconf/profile/hyde_hyprland b/gui/.config/dconf/profile/hyde_hyprland new file mode 100644 index 0000000..0689bf4 --- /dev/null +++ b/gui/.config/dconf/profile/hyde_hyprland @@ -0,0 +1 @@ +user-db:hyde_hyprland diff --git a/gui/.config/environment.d/10-hyde-path.conf b/gui/.config/environment.d/10-hyde-path.conf new file mode 100644 index 0000000..3841157 --- /dev/null +++ b/gui/.config/environment.d/10-hyde-path.conf @@ -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 diff --git a/gui/.config/fish/conf.d/00-hypr-config.fish b/gui/.config/fish/conf.d/00-hypr-config.fish index 29cb279..81570aa 100644 --- a/gui/.config/fish/conf.d/00-hypr-config.fish +++ b/gui/.config/fish/conf.d/00-hypr-config.fish @@ -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" diff --git a/gui/.config/hypr/.luarc.json b/gui/.config/hypr/.luarc.json new file mode 100644 index 0000000..b7a5b18 --- /dev/null +++ b/gui/.config/hypr/.luarc.json @@ -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" + } +} diff --git a/gui/.config/hypr/animations.conf b/gui/.config/hypr/animations.conf deleted file mode 100644 index bdb8598..0000000 --- a/gui/.config/hypr/animations.conf +++ /dev/null @@ -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 diff --git a/gui/.config/hypr/animations/classic.conf b/gui/.config/hypr/animations/classic.conf deleted file mode 100644 index d1b032f..0000000 --- a/gui/.config/hypr/animations/classic.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/diablo-1.conf b/gui/.config/hypr/animations/diablo-1.conf deleted file mode 100644 index 6de9a8c..0000000 --- a/gui/.config/hypr/animations/diablo-1.conf +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/gui/.config/hypr/animations/diablo-2.conf b/gui/.config/hypr/animations/diablo-2.conf deleted file mode 100644 index 679db41..0000000 --- a/gui/.config/hypr/animations/diablo-2.conf +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/gui/.config/hypr/animations/disable.conf b/gui/.config/hypr/animations/disable.conf deleted file mode 100644 index b9ac665..0000000 --- a/gui/.config/hypr/animations/disable.conf +++ /dev/null @@ -1,9 +0,0 @@ - -# // ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█ -# // █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█ - -# See https://wiki.hyprland.org/Configuring/Animations/ -# credits: https://github.com/prasanthrangan/hyprdots - -animations:enabled=false - diff --git a/gui/.config/hypr/animations/dynamic.conf b/gui/.config/hypr/animations/dynamic.conf deleted file mode 100644 index f460dac..0000000 --- a/gui/.config/hypr/animations/dynamic.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/end4.conf b/gui/.config/hypr/animations/end4.conf deleted file mode 100644 index 8be409c..0000000 --- a/gui/.config/hypr/animations/end4.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/fast.conf b/gui/.config/hypr/animations/fast.conf deleted file mode 100644 index f6f2984..0000000 --- a/gui/.config/hypr/animations/fast.conf +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/gui/.config/hypr/animations/high.conf b/gui/.config/hypr/animations/high.conf deleted file mode 100644 index f05559a..0000000 --- a/gui/.config/hypr/animations/high.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/ja.conf b/gui/.config/hypr/animations/ja.conf deleted file mode 100644 index d1768f6..0000000 --- a/gui/.config/hypr/animations/ja.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/me-1.conf b/gui/.config/hypr/animations/me-1.conf deleted file mode 100644 index 3606429..0000000 --- a/gui/.config/hypr/animations/me-1.conf +++ /dev/null @@ -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 -} - - diff --git a/gui/.config/hypr/animations/me-2.conf b/gui/.config/hypr/animations/me-2.conf deleted file mode 100644 index 4de5ceb..0000000 --- a/gui/.config/hypr/animations/me-2.conf +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/gui/.config/hypr/animations/minimal-1.conf b/gui/.config/hypr/animations/minimal-1.conf deleted file mode 100644 index 4aa1f79..0000000 --- a/gui/.config/hypr/animations/minimal-1.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/minimal-2.conf b/gui/.config/hypr/animations/minimal-2.conf deleted file mode 100644 index 66da51b..0000000 --- a/gui/.config/hypr/animations/minimal-2.conf +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/gui/.config/hypr/animations/moving.conf b/gui/.config/hypr/animations/moving.conf deleted file mode 100644 index 23b1dec..0000000 --- a/gui/.config/hypr/animations/moving.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/optimized.conf b/gui/.config/hypr/animations/optimized.conf deleted file mode 100644 index 23c698f..0000000 --- a/gui/.config/hypr/animations/optimized.conf +++ /dev/null @@ -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% -} \ No newline at end of file diff --git a/gui/.config/hypr/animations/standard.conf b/gui/.config/hypr/animations/standard.conf deleted file mode 100644 index 4482dd9..0000000 --- a/gui/.config/hypr/animations/standard.conf +++ /dev/null @@ -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 -} diff --git a/gui/.config/hypr/animations/vertical.conf b/gui/.config/hypr/animations/vertical.conf deleted file mode 100644 index 272d28f..0000000 --- a/gui/.config/hypr/animations/vertical.conf +++ /dev/null @@ -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% -} diff --git a/gui/.config/hypr/dots_binds.lua b/gui/.config/hypr/dots_binds.lua new file mode 100644 index 0000000..5c88057 --- /dev/null +++ b/gui/.config/hypr/dots_binds.lua @@ -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 diff --git a/gui/.config/hypr/dots_rules.lua b/gui/.config/hypr/dots_rules.lua new file mode 100644 index 0000000..4e3c957 --- /dev/null +++ b/gui/.config/hypr/dots_rules.lua @@ -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, +}) diff --git a/gui/.config/hypr/host.conf b/gui/.config/hypr/host.conf deleted file mode 100644 index 0c871ad..0000000 --- a/gui/.config/hypr/host.conf +++ /dev/null @@ -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"). diff --git a/gui/.config/hypr/hyde.conf b/gui/.config/hypr/hyde.conf deleted file mode 100644 index f72a9db..0000000 --- a/gui/.config/hypr/hyde.conf +++ /dev/null @@ -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 diff --git a/gui/.config/hypr/hypridle.conf b/gui/.config/hypr/hypridle.conf index b1466e7..790dc81 100644 --- a/gui/.config/hypr/hypridle.conf +++ b/gui/.config/hypr/hypridle.conf @@ -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 diff --git a/gui/.config/hypr/hyprland.conf b/gui/.config/hypr/hyprland.conf deleted file mode 100644 index 328e8ab..0000000 --- a/gui/.config/hypr/hyprland.conf +++ /dev/null @@ -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 diff --git a/gui/.config/hypr/hyprland.lua b/gui/.config/hypr/hyprland.lua new file mode 100644 index 0000000..e158712 --- /dev/null +++ b/gui/.config/hypr/hyprland.lua @@ -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") diff --git a/gui/.config/hypr/keybindings.conf b/gui/.config/hypr/keybindings.conf deleted file mode 100644 index 928b300..0000000 --- a/gui/.config/hypr/keybindings.conf +++ /dev/null @@ -1,266 +0,0 @@ -## █▄▀ █▀▀ █▄█ █▄▄ █ █▄░█ █▀▄ █ █▄░█ █▀▀ █▀ -## █░█ ██▄ ░█░ █▄█ █ █░▀█ █▄▀ █ █░▀█ █▄█ ▄█ - -# see https://wiki.hyprland.org/configuring/keywords/ for more -# example binds, see https://wiki.hyprland.org/configuring/binds/ for more - -# Grouping of binds for easier management -# $d=[Group Name|Subgroup Name1|Subgroup Name2|...] -# '$d' is a variable that is used to group binds together (or use another variable) -# This is only for organization purposes and is not a defined hyprland variable -# What we did here is to modify the Description of the binds to include the group name -# The $d will be parsed as a separate key to be use for a GUI or something pretty -# [Main|Subgroup1|Subgroup2|...] -# Main - The main groupname -# Subgroup1.. - The subgroup names can be use to avoid repeating the same description - - -#*  Variables  -# Default if commented out -# $mainMod = Super # super / meta / windows key -# Assign apps -# $TERMINAL = kitty -# $EDITOR = code -# $EXPLORER = dolphin -# $BROWSER = firefox - -$wm=Window Management -$d=[$wm] -# `dontkillsteam.sh` was dropped by HyDE (gone from lib/hyde as of the 2026-08-13 update) and -# upstream now closes with the native dispatcher — its Lua key_binds.lua binds both of these to -# `hl.dsp.window.close()`. A bind pointing at a missing script stays valid and silently does -# nothing, which on these two keys means windows stop closing. -bindd = $mainMod, Q, $d close focused window, killactive, -bindd = Alt, F4, $d close focused window, killactive, -bindd = $mainMod, Delete, $d kill hyprland session, exit -bindd = $mainMod, W, $d toggle float, togglefloating, # -bindd = $mainMod Shift, G, $d toggle group, togglegroup -bindd = Shift, F11, $d toggle fullscreen, fullscreen -bindd = Alt, Return, $d toggle fullscreen, fullscreen -bindd = $mainMod, L, $d lock screen, exec, lockscreen.sh -bindd = $mainMod Shift, F, $d toggle pin on focused window, exec, $scrPath/windowpin.sh # renamed upstream (was window.pin.sh); the old machine-local shim is gone with the update -bindd = Control Alt, Delete, $d logout menu, exec, $scrPath/logoutlaunch.sh -bindd = Control Alt, W, $d toggle waybar, exec, killall waybar || waybar -c ~/.config/waybar/layouts/custom.jsonc -s ~/.config/waybar/layouts/custom.css -# bindd = Control Alt, W, $d toggle waybar, exec, hyde-shell waybar --hide -# bindd = Alt_R, Control_R, W, $d toggle waybar and reload config, exec, killall waybar || (env reload_flag=1 $scrPath/wbarconfgen.sh) # was throwing error -# bindd = ALT_R, Control_R,toggle waybar, exec, killall waybar || waybar # toggle waybar without reloading, this is faster - -$d=[$wm|Group Navigation] -bindd = $mainMod Control, H, $d change active group backwards , changegroupactive, b -bindd = $mainMod Control, L, $d change active group forwards , changegroupactive, f - -$d=[$wm|Change focus] -bindd = $mainMod, Left, $d focus left, movefocus, l -bindd = $mainMod, Right, $d focus right , movefocus, r -bindd = $mainMod, Up, $d focus up , movefocus, u -bindd = $mainMod, Down , $d focus 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 -# Super+Ctrl h/l intentionally share a combo with Group Navigation above: hyprland fires -# every bind matching a combo, and changegroupactive is a no-op outside a group. -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 - -$d=[$wm|Resize Active Window] -# Resize windows -bindde = $mainMod Shift, Right, $d resize window right , resizeactive, 30 0 -bindde = $mainMod Shift, Left, $d resize window left, resizeactive, -30 0 -bindde = $mainMod Shift, Up, $d resize window up, resizeactive, 0 -30 -bindde = $mainMod Shift, Down, $d resize window 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 - -# 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 swapwindow l -bindde = $mainMod Alt, right, Move activewindow to the right, exec, $moveactivewindow 30 0 || hyprctl dispatch swapwindow r -bindde = $mainMod Alt, up, Move activewindow up, exec, $moveactivewindow 0 -30 || hyprctl dispatch swapwindow u -bindde = $mainMod Alt, down, Move activewindow down, exec, $moveactivewindow 0 30 || hyprctl dispatch swapwindow d -bindde = $mainMod Alt, h, Move activewindow to the left, exec, $moveactivewindow -30 0 || hyprctl dispatch swapwindow l -bindde = $mainMod Alt, l, Move activewindow to the right, exec, $moveactivewindow 30 0 || hyprctl dispatch swapwindow r -bindde = $mainMod Alt, k, Move activewindow up, exec, $moveactivewindow 0 -30 || hyprctl dispatch swapwindow u -bindde = $mainMod Alt, j, Move activewindow down, exec, $moveactivewindow 0 30 || hyprctl dispatch swapwindow d -# (Super+Alt+Ctrl h/l belong to "move window to relative workspace" below; the movewindow -# variant duplicated the swapwindow block above and double-fired with those binds) - -# Move/Resize focused window -$d=[$wm|Move & Resize with mouse] -binddm = $mainMod, mouse:272, $d hold to move window, movewindow -binddm = $mainMod, mouse:273, $d hold to resize window, resizewindow -binddm = $mainMod, Z, $d hold to move window , movewindow -binddm = $mainMod, X, $d hold to resize window, resizewindow - -# Toggle focused window split -$d=[$wm] -bindd = $mainMod, J, $d toggle split, layoutmsg, togglesplit - -$l=Launcher -$d=[$l|Apps] -bindd = $mainMod, T, $d terminal emulator , exec, $TERMINAL -bindd = $mainMod Shift, T, $d terminal emulator , exec, alacritty -bindd = Control Alt, T, $d terminal emulator , exec, $TERMINAL -# bindd = Control Shift, Alt, T, $d terminal emulator , exec, tilix # launch tilix terminal emulator - using for ssh -bindd = $mainMod Control, T, $d terminal emulator, exec, tilix # launch tilix terminal emulator -bindd = $mainMod, R, $d pyradio, exec, kitty --session ~/.config/kitty/music.conf -bindd = $mainMod, E, $d file explorer , exec, $EXPLORER -bindd = $mainMod, C, $d neovim, exec, $TERMINAL nvim #editor -bindd = $mainMod, B, $d web browser , exec, $BROWSER -bindd = $mainMod Shift, B, $d web browser , exec, librewolf -bindd = $mainMod Control, B, $d web browser , exec, firefox -bindd = $mainMod, Y, $d ferdium, exec, ferdium -bindd = Control Shift, Escape, $d system monitor , exec, $scrPath/system.monitor.sh # renamed upstream (was sysmonlaunch.sh) -bindd = $mainMod, F, $d freetube , exec, freetube # youtube alternative -bindd = $mainMod, K, $d password manager , exec, keepassxc # launch password manager -bindd = $mainMod, G, $d xmpp, exec, gajim # launch messanger -bindd = $mainMod, M, $d betterbird, exec, betterbird - -# bindd = $mainMod, M, exec, $notes # launch notes app -# bindd = Ctrl+Shift, Escape, exec, $scrPath/sysmonlaunch.sh # launch system monitor (htop/btop or fallback to top) - -$d=[$l|Rofi menus] -$rofi-launch=$scrPath/rofilaunch.sh -bindd = $mainMod, A, $d application finder , exec, pkill -x rofi || $rofi-launch d -bindd = $mainMod, TAB, $d window switcher , exec, pkill -x rofi || $rofi-launch w -bindd = $mainMod Shift, E, $d file finder , exec, pkill -x rofi || $rofi-launch f -bindd = $mainMod, slash, $d keybindings hint, exec, pkill -x rofi || $scrPath/keybinds_hint.sh c # launch keybinds hint -bindd = $mainMod, comma, $d emoji picker , exec, pkill -x rofi || $scrPath/emoji-picker.sh # launch emoji picker -bindd = $mainMod, period, $d glyph picker , exec, pkill -x rofi || $scrPath/glyph-picker.sh # launch glyph picker -bindd = $mainMod, V, $d clipboard , exec, pkill -x rofi || $scrPath/cliphist.sh -c # launch clipboard, -bindd = $mainMod Shift, V, $d clipboard manager , exec, pkill -x rofi || $scrPath/cliphist.sh # launch clipboard Manager -bindd = $mainMod Shift, A, $d select rofi launcher , exec, pkill -x rofi || $scrPath/rofiselect.sh # launch select menu - - -$hc=Hardware Controls -$d=[$hc|Audio] -binddl = , F10, $d toggle mute output , exec, $scrPath/volumecontrol.sh -o m # toggle audio mute -binddl = , XF86AudioMute,$d toggle mute output, exec, $scrPath/volumecontrol.sh -o m # toggle audio mute -# binddel = , F11, $d decrease volume , exec, $scrPath/volumecontrol.sh -o d # decrease volume -# binddel = , F12, $d increase volume , exec, $scrPath/volumecontrol.sh -o i # increase volume -binddl = , XF86AudioMicMute,$d un/mute microphone , exec, $scrPath/volumecontrol.sh -i m # toggle microphone mute -binddel = , XF86AudioLowerVolume, $d decrease volume , exec, $scrPath/volumecontrol.sh -o d # decrease volume -binddel = , XF86AudioRaiseVolume, $d increase volume , exec, $scrPath/volumecontrol.sh -o i # increase volume - -$d=[$hc|Media] -binddl = , XF86AudioPlay,$d play media, exec, playerctl play-pause # toggle between media play and pause -binddl = , XF86AudioPause,$d pause media, exec, playerctl play-pause # toggle between media play and pause -binddl = , XF86AudioNext,$d next media , exec, playerctl next # media next -binddl = , XF86AudioPrev,$d previous media , exec, playerctl previous # media previous - -$d=[$hc|Brightness] -binddel = , XF86MonBrightnessUp, $d increase brightness , exec, $scrPath/brightnesscontrol.sh i # increase brightness -binddel = , XF86MonBrightnessDown, $d decrease brightness , exec, $scrPath/brightnesscontrol.sh d # decrease brightness - - -$ut=Utilities -$d=[$ut] -# binddl = $mainMod, K, $d toggle keyboard layout , exec, $scrPath/keyboardswitch.sh # switch keyboard layout -bindd = $mainMod Alt, G, $d game mode , exec, $scrPath/gamemode.sh # disable hypr effects for gamemode - -$d=[$ut|Screen Capture] -bindd = $mainMod Shift, P, $d color picker, exec, hyprpicker -an # Pick color (Hex) >> clipboard# -bindd = $mainMod, P, $d snip screen , exec, $scrPath/screenshot.sh s # partial screenshot capture -bindd = $mainMod Control, P, $d freeze and snip screen, exec, $scrPath/screenshot.sh sf # partial screenshot capture (frozen screen) -binddl = $mainMod Alt, P, $d print monitor , exec, $scrPath/screenshot.sh m # monitor screenshot capture -binddl = , Print, $d print all monitors , exec, $scrPath/screenshot.sh p # all monitors screenshot capture - -# TODO Make a main rofi menu for these selectors -$rice=Theming and Wallpaper -$d=[$rice] -bindd = $mainMod Alt Control Shift, Right, $d next global wallpaper , exec, $scrPath/wallpaper.sh -Gn # next global wallpaper -bindd = $mainMod Alt Control Shift, Left, $d previous global wallpaper , exec, $scrPath/wallpaper.sh -Gp # previous global wallpaper -bindd = $mainMod Shift, W, $d select a global wallpaper , exec, pkill -x rofi || $scrPath/wallpaper.sh -SG # launch wallpaper select menu -# bindd = $mainMod Alt, Up, $d next waybar layout , exec, $scrPath/wbarconfgen.sh n # next waybar mode -# bindd = $mainMod Alt, Down, $d previous waybar layout , exec, $scrPath/wbarconfgen.sh p # previous waybar mode -bindd = $mainMod Shift, R, $d reload hyprland + waybar, exec, hyprctl reload; killall waybar; setsid -f waybar -c ~/.config/waybar/layouts/custom.jsonc -s ~/.config/waybar/layouts/custom.css -# bindd = $mainMod Shift, R, $d wallbash mode selector , exec, pkill -x rofi || $scrPath/wallbashtoggle.sh -m # (HyDE default; needs framework) -# bindd = $mainMod Shift, T, $d select a theme, exec, pkill -x rofi || $scrPath/themeselect.sh # launch theme select menu - - - -$ws=Workspaces -$d=[$ws|Navigation] -bindd = $mainMod, 1, $d navigate to workspace 1 , workspace, r~1 -bindd = $mainMod, 2, $d navigate to workspace 2 , workspace, r~2 -bindd = $mainMod, 3, $d navigate to workspace 3 , workspace, r~3 -bindd = $mainMod, 4, $d navigate to workspace 4 , workspace, r~4 -bindd = $mainMod, 5, $d navigate to workspace 5 , workspace, r~5 -bindd = $mainMod, 6, $d navigate to workspace 6 , workspace, r~6 -bindd = $mainMod, 7, $d navigate to workspace 7 , workspace, r~7 -bindd = $mainMod, 8, $d navigate to workspace 8 , workspace, r~8 -bindd = $mainMod, 9, $d navigate to workspace 9 , workspace, r~9 -bindd = $mainMod, 0, $d navigate to workspace 10 , workspace, r~10 - -$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 - -$d=[$ws|Navigation] -# E for "empty" — Super+Ctrl+Down is taken by movefocus above -bindd = $mainMod Control, E, $d navigate to the nearest empty workspace , workspace, empty - -# Move focused window to a workspace -$d=[$ws|Move window to workspace] -bindd = $mainMod Control, 1, $d move to workspace 1 , movetoworkspace, r~1 -bindd = $mainMod Control, 2, $d move to workspace 2 , movetoworkspace, r~2 -bindd = $mainMod Control, 3, $d move to workspace 3 , movetoworkspace, r~3 -bindd = $mainMod Control, 4, $d move to workspace 4 , movetoworkspace, r~4 -bindd = $mainMod Control, 5, $d move to workspace 5 , movetoworkspace, r~5 -bindd = $mainMod Control, 6, $d move to workspace 6 , movetoworkspace, r~6 -bindd = $mainMod Control, 7, $d move to workspace 7 , movetoworkspace, r~7 -bindd = $mainMod Control, 8, $d move to workspace 8 , movetoworkspace, r~8 -bindd = $mainMod Control, 9, $d move to workspace 9 , movetoworkspace, r~9 -bindd = $mainMod Control, 0, $d move to workspace 10 , movetoworkspace, r~10 - -# Move focused window to a relative workspace -$d=[$ws] -bindd = $mainMod Control Alt, Right, $d move window to next relative workspace , movetoworkspace, r+1 -bindd = $mainMod Control Alt, Left, $d move window to previous relative workspace , movetoworkspace, r-1 -bindd = $mainMod Control Alt, l, $d move window to next relative workspace , movetoworkspace, r+1 -bindd = $mainMod Control Alt, h, $d move window to previous relative workspace , movetoworkspace, r-1 - -# Scroll through existing workspaces -$d=[$ws|Navigation] -bindd = $mainMod, mouse_down, $d next workspace, workspace, e+1 -bindd = $mainMod, mouse_up, $d previous workspace, workspace, e-1 - - -# Move/Switch to special workspace (scratchpad) -$d=[$ws|Navigation|Special workspace] -bindd = $mainMod Shift, S, $d move to scratchpad , movetoworkspace, special -bindd = $mainMod Alt, S, $d move to scratchpad (silent) , movetoworkspacesilent, special -bindd = $mainMod, S, $d toggle scratchpad , togglespecialworkspace - -# Move focused window to a workspace silently -$d=[$ws|Navigation|Move window silently] -bindd = $mainMod Alt, 1, $d move to workspace 1 (silent), movetoworkspacesilent, r~1 -bindd = $mainMod Alt, 2, $d move to workspace 2 (silent), movetoworkspacesilent, r~2 -bindd = $mainMod Alt, 3, $d move to workspace 3 (silent), movetoworkspacesilent, r~3 -bindd = $mainMod Alt, 4, $d move to workspace 4 (silent), movetoworkspacesilent, r~4 -bindd = $mainMod Alt, 5, $d move to workspace 5 (silent), movetoworkspacesilent, r~5 -bindd = $mainMod Alt, 6, $d move to workspace 6 (silent), movetoworkspacesilent, r~6 -bindd = $mainMod Alt, 7, $d move to workspace 7 (silent), movetoworkspacesilent, r~7 -bindd = $mainMod Alt, 8, $d move to workspace 8 (silent), movetoworkspacesilent, r~8 -bindd = $mainMod Alt, 9, $d move to workspace 9 (silent), movetoworkspacesilent, r~9 -bindd = $mainMod Alt, 0, $d move to workspace 10 (silent), movetoworkspacesilent, r~10 - - - -$d=#! unset the group name diff --git a/gui/.config/hypr/monitors.conf b/gui/.config/hypr/monitors.conf deleted file mode 100644 index 89727e0..0000000 --- a/gui/.config/hypr/monitors.conf +++ /dev/null @@ -1,6 +0,0 @@ -# Default monitor config (fallback shared by all GUI hosts). -# Per-host overlays (lw/, wm/, fl/, …) override this with their own monitors.conf via -# stow --override. It exists here so `source = ./monitors.conf` in hyprland.conf always -# resolves — even mid-install, before the host overlay is stowed (avoids the transient -# "source globbing error: found no match"). -monitor = , preferred, auto, 1 diff --git a/gui/.config/hypr/userprefs.conf b/gui/.config/hypr/userprefs.conf deleted file mode 100644 index ef4e1c4..0000000 --- a/gui/.config/hypr/userprefs.conf +++ /dev/null @@ -1,45 +0,0 @@ - -# █░█ █▀ █▀▀ █▀█   █▀█ █▀█ █▀▀ █▀▀ █▀ -# █▄█ ▄█ ██▄ █▀▄   █▀▀ █▀▄ ██▄ █▀░ ▄█ - -# Set your personal hyprland configuration here - - -# BAR BLUR -# -# The blur behind the bars only ever came from HyDE's *generated* `hypr/themes/theme.conf`, -# which this repo deliberately does not carry (HyDE writes it per host) — so without this -# block the bars have no blur behind them. That generated file also still ships the pre-0.45 -# form (`layerrule = blur,waybar`), which current hyprland rejects; HyDE wraps the source in -# `hyprlang noerror true`, so it fails silently either way. Re-declared here in the current -# `,match:namespace ` syntax, matching windowrules.conf. `layerrule` is additive. -# Keep comments on their own lines: a trailing one is parsed as part of the value. -# (`cavabar` is the standalone cava bar from waybar/layouts/custom.jsonc.) -layerrule = blur true,match:namespace waybar -layerrule = ignore_alpha 0,match:namespace waybar -layerrule = blur true,match:namespace cavabar -layerrule = ignore_alpha 0,match:namespace cavabar - - - -# // █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█ -# // █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█ - -# (xremap lives in lw's host.conf — it's the only host with an xremap config; wm uses evremap) -env = SSH_AUTH_SOCK,$XDG_RUNTIME_DIR/ssh-agent.socket # ssh on start -env = ELECTRON_OZONE_PLATFORM_HINT,x11 # Disable after hyprland fixes electron flickering -exec-once = waybar -c ~/.config/waybar/layouts/custom.jsonc -s ~/.config/waybar/layouts/custom.css -# Steam Controller -> Deck-style binds. Not a systemd user unit: graphical-session.target -exec-once = scc-daemon start - -# (No static `noanim` layerrule: current Hyprland rejects it in the match: syntax, and -# grimblast already suppresses selection animation at runtime via `hyprctl keyword layerrule`.) - - -# I know there is like two 'solutions' already, so have third one - (tested GTX 1060, 555.58.02 drivers) - run flickery apps with --disable-gpu-compositing. This makes apps slightly less smooth (GPU still used tho), but fixes the issue. I think this is better then to disable damage_tracking, which works too. - -# render { -# explicit_sync = 1 -# explicit_sync_kms = 1 -# direct_scanout = no -# } diff --git a/gui/.config/hypr/windowrules.conf b/gui/.config/hypr/windowrules.conf deleted file mode 100644 index aba683f..0000000 --- a/gui/.config/hypr/windowrules.conf +++ /dev/null @@ -1,145 +0,0 @@ - -# █░█░█ █ █▄░█ █▀▄ █▀█ █░█░█   █▀█ █░█ █░░ █▀▀ █▀ -# ▀▄▀▄▀ █ █░▀█ █▄▀ █▄█ ▀▄▀▄▀   █▀▄ █▄█ █▄▄ ██▄ ▄█ - -# See https://wiki.hypr.land/Configuring/Window-Rules/ - -#? ------- WINDOWRULES_HYPRLAND_V_0_53 COMPATIBLE ---------------- -# This Config is compatible with windowrules changes in Hyprland v0.53+ -# Any future breaking changes disables this block and notifies the user to update. -# User template files will be provided in $XDG_DATA_HOME/hyde/templates/hypr/windowrules.conf for manual update. - -# hyprlang if WINDOWRULES_HYPRLAND_V_0_53 - -# idle_inhibit rules -windowrule = idle_inhibit fullscreen, match:class ^(.*celluloid.*)$|^(.*mpv.*)$|^(.*vlc.*)$ -windowrule = idle_inhibit fullscreen, match:class ^(.*[Ss]potify.*)$ -windowrule = idle_inhibit fullscreen, match:class ^(.*LibreWolf.*)$|^(.*floorp.*)$|^(.*brave-browser.*)$|^(.*firefox.*)$|^(.*chromium.*)$|^(.*zen.*)$|^(.*vivaldi.*)$ - -# Picture-in-Picture -windowrule { - name = hyde_picture_in_picture - match:title = ^([Pp]icture[-\s]?[Ii]n[-\s]?[Pp]icture)(.*)$ - tag = +picture-in-picture - tag = +hyde_picture_in_picture - float = true - keep_aspect_ratio = true - move = (monitor_w*0.73) (monitor_h*0.72) - size = (monitor_w*0.25) (monitor_h*0.25) - pin = true -} - -windowrule = opacity 0.90 $& 0.90 $& 1,match:class ^(firefox)$ -windowrule = opacity 0.90 $& 0.90 $& 1,match:class ^(zen)$ -windowrule = opacity 0.90 $& 0.90 $& 1,match:class ^(brave-browser)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(code-oss)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^([Cc]ode)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(code-url-handler)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(code-insiders-url-handler)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(kitty)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(org.kde.dolphin)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(org.kde.ark)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(nwg-look)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(qt5ct)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(qt6ct)$ -windowrule = opacity 0.80 $& 0.80 $& 1,match:class ^(kvantummanager)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(org.pulseaudio.pavucontrol)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(blueman-manager)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(nm-applet)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(nm-connection-editor)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(hyprpolkitagent)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(org.freedesktop.impl.portal.desktop.gtk)$ -windowrule = opacity 0.80 $& 0.70 $& 1,match:class ^(org.freedesktop.impl.portal.desktop.hyprland)$ -windowrule = opacity 0.70 $& 0.70 $& 1,match:class ^([Ss]team)$ -windowrule = opacity 0.70 $& 0.70 $& 1,match:class ^(steamwebhelper)$ -windowrule = opacity 0.70 $& 0.70 $& 1,match:class ^([Ss]potify)$ -windowrule = opacity 0.70 $& 0.70 $& 1,match:initial_title ^(Spotify Free)$ -windowrule = opacity 0.70 $& 0.70 $& 1,match:initial_title ^(Spotify Premium)$ -windowrule = opacity 1.00 $& 1.00 $& 1,match:class ^(blender)$ - -windowrule = opacity 0.90 0.90,match:class ^(com.github.rafostar.Clapper)$ # Clapper-Gtk -windowrule = opacity 0.80 0.80,match:class ^(com.github.tchx84.Flatseal)$ # Flatseal-Gtk -windowrule = opacity 0.80 0.80,match:class ^(hu.kramo.Cartridges)$ # Cartridges-Gtk -windowrule = opacity 0.80 0.80,match:class ^(com.obsproject.Studio)$ # Obs-Qt -windowrule = opacity 0.80 0.80,match:class ^(gnome-boxes)$ # Boxes-Gtk -windowrule = opacity 0.80 0.80,match:class ^(vesktop)$ # Vesktop -windowrule = opacity 0.80 0.80,match:class ^(discord)$ # Discord-Electron -windowrule = opacity 0.80 0.80,match:class ^(WebCord)$ # WebCord-Electron -windowrule = opacity 0.80 0.80,match:class ^(ArmCord)$ # ArmCord-Electron -windowrule = opacity 0.80 0.80,match:class ^(app.drey.Warp)$ # Warp-Gtk -windowrule = opacity 0.80 0.80,match:class ^(net.davidotek.pupgui2)$ # ProtonUp-Qt -windowrule = opacity 0.80 0.80,match:class ^(yad)$ # Protontricks-Gtk -windowrule = opacity 0.80 0.80,match:class ^(Signal)$ # Signal-Gtk -windowrule = opacity 0.80 0.80,match:class ^(io.github.alainm23.planify)$ # planify-Gtk -windowrule = opacity 0.80 0.80,match:class ^(io.gitlab.theevilskeleton.Upscaler)$ # Upscaler-Gtk -windowrule = opacity 0.80 0.80,match:class ^(com.github.unrud.VideoDownloader)$ # VideoDownloader-Gtk -windowrule = opacity 0.80 0.80,match:class ^(io.gitlab.adhami3310.Impression)$ # Impression-Gtk -windowrule = opacity 0.80 0.80,match:class ^(io.missioncenter.MissionCenter)$ # MissionCenter-Gtk -windowrule = opacity 0.80 0.80,match:class ^(io.github.flattool.Warehouse)$ # Warehouse-Gtk - -windowrule = float true,match:class ^(Signal)$ # Signal-Gtk -windowrule = float true,match:class ^(com.github.rafostar.Clapper)$ # Clapper-Gtk -windowrule = float true,match:class ^(app.drey.Warp)$ # Warp-Gtk -windowrule = float true,match:class ^(net.davidotek.pupgui2)$ # ProtonUp-Qt -windowrule = float true,match:class ^(yad)$ # Protontricks-Gtk -windowrule = float true,match:class ^(eog)$ # Imageviewer-Gtk -windowrule = float true,match:class ^(io.github.alainm23.planify)$ # planify-Gtk -windowrule = float true,match:class ^(io.gitlab.theevilskeleton.Upscaler)$ # Upscaler-Gtk -windowrule = float true,match:class ^(com.github.unrud.VideoDownloader)$ # VideoDownloader-Gkk -windowrule = float true,match:class ^(io.gitlab.adhami3310.Impression)$ # Impression-Gtk -windowrule = float true,match:class ^(io.missioncenter.MissionCenter)$ # MissionCenter-Gtk -windowrule = float true,match:title ^(Friends List)$ # Steam Friends List -windowrule = float true,match:title ^(Steam Settings)$ # Steam Settings -windowrule = float true,match:initial_title ^(Image Editor)$,match:class ^(blender)$ # Blender Render -windowrule = size (monitor_w*0.5) (monitor_h*0.5),match:initial_title ^(Image Editor)$,match:class ^(blender)$ -windowrule = float true, match:initial_title ^(Ghidra: NO ACTIVE PROJECT) #Ghidra Project manager - -# workaround for jetbrains IDEs dropdowns/popups cause flickering -windowrule = no_initial_focus true,match:class ^(.*jetbrains.*)$,match:title ^(win[0-9]+)$ - -# █░░ ▄▀█ █▄█ █▀▀ █▀█   █▀█ █░█ █░░ █▀▀ █▀ -# █▄▄ █▀█ ░█░ ██▄ █▀▄   █▀▄ █▄█ █▄▄ ██▄ ▄█ - -layerrule = blur true,match:namespace rofi -layerrule = ignore_alpha 0,match:namespace rofi -layerrule = blur true,match:namespace notifications -layerrule = ignore_alpha 0,match:namespace notifications -layerrule = blur true,match:namespace swaync-notification-window -layerrule = ignore_alpha 0,match:namespace swaync-notification-window -layerrule = blur true,match:namespace swaync-control-center -layerrule = ignore_alpha 0,match:namespace swaync-control-center -layerrule = blur true,match:namespace logout_dialog - -# █▀▄ █▀█ ▀█▀ █▀ █▀▀ █░█ █▀ ▀█▀ █▀█ █▀▄▀█ -# █▄▀ █▄█ ░█░ ▄█ █▄▄ █▄█ ▄█ ░█░ █▄█ █░▀░█ -# dots-only rules preserved from the legacy windowrules.conf (converted to v0.53 syntax) - -# float utility / settings / dialog windows -windowrule = float true,match:class ^(org.kde.dolphin)$,match:title ^(Progress Dialog — Dolphin)$ -windowrule = float true,match:class ^(org.kde.dolphin)$,match:title ^(Copying — Dolphin)$ -windowrule = float true,match:title ^(About Mozilla Firefox)$ -windowrule = float true,match:class ^(firefox)$,match:title ^(Library)$ -windowrule = float true,match:class ^(kitty)$,match:title ^(top)$ -windowrule = float true,match:class ^(kitty)$,match:title ^(btop)$ -windowrule = float true,match:class ^(kitty)$,match:title ^(htop)$ -windowrule = float true,match:class ^(vlc)$ -windowrule = float true,match:class ^(kvantummanager)$ -windowrule = float true,match:class ^(qt5ct)$ -windowrule = float true,match:class ^(qt6ct)$ -windowrule = float true,match:class ^(nwg-look)$ -windowrule = float true,match:class ^(org.kde.ark)$ -windowrule = float true,match:class ^(org.pulseaudio.pavucontrol)$ -windowrule = float true,match:class ^(blueman-manager)$ -windowrule = float true,match:class ^(nm-applet)$ -windowrule = float true,match:class ^(nm-connection-editor)$ -windowrule = float true,match:class ^(org.kde.polkit-kde-authentication-agent-1)$ -windowrule = float true,match:class ^(xdg-desktop-portal-gtk)$ - -# common file-dialog modals -windowrule = float true,match:title ^(Open)$ -windowrule = float true,match:title ^(Choose Files)$ -windowrule = float true,match:title ^(Save As)$ -windowrule = float true,match:title ^(Confirm to replace files)$ -windowrule = float true,match:title ^(File Operation Progress)$ - -# hyprlang endif diff --git a/gui/.config/kitty/kitty.conf b/gui/.config/kitty/kitty.conf index ae30935..6008420 100644 --- a/gui/.config/kitty/kitty.conf +++ b/gui/.config/kitty/kitty.conf @@ -1,5 +1,19 @@ +# Colours: HyDE owns both includes; the repo tracks only its own colour changes below. +# - theme.conf: HyDE renders the current theme's kitty.theme here on every theme switch +# (color.set.sh, by mv, so it's always a real file and never a link into this repo). +# Untracked since 2026-11-09; a missing one is silently ignored, so kitty would fall back +# to its default palette (bin/hyde-materialize.sh recovers it on a host after the pull). +# - hyde.conf: what HyDE's wallbash kitty.sh puts at the top. Absent on most hosts; harmless. +include hyde.conf include theme.conf +# The repo's own colours. They come after the include, so they win (later definitions +# override) over EVERY HyDE theme, wallbash included, not just Catppuccin Mocha. +background #262335 +foreground #ffffff +# HyDE's Mocha sets #11111B; none = the tab bar uses the background above +tab_bar_background none + font_size 12.0 font_family CaskaydiaCove Nerd Font Mono bold_font auto @@ -14,6 +28,11 @@ tab_bar_style powerline tab_powerline_style slanted confirm_os_window_close 0 +# Remembering size also remembers the maximize state (~/.cache/kitty/main.json): +# close one maximized kitty and every new one opens maximized under Hyprland. +remember_window_size no +initial_window_width 100c +initial_window_height 30c #background_opacity 0.60 #hide_window_decorations yes diff --git a/gui/.config/kitty/theme.conf b/gui/.config/kitty/theme.conf deleted file mode 100644 index 700f0f4..0000000 --- a/gui/.config/kitty/theme.conf +++ /dev/null @@ -1,78 +0,0 @@ - -## name: Catppuccin Mocha 🌿 -## author: Pocco81 (https://github.com/Pocco81) -## license: MIT -## upstream: https://github.com/catppuccin/kitty/blob/main/mocha.conf -## blurb: Soothing pastel theme for the high-spirited! - - - -# The basic colors -foreground #ffffff -background #262335 -selection_foreground #1E1E2E -selection_background #F5E0DC - -# Cursor colors -cursor #F5E0DC -cursor_text_color #1E1E2E - -# URL underline color when hovering with mouse -url_color #B4BEFE - -# Kitty window border colors -active_border_color #CBA6F7 -inactive_border_color #8E95B3 -bell_border_color #EBA0AC - -# OS Window titlebar colors -wayland_titlebar_color system -macos_titlebar_color system - -# Tab bar colors -active_tab_foreground #11111B -active_tab_background #CBA6F7 -inactive_tab_foreground #CDD6F4 -inactive_tab_background #181825 - -# Colors for marks (marked text in the terminal) -mark1_foreground #1E1E2E -mark1_background #87B0F9 -mark2_foreground #1E1E2E -mark2_background #CBA6F7 -mark3_foreground #1E1E2E -mark3_background #74C7EC - -# The 16 terminal colors - -# black -color0 #43465A -color8 #43465A - -# red -color1 #F38BA8 -color9 #F38BA8 - -# green -color2 #A6E3A1 -color10 #A6E3A1 - -# yellow -color3 #F9E2AF -color11 #F9E2AF - -# blue -color4 #87B0F9 -color12 #87B0F9 - -# magenta -color5 #F5C2E7 -color13 #F5C2E7 - -# cyan -color6 #94E2D5 -color14 #94E2D5 - -# white -color7 #CDD6F4 -color15 #A1A8C9 diff --git a/gui/.config/uwsm/env-hyprland.d/10-dots.sh b/gui/.config/uwsm/env-hyprland.d/10-dots.sh index 2af226e..b5d7b79 100644 --- a/gui/.config/uwsm/env-hyprland.d/10-dots.sh +++ b/gui/.config/uwsm/env-hyprland.d/10-dots.sh @@ -1,28 +1,25 @@ #!/usr/bin/env sh -# Keep Hyprland on this repo's hyprlang config instead of HyDE's Lua one. +# Pin Hyprland to HyDE's Lua entry point, explicitly. # -# HyDE 26.x moved its Hyprland config to Lua. Its own `00-hyde.sh` (synced on -# every update) points HYPRLAND_CONFIG at ~/.local/share/hypr/hyde.lua, and -# Hyprland then never reads ~/.config/hypr/hyprland.conf — so every dots hypr -# file (theme, keybinds, window rules, monitors, userprefs) is silently bypassed -# and the session comes up as stock HyDE: no rounding/gaps/borders, HyDE's binds, -# HyDE's plain waybar instead of the cava rig. Nothing errors; `hyprctl -# configerrors` stays empty. The tell is the log's first lines: -# [cfg] Config is either explicit or special. / [cfg] Config is lua, loading lua mgr -# -# `00-hyde.sh` assigns with `${HYPRLAND_CONFIG:-...}` and `env-hyprland` sources +# HyDE's own `00-hyde.sh` (synced on every update) assigns HYPRLAND_CONFIG with +# `${HYPRLAND_CONFIG:-...}` after sourcing shell/activate; env-hyprland sources # `env-hyprland.d/*.sh` in glob order, so this file (10 sorts after 00) wins by -# assigning unconditionally. Session-start only — this needs a relogin, not a -# `hyprctl reload`. HyDE syncs the whole env-hyprland.d/ directory but only ever -# writes its own files into it, so this one survives updates. +# assigning unconditionally. Keeping the pin alive — rather than deleting this +# file and trusting HyDE's default — preserves the invariant that the dots repo +# decides which config Hyprland starts with, and makes any rollback a one-line +# edit here. Session-start only: relogin, not `hyprctl reload`. # -# ── STOPGAP, not the destination ────────────────────────────────────────────── -# It pins ~/.local/share/hyde/hyprland.conf, which current HyDE no longer ships: -# ours is a leftover from the pre-update install, so nothing regenerates it. And -# 8 scripts our binds reach for through $scrPath are already gone from the new -# lib/hyde — including dontkillsteam.sh, which is bound to Super+Q and Alt+F4. -# The supported path is porting the hypr layer into ~/.config/hypr/hyprland.lua -# (the one file HyDE promises never to overwrite). See HYDE-UPDATE.md. -HYPRLAND_CONFIG="$HOME/.config/hypr/hyprland.conf" +# The Lua chain: hyde.lua loads HyDE's lua/ modules, 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 for the full map, flip-day steps and rollback. +# +# 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 this and +# the fish twin (gui/.config/fish/conf.d/00-hypr-config.fish) back at +# "$HOME/.config/hypr/hyprland.conf" and relogin. d851213 is the last +# hyprlang `main` (the port merged 2026-11-09). +HYPRLAND_CONFIG="${XDG_DATA_HOME:-$HOME/.local/share}/hypr/hyde.lua" export HYPRLAND_CONFIG diff --git a/gui/.config/waybar/layouts/custom.jsonc b/gui/.config/waybar/layouts/custom.jsonc index 27b6323..66f2678 100644 --- a/gui/.config/waybar/layouts/custom.jsonc +++ b/gui/.config/waybar/layouts/custom.jsonc @@ -56,7 +56,6 @@ "(.*)Ferdium": "󰭻 Ferdium", "(.*)FreeTube": " FreeTube", "(.*) - VLC media player": "󰕼 $1", - "(.*)Steam": "󰓓 Steam", "(.*)Neovide": " Nvim", "(.*)Neovim": " Nvim", "(.*)Nvim": " Nvim", @@ -279,7 +278,7 @@ "battery": { // Pinned to the laptop battery on purpose. Unpinned, waybar 0.15 scans every // /sys/class/power_supply node, and a flapping one (the DualShock's - // ps-controller-battery-* appearing at login while opensdd/scc-daemon wake it) + // ps-controller-battery-* appearing at login when something wakes the pad) // makes refreshBatteries() throw off its worker thread → SIGABRT, killing the // whole bar (battery.cpp:142 uncaught; all 5 of 2026-08-17's coredumps). The pin // (battery.cpp:117) gates every read/watch of non-matching nodes. On wm no BAT0 diff --git a/gui/.config/zellij/config.kdl b/gui/.config/zellij/config.kdl index f2ecf85..9f38e36 100644 --- a/gui/.config/zellij/config.kdl +++ b/gui/.config/zellij/config.kdl @@ -314,7 +314,7 @@ load_plugins { // Choose the theme that is specified in the themes section. // Default: default // -theme "gruvbox-dark" // defined in themes/gruvbox-dark.kdl +theme "gruvbox-dark" // built into zellij (identical to the themes/ file this repo used to ship) // Choose the base input mode of zellij. // Default: normal diff --git a/gui/.config/zellij/themes/gruvbox-dark.kdl b/gui/.config/zellij/themes/gruvbox-dark.kdl deleted file mode 100644 index da95f4e..0000000 --- a/gui/.config/zellij/themes/gruvbox-dark.kdl +++ /dev/null @@ -1,120 +0,0 @@ -themes { - gruvbox-dark { - text_unselected { - base 251 241 199 - background 60 56 54 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - text_selected { - base 251 241 199 - background 80 73 69 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - ribbon_selected { - base 60 56 54 - background 152 151 26 - emphasis_0 204 36 29 - emphasis_1 214 93 14 - emphasis_2 177 98 134 - emphasis_3 69 133 136 - } - ribbon_unselected { - base 60 56 54 - background 235 219 178 - emphasis_0 204 36 29 - emphasis_1 251 241 199 - emphasis_2 69 133 136 - emphasis_3 177 98 134 - } - table_title { - base 152 151 26 - background 0 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - table_cell_selected { - base 251 241 199 - background 80 73 69 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - table_cell_unselected { - base 251 241 199 - background 60 56 54 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - list_selected { - base 251 241 199 - background 80 73 69 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - list_unselected { - base 251 241 199 - background 60 56 54 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 152 151 26 - emphasis_3 177 98 134 - } - frame_selected { - base 152 151 26 - background 0 - emphasis_0 214 93 14 - emphasis_1 104 157 106 - emphasis_2 177 98 134 - emphasis_3 0 - } - frame_highlight { - base 214 93 14 - background 0 - emphasis_0 177 98 134 - emphasis_1 214 93 14 - emphasis_2 214 93 14 - emphasis_3 214 93 14 - } - exit_code_success { - base 152 151 26 - background 0 - emphasis_0 104 157 106 - emphasis_1 60 56 54 - emphasis_2 177 98 134 - emphasis_3 69 133 136 - } - exit_code_error { - base 204 36 29 - background 0 - emphasis_0 215 153 33 - emphasis_1 0 - emphasis_2 0 - emphasis_3 0 - } - multiplayer_user_colors { - player_1 177 98 134 - player_2 69 133 136 - player_3 0 - player_4 215 153 33 - player_5 104 157 106 - player_6 0 - player_7 204 36 29 - player_8 0 - player_9 0 - player_10 0 - } - } -} diff --git a/gui/.local/bin/wbar b/gui/.local/bin/wbar new file mode 100755 index 0000000..8c4fe30 --- /dev/null +++ b/gui/.local/bin/wbar @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# wbar — run the custom waybar rig (layouts/custom.jsonc + custom.css) as a +# supervised systemd user service, and control it. +# +# Why a service: waybar 0.15.0 segfaults when the LG 4K drops its DisplayPort +# link on power-off (output removed) and again when it comes back (output +# re-added). Launched as a bare scope nothing relaunched it, so the bar was gone +# every morning. As a transient *service* with Restart=always systemd brings it +# back after RestartSec, at both ends of the cycle. StartLimitIntervalSec=0 keeps +# systemd from giving up while the monitor is gone and Hyprland sits on its +# headless fallback output (the default limit of 5 starts per 10 s would +# reproduce the original "stays dead" behavior). RestartSec=3 keeps that +# potential loop cheap; `systemctl --user show dots-waybar.service -p NRestarts` +# after a night says how many restarts a sleep cycle really costs. +# +# The unit is dots-owned on purpose. HyDE's own bar unit +# (hyde-$XDG_SESSION_DESKTOP-bar.service) is masked on rig hosts (README.md) so +# waybar.py can never spawn a second, plain bar — reusing that name would need +# the mask lifted. Under a different name HyDE keeps seeing "no bar", exactly as +# before. +# +# Because a killed waybar now comes straight back, every killall-based control +# goes through here: +# wbar restart (what the old `wbar` abbr did) also: hc.start.bar +# wbar toggle Ctrl+Alt+W +# wbar reload re-read config + CSS in place (SIGUSR2), no restart +# wbar start|stop|status +set -euo pipefail + +unit="dots-waybar.service" +cfg="$HOME/.config/waybar/layouts/custom.jsonc" +css="$HOME/.config/waybar/layouts/custom.css" + +active() { systemctl --user is-active --quiet "$unit"; } + +start() { + active && return 0 + systemctl --user reset-failed "$unit" 2>/dev/null || true + # A service gets the user manager's environment, not Hyprland's. Forward the + # session vars app2unit forwards for HyDE's own units, plus GTK scaling + # (GDK_SCALE=1 is what keeps the bar sized right on the 1.25-scaled output). + local -a env=() + local v + for v in XDG_VTNR XDG_SEAT XDG_SEAT_PATH XDG_SESSION_ID XDG_SESSION_PATH GDK_SCALE GDK_DPI_SCALE; do + [ -n "${!v:-}" ] && env+=("--setenv=$v=${!v}") + done + systemd-run --user --quiet --collect \ + --unit="$unit" --slice=app-graphical.slice --description="waybar (dots custom rig)" \ + --property=Type=exec --property=ExitType=cgroup \ + --property=Restart=always --property=RestartSec=3 --property=StartLimitIntervalSec=0 \ + --property=After=graphical-session.target --property=PartOf=graphical-session.target \ + "${env[@]}" -- waybar -c "$cfg" -s "$css" +} + +case "${1:-restart}" in + start) start ;; + stop) systemctl --user stop "$unit" ;; + restart) + if active; then systemctl --user restart "$unit"; else start; fi + ;; + toggle) + if active; then systemctl --user stop "$unit"; else start; fi + ;; + reload) systemctl --user kill -s SIGUSR2 "$unit" ;; + status) systemctl --user status --no-pager "$unit" ;; + *) + echo "usage: wbar [start|stop|restart|toggle|reload|status] (default: restart)" >&2 + exit 2 + ;; +esac diff --git a/gui/.local/lib/hyde/windowpin.sh b/gui/.local/lib/hyde/windowpin.sh deleted file mode 100755 index fa4caed..0000000 --- a/gui/.local/lib/hyde/windowpin.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# enable float -WinFloat=$(hyprctl -j clients | jq '.[] | select(.focusHistoryID == 0) | .floating') -WinPinned=$(hyprctl -j clients | jq '.[] | select(.focusHistoryID == 0) | .pinned') - -if [ "${WinFloat}" == "false" ] && [ "${WinPinned}" == "false" ] ; then - hyprctl dispatch togglefloating active -fi - -# toggle pin -hyprctl dispatch pin active - -# disable float -WinFloat=$(hyprctl -j clients | jq '.[] | select(.focusHistoryID == 0) | .floating') -WinPinned=$(hyprctl -j clients | jq '.[] | select(.focusHistoryID == 0) | .pinned') - -if [ "${WinFloat}" == "true" ] && [ "${WinPinned}" == "false" ] ; then - hyprctl dispatch togglefloating active -fi - diff --git a/gui/.local/share/hyde/hyde.conf b/gui/.local/share/hyde/hyde.conf index 31245e3..5b1f6b5 100644 --- a/gui/.local/share/hyde/hyde.conf +++ b/gui/.local/share/hyde/hyde.conf @@ -1,133 +1,9 @@ -#!   ░▒▒▒░░░▓▓          ___________ -#! ░░▒▒▒░░░░░▓▓        //___________/ -#! ░░▒▒▒░░░░░▓▓     _   _ _    _ _____ -#! ░░▒▒░░░░░▓▓▓▓▓▓ | | | | |  | |  __/ -#! ░▒▒░░░░▓▓   ▓▓ | |_| | |_/ /| |___ -#!  ░▒▒░░▓▓   ▓▓   \__  |____/ |____/ █░█ ▄▀█ █▀█ █ ▄▀█ █▄▄ █░░ █▀▀ █▀ -#!    ░▒▓▓   ▓▓  //____/ ▀▄▀ █▀█ █▀▄ █ █▀█ █▄█ █▄▄ ██▄ ▄█ - -# Override configuration for user environment variables and startup commands -# Static variables here take priority over default and dynamic configurations - -#  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 -$BROWSER = firefox # default browser, if commented out , will use the default browser -$EDITOR = code # default editor, if commented out , will use the default editor -$EXPLORER= dolphin # default file manager, if commented out , will use the default file manager -$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 - -# This is used to override the default startup commands -# $start.VAR = command - -$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 -$start.BAR=waybar -$start.NOTIFICATIONS=swaync # dunst -$start.APPTRAY_BLUETOOTH=blueman-applet -$start.WALLPAPER=$scrPath/swwwallpaper.sh -$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 -$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 - -# This is used to override the default environment variables -# $env.VAR = value - -# 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 # 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 # 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 -$ICON_THEME=Tela-circle-dracula -$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 -# $BACKGROUND_PATH=$HYPRLOCK_BACKGROUND # set the background to the specific hyprlock wallpaper +# gui package — the one surviving line of HyDE's retired hyde.conf. +# +# HyDE 26.x ships neither ~/.local/share/hyde/hyde.conf nor hyprland.conf; the repo carried +# copies of both as the hyprlang chain's framework sources until LUA-PORT.md step 8 retired +# that chain (2026-09-10). Nothing in Lua mode reads them — except get_hyprConf() in +# globalcontrol.sh, which HyDE's rofi/menu scripts call for FONT and which falls back to +# `$default.FONT` in exactly this file when the active theme's hypr.theme has no font-name +# line (the Catppuccin themes don't). Without it the menus drop to rofi's default font. +$default.FONT=Cantarell diff --git a/gui/.local/share/hyde/hyprland.conf b/gui/.local/share/hyde/hyprland.conf deleted file mode 100644 index 5e5e09d..0000000 --- a/gui/.local/share/hyde/hyprland.conf +++ /dev/null @@ -1,577 +0,0 @@ -#   ░▒▒▒░░░▓▓          ___________ -# ░░▒▒▒░░░░░▓▓        //___________/ -# ░░▒▒▒░░░░░▓▓     _   _ _    _ _____ -# ░░▒▒░░░░░▓▓▓▓▓▓ | | | | |  | |  __/ -# ░▒▒░░░░▓▓   ▓▓ | |_| | |_/ /| |___ -#  ░▒▒░░▓▓   ▓▓   \__  |____/ |____/ -#    ░▒▓▓   ▓▓  //____/ - -# // █░█ █▄█ █▀▄ █▀▀ -# // █▀█ ░█░ █▄▀ ██▄ - -$scrPath=$HOME/.local/lib/hyde # set scripts path - -# Main modifier -$mainMod = SUPER # windows key -# assign apps - -$default.$QUICKAPPS = -$default.BROWSER = hyde-launch.sh --fall firefox web-browser -$default.EDITOR = hyde-launch.sh --fall code-oss text-editor -$default.EXPLORER= hyde-launch.sh --fall dolphin file-manager -$default.TERMINAL = kitty -$default.LOCKSCREEN=hyprlock -$default.IDLE=hypridle - -$QUICKAPPS = $default.$QUICKAPPS -$BROWSER = $default.BROWSER -$EDITOR = $default.EDITOR -$EXPLORER= $default.EXPLORER -$TERMINAL = $default.TERMINAL -$LOCKSCREEN=$default.LOCKSCREEN -$IDLE= $default.IDLE - - - -# // █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█ -# // █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█ - -# See https://wiki.hyprland.org/Configuring/Keywords/ - -$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 -$start.BAR=waybar -$start.NOTIFICATIONS=dunst -$start.APPTRAY_BLUETOOTH=blueman-applet -$start.WALLPAPER=$scrPath/wallpaper.sh --global -$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 -$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/ - -# Toolkit Backend Variables - https://wiki.hyprland.org/Configuring/Environment-variables/#toolkit-backend-variables -$env.GDK_BACKEND = wayland,x11,* # 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 # 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 # 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 -# $XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)} -# $XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} -# >> $env.XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} -# $XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} - -$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 -$env.XDG_STATE_HOME=$HOME/.local/state - - - -# // █▀▀ ▀█▀ █▄▀ -# // █▄█ ░█░ █░█ - -$default.GTK_THEME=Wallbash-Gtk -$default.ICON_THEME=Tela-circle-dracula -$default.COLOR_SCHEME=prefer-dark - -$GTK_THEME=$default.GTK_THEME -$ICON_THEME=$default.ICON_THEME -$COLOR_SCHEME=$default.COLOR_SCHEME - -# // █▀▀ █░█ █▀█ █▀ █▀█ █▀█ -# // █▄▄ █▄█ █▀▄ ▄█ █▄█ █▀▄ - -$default.CURSOR_THEME=Bibata-Modern-Ice -$default.CURSOR_SIZE=24 - -$CURSOR_THEME=$default.CURSOR_THEME -$CURSOR_SIZE=$default.CURSOR_SIZE - -# // █▀▀ █▀█ █▄░█ ▀█▀ -# // █▀░ █▄█ █░▀█ ░█░ - -$default.FONT=Cantarell -$default.FONT_SIZE=10 -$default.DOCUMENT_FONT=Cantarell -$default.DOCUMENT_FONT_SIZE=10 -$default.MONOSPACE_FONT=CaskaydiaCove Nerd Font Mono -$default.MONOSPACE_FONT_SIZE=9 -$default.NOTIFICATION_FONT=Mononoki Nerd Font Mono -$default.BAR_FONT=JetBrainsMono Nerd Font -# $default.MENU_FONT=This is not yet set as it looks perfect with the global font -$default.FONT_ANTIALIASING=rgba -$default.FONT_HINTING=full - -$FONT=$default.FONT -$FONT_SIZE=$default.FONT_SIZE -$DOCUMENT_FONT=$default.DOCUMENT_FONT -$DOCUMENT_FONT_SIZE=$default.DOCUMENT_FONT_SIZE -$MONOSPACE_FONT=$default.MONOSPACE_FONT -$MONOSPACE_FONT_SIZE=$default.MONOSPACE_FONT_SIZE -$NOTIFICATION_FONT=$default.NOTIFICATION_FONT -$BAR_FONT=$default.BAR_FONT -# $MENU_FONT=$default.MENU_FONT -$FONT_ANTIALIASING=$default.FONT_ANTIALIASING -$FONT_HINTING=$default.FONT_HINTING - - - -# // █▀ █▀█ █▀▀ █▀▀ █ ▄▀█ █░░ -# // ▄█ █▀▀ ██▄ █▄▄ █ █▀█ █▄▄ - -decoration { - dim_special = 0.3 - active_opacity = 0.90 - inactive_opacity = 0.75 - fullscreen_opacity = 1 - blur { - special = true - } -} - -# // █▀▄▀█ █▀█ █▄░█ █ ▀█▀ █▀█ █▀█ -# // █░▀░█ █▄█ █░▀█ █ ░█░ █▄█ █▀▄ - -# See https://wiki.hyprland.org/Configuring/Monitors/ - -monitor = ,preferred,auto,auto - -# // █ █▄░█ █▀█ █░█ ▀█▀ -# // █ █░▀█ █▀▀ █▄█ ░█░ - -# See https://wiki.hyprland.org/Configuring/Variables/ - -input { - kb_layout = us - follow_mouse = 1 - - touchpad { - natural_scroll = no - } - - sensitivity = 0 - force_no_accel = 1 - numlock_by_default = true -} - -# 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 -} - - -# // ▄▀█ █▄░█ █ █▀▄▀█ ▄▀█ ▀█▀ █ █▀█ █▄░█ -# // █▀█ █░▀█ █ █░▀░█ █▀█ ░█░ █ █▄█ █░▀█ - -# 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, once - animation = fade, 1, 10, default - animation = workspaces, 1, 5, wind -} - - -general { - snap { # snapping for floating windows - enabled = true - } -} - - -# // █▀ █▀█ █░█ █▀█ █▀▀ █▀▀ -# // ▄█ █▄█ █▄█ █▀▄ █▄▄ ██▄ - -# hyprlang noerror true -# source = $env.XDG_STATE_HOME/hyde/hyprgui.conf # GUI specific config. Used to not break the low level configs. this will be available downstream -source = $env.XDG_CONFIG_HOME/hypr/themes/colors.conf # Hyde wallbash colors - -# Source groupbar in here -group:groupbar { - col.inactive = rgba($wallbash_pry3ee) - col.active = rgba($wallbash_pry1ee) - col.locked_active = rgba($wallbash_pry2ee) - col.locked_inactive = rgba($wallbash_pry4ee) -} - -source = $env.XDG_CONFIG_HOME/hypr/themes/theme.conf # theme specific settings -source = $env.XDG_CONFIG_HOME/hypr/themes/wallbash.conf # Theme specific settings after Sanitize and handle fallbacks -source = $env.XDG_CONFIG_HOME/hypr/nvidia.conf # Nvidia specific settings -source = $env.XDG_CONFIG_HOME/hypr/animations.conf # source animations variables -# hyprlang noerror false -source = $env.XDG_CONFIG_HOME/hypr/hyde.conf -# hyprlang noerror true -source = $env.XDG_STATE_HOME/hyde/hyprland.conf # translated from config.tomls -source = $ANIMATION_PATH # source animations configuration -# hyprlang noerror false - -#! Below this is an immutable part of the configuration file, and should not be modified by the user. - -#? By Hyprland convention env and startup files are sourced at the end of the main configuration file -#? To ensure that the user's settings are not overridden by the default settings -#? This will let us launch after all envs and variables are set. - -# LEGACY -$quickapps = $QUICKAPPS -$term = $TERMINAL -$editor = $EDITOR -$file = $EXPLORER -$browser = $BROWSER - -# Group Bar -group:groupbar{ - enabled = true - gradients = 1 - render_titles = 1 - font_size = 12 - font_family = $FONT -} - -# Misc - -misc { - font_family = $FONT - -} - - -# // █▀▀ █▄░█ █░█ -# // ██▄ █░▀█ ▀▄▀ - -# # See https://wiki.hyprland.org/Configuring/Environment-variables/ -#! Remove for now -# env = GDK_BACKEND,$env.GDK_BACKEND -# env = SDL_VIDEODRIVER,$env.SDL_VIDEODRIVER -# env = CLUTTER_BACKEND,$env.CLUTTER_BACKEND - -# Qt Variables -env = QT_AUTO_SCREEN_SCALE_FACTOR,$env.QT_AUTO_SCREEN_SCALE_FACTOR -env = QT_QPA_PLATFORM,$env.QT_QPA_PLATFORM -env = QT_WAYLAND_DISABLE_WINDOWDECORATION,$env.QT_WAYLAND_DISABLE_WINDOWDECORATION -env = QT_QPA_PLATFORMTHEME,$env.QT_QPA_PLATFORMTHEME - -# Wayland-ENV -env = MOZ_ENABLE_WAYLAND,$env.MOZ_ENABLE_WAYLAND -env = GDK_SCALE,$env.GDK_SCALE -env = ELECTRON_OZONE_PLATFORM_HINT,$env.ELECTRON_OZONE_PLATFORM_HINT - -# XDG Spec -env = XDG_CURRENT_DESKTOP,$env.XDG_CURRENT_DESKTOP -env = XDG_SESSION_TYPE,$env.XDG_SESSION_TYPE -env = XDG_SESSION_DESKTOP,$env.XDG_SESSION_DESKTOP - - -# HyDE Variables -$hyde.PATH=$HOME/.local/bin:$scrPath:$env.PATH:$PATH -env = PATH,$hyde.PATH - -# XDG-ENV -env = XDG_CONFIG_HOME,$env.XDG_CONFIG_HOME -env = XDG_CACHE_HOME,$env.XDG_CACHE_HOME -env = XDG_DATA_HOME,$env.XDG_DATA_HOME - -# HyDE-ENV -env = HYDE_RUNTIME_DIR,$env.XDG_RUNTIME_DIR/hyde -env = HYDE_CONFIG_HOME,$env.XDG_CONFIG_HOME/hyde -env = HYDE_CACHE_HOME,$env.XDG_CACHE_HOME/hyde -env = HYDE_DATA_HOME,$env.XDG_DATA_HOME/hyde -env = HYDE_STATE_HOME,$env.XDG_STATE_HOME/hyde - -# CURSOR ENV -# env = HYPRCURSOR_THEME,$CURSOR_THEME -# env = HYPRCURSOR_SIZE,$CURSOR_SIZE -# env = XCURSOR_THEME,$CURSOR_THEME -# env = XCURSOR_SIZE,$CURSOR_SIZE - -# Execute on reload - -# HyDE Preparation -$exec.mkdir = mkdir -p $env.XDG_RUNTIME_DIR/hyde $env.XDG_CACHE_HOME/hyde/wallbash $env.XDG_CONFIG_HOME/hyde $env.XDG_DATA_HOME/hyde $(dirname $env.XDG_DATA_HOME)/state/hyde # Create HyDE directories -$set.env = printf "\n_SHELL='$SHELL'\nGDK_BACKEND='$env.GDK_BACKEND'\nQT_QPA_PLATFORM='$env.QT_QPA_PLATFORM'\nSDL_VIDEODRIVER='$env.SDL_VIDEODRIVER'\nCLUTTER_BACKEND='$env.CLUTTER_BACKEND'\nXDG_CURRENT_DESKTOP='$env.XDG_CURRENT_DESKTOP'\nXDG_SESSION_TYPE='$env.XDG_SESSION_TYPE'\nXDG_SESSION_DESKTOP='$env.XDG_SESSION_DESKTOP'\nQT_AUTO_SCREEN_SCALE_FACTOR='$env.QT_AUTO_SCREEN_SCALE_FACTOR'\nQT_WAYLAND_DISABLE_WINDOWDECORATION='$env.QT_WAYLAND_DISABLE_WINDOWDECORATION'\nQT_QPA_PLATFORMTHEME='$env.QT_QPA_PLATFORMTHEME'\nHYDE_PATH='$hyde.PATH'\nMOZ_ENABLE_WAYLAND='$env.MOZ_ENABLE_WAYLAND'\nGDK_SCALE='$env.GDK_SCALE'\nELECTRON_OZONE_PLATFORM_HINT='$env.ELECTRON_OZONE_PLATFORM_HINT'\nXDG_RUNTIME_DIR='$env.XDG_RUNTIME_DIR'\nXDG_CONFIG_HOME='$env.XDG_CONFIG_HOME'\nXDG_CACHE_HOME='$env.XDG_CACHE_HOME'\nXDG_DATA_HOME='$env.XDG_DATA_HOME'\n_GTK_THEME='$GTK_THEME'\n_ICON_THEME='$ICON_THEME'\n_COLOR_SCHEME='$COLOR_SCHEME'\n_CURSOR_SIZE='$CURSOR_SIZE'\n_CURSOR_THEME='$CURSOR_THEME'\nFONT='$FONT'\nFONT_SIZE='$FONT_SIZE'\nDOCUMENT_FONT='$DOCUMENT_FONT'\nDOCUMENT_FONT_SIZE='$DOCUMENT_FONT_SIZE'\nMONOSPACE_FONT='$MONOSPACE_FONT'\nMONOSPACE_FONT_SIZE='$MONOSPACE_FONT_SIZE'\nFONT_ANTIALIASING='$FONT_ANTIALIASING'\nFONT_HINTING='$FONT_HINTING'\nHYDE_RUNTIME_DIR='$env.XDG_RUNTIME_DIR/hyde'\nHYDE_CONFIG_HOME='$env.XDG_CONFIG_HOME/hyde'\nHYDE_CACHE_HOME='$env.XDG_CACHE_HOME/hyde'\nHYDE_DATA_HOME='$env.XDG_DATA_HOME/hyde'\nHYDE_STATE_HOME='$(dirname $env.XDG_DATA_HOME)/state/hyde'\nexport TERMINAL='$(which $TERMINAL)'\nexport LOCKSCREEN='$LOCKSCREEN'" > "$env.XDG_RUNTIME_DIR/hyde/environment" - -# Create a dconf file that will be loaded later one time, better than multiple dconf commands and better than gsettings -$dconFile=$env.XDG_CACHE_HOME/hyde/dconf -$dConf = \n[org/gnome/desktop/interface]\n -$dConf = $dConf icon-theme='$ICON_THEME'\n -$dConf = $dConf gtk-theme='$GTK_THEME'\n -$dConf = $dConf color-scheme='$COLOR_SCHEME'\n -$dConf = $dConf cursor-theme='$CURSOR_THEME'\n -$dConf = $dConf cursor-size=$CURSOR_SIZE\n -$dConf = $dConf font-name='$FONT $FONT_SIZE'\n -$dConf = $dConf document-font-name='$DOCUMENT_FONT $DOCUMENT_FONT_SIZE'\n -$dConf = $dConf monospace-font-name='$MONOSPACE_FONT $MONOSPACE_FONT_SIZE'\n -$dConf = $dConf font-antialiasing='$FONT_ANTIALIASING'\n -$dConf = $dConf font-hinting='$FONT_HINTING'\n -$dConf = $dConf [org/gnome/desktop/default-applications/terminal]\nexec='$(which $TERMINAL)' -$dconf.preserve = dconf dump / > $dconFile -$dconf.populate = printf "$dConf" >> $dconFile -$dconf.reset = dconf reset -f / < $dconFile -$dconf.load = dconf load -f / < $dconFile -$set.hyprCursor = hyprctl setcursor $CURSOR_THEME $CURSOR_SIZE -$exec.dConf = ( $dconf.preserve && $dconf.populate && $dconf.reset && $dconf.load && $set.hyprCursor ) - -$exec.keybinds_hint = $scrPath/keybinds.hint.py --format rofi > $env.XDG_RUNTIME_DIR/hyde/keybinds_hint.rofi - -# Execute -exec = $exec.mkdir & $set.env & $exec.dConf & $exec.animation & $exec.keybinds_hint - -# // █░░ ▄▀█ █░█ █▄░█ █▀▀ █░█ -# // █▄▄ █▀█ █▄█ █░▀█ █▄▄ █▀█ - - -exec-once = $start.XDG_PORTAL_RESET # reset XDPH for screenshare -exec-once = $start.DBUS_SHARE_PICKER # for XDPHgl -exec-once = $start.SYSTEMD_SHARE_PICKER # for XDPH -exec-once = $start.AUTH_DIALOGUE # authentication dialogue for GUI apps -exec-once = $start.BAR # launch the system bar -exec-once = $start.APPTRAY_BLUETOOTH # systray app for Bluetooth -exec-once = $start.REMOVABLE_MEDIA # front-end that allows to manage removable media -exec-once = $start.NETWORK_MANAGER # systray app for Network/Wifi -exec-once = $start.NOTIFICATIONS # start notification demon -exec-once = $start.TEXT_CLIPBOARD # clipboard store text data -exec-once = $start.IMAGE_CLIPBOARD # clipboard store image data -exec-once = $start.WALLPAPER # start wallpaper daemon -exec-once = $start.BATTERY_NOTIFY # battery notification -exec-once = $start.IDLE_DAEMON # idle daemon - -# hyde/config.toml is parsed and exported to the environment -exec-once = $scrPath/parse.config.py --daemon --input $env.XDG_CONFIG_HOME/hyde/config.toml --env $env.XDG_STATE_HOME/hyde/config --hypr $env.XDG_STATE_HOME/hyde/hyprland.conf - - - -#? Rules can be added here as most of the configuration are dynamic - -# // █░█░█ █ █▄░█ █▀▄ █▀█ █░█░█   █▀█ █░█ █░░ █▀▀ █▀ -# // ▀▄▀▄▀ █ █░▀█ █▄▀ █▄█ ▀▄▀▄▀   █▀▄ █▄█ █▄▄ ██▄ ▄█ - -# See https://wiki.hyprland.org/Configuring/Window-Rules/ - -# Ony add the Core applications here -windowrule { - name = windowrule-1 - float = on - match:class = ^(com.gabm.satty)$ -} - -windowrule { - name = windowrule-2 - float = on - match:class = ^(org.kde.dolphin)$ - match:title = ^(Progress Dialog — Dolphin)$ -} - -windowrule { - name = windowrule-3 - float = on - match:class = ^(org.kde.dolphin)$ - match:title = ^(Copying — Dolphin)$ -} - -windowrule { - name = windowrule-4 - float = on - match:title = ^(About Mozilla Firefox)$ -} - -windowrule { - name = windowrule-5 - float = on - match:class = ^(firefox)$ - match:title = ^(Picture-in-Picture)$ -} - -windowrule { - name = windowrule-6 - float = on - match:class = ^(firefox)$ - match:title = ^(Library)$ -} - -windowrule { - name = windowrule-7 - float = on - match:class = ^(.*)$ - match:initial_title = ^(.*)(top)(.*)$ -} - -windowrule { - name = windowrule-8 - float = on - match:class = ^(.*)$ - match:initial_title = ^(.*)(btop)(.*)$ -} - -windowrule { - name = windowrule-9 - float = on - match:class = ^(.*)$ - match:initial_title = ^(.*)(htop)(.*)$ -} - -windowrule { - name = windowrule-10 - float = on - match:class = ^(vlc)$ -} - -windowrule { - name = windowrule-11 - float = on - match:class = ^(kvantummanager)$ -} - -windowrule { - name = windowrule-12 - float = on - match:class = ^(qt5ct)$ -} - -windowrule { - name = windowrule-13 - float = on - match:class = ^(qt6ct)$ -} - -windowrule { - name = windowrule-14 - float = on - match:class = ^(nwg-look)$ -} - -windowrule { - name = windowrule-15 - float = on - match:class = ^(nwg-displays)$ -} - -windowrule { - name = windowrule-16 - float = on - match:class = ^(org.kde.ark)$ -} - -windowrule { - name = windowrule-17 - float = on - match:class = ^(org.pulseaudio.pavucontrol)$ -} - -windowrule { - name = windowrule-18 - float = on - match:class = ^(blueman-manager)$ -} - -windowrule { - name = windowrule-19 - float = on - match:class = ^(nm-applet)$ -} - -windowrule { - name = windowrule-20 - float = on - match:class = ^(nm-connection-editor)$ -} - -windowrule { - name = windowrule-21 - float = on - match:class = ^(org.kde.polkit-kde-authentication-agent-1)$ -} - -windowrule { - name = windowrule-22 - float = on - match:class = ^([Xx]dg-desktop-portal-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 diff --git a/gui/.local/share/icons/dots-icons/index.theme b/gui/.local/share/icons/dots-icons/index.theme index 7a3d2ee..580ea44 100644 --- a/gui/.local/share/icons/dots-icons/index.theme +++ b/gui/.local/share/icons/dots-icons/index.theme @@ -1,6 +1,6 @@ [Icon Theme] Name=dots-icons -Comment=Thin overlay over Tela-circle-dracula: monochrome tray/status icons at every size. Tela only ships its flat panel icons at fixed 16/22/24 - on a fractionally scaled monitor waybar requests size*2 (e.g. 44), which falls through to scalable/devices, whose icons are colored circles (the blue nm-applet blob). This theme is searched first and serves the panel SVGs as scalable, so status icons stay flat; everything else inherits Tela unchanged. Pinned via $ICON_THEME in gui/.config/hypr/hyde.conf. +Comment=Thin overlay over Tela-circle-dracula: monochrome tray/status icons at every size. Tela only ships its flat panel icons at fixed 16/22/24 - on a fractionally scaled monitor waybar requests size*2 (e.g. 44), which falls through to scalable/devices, whose icons are colored circles (the blue nm-applet blob). This theme is searched first and serves the panel SVGs as scalable, so status icons stay flat; everything else inherits Tela unchanged. Pinned via hc.ui.icon_theme in gui/.config/hypr/hyprland.lua. The status icons are symlinks into the host's own Tela-circle-dracula, made by bin/dots-icons-link.sh (install.sh step 3.6) - Tela-circle publishes no licence, so the repo ships none of its files. Inherits=Tela-circle-dracula Directories=scalable/status diff --git a/gui/.local/share/icons/dots-icons/scalable/status/blueman-active.svg b/gui/.local/share/icons/dots-icons/scalable/status/blueman-active.svg deleted file mode 100644 index 598b6b8..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/blueman-active.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/blueman-disabled.svg b/gui/.local/share/icons/dots-icons/scalable/status/blueman-disabled.svg deleted file mode 100644 index 58c037f..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/blueman-disabled.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray-active.svg b/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray-active.svg deleted file mode 100644 index 598b6b8..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray-active.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray-disabled.svg b/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray-disabled.svg deleted file mode 100644 index 58c037f..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray-disabled.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray.svg b/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray.svg deleted file mode 100644 index 598b6b8..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/blueman-tray.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-adhoc.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-adhoc.svg deleted file mode 100644 index 7da431c..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-adhoc.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-autoip.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-autoip.svg deleted file mode 100644 index 245d61b..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-autoip.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-secure.svg deleted file mode 100644 index 7bfbb34..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-secure.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-secureoffline.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-secureoffline.svg deleted file mode 100644 index 64dc183..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired-secureoffline.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired.svg deleted file mode 100644 index 245d61b..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wired.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wireless.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wireless.svg deleted file mode 100644 index f89a3ff..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wireless.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wwan.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wwan.svg deleted file mode 100644 index 5909ac6..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-device-wwan.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-mb-roam.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-mb-roam.svg deleted file mode 100644 index 7a8d631..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-mb-roam.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-00.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-00.svg deleted file mode 100644 index 29f61b4..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-00.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-100.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-100.svg deleted file mode 100644 index f89a3ff..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-100.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-25.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-25.svg deleted file mode 100644 index cab4710..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-25.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-50.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-50.svg deleted file mode 100644 index da74dbb..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-50.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-75.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-75.svg deleted file mode 100644 index 96cae57..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-nm-signal-75.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-no-connection.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-no-connection.svg deleted file mode 100644 index 4eee086..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-no-connection.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-secure-lock.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-secure-lock.svg deleted file mode 100644 index da25e2b..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-secure-lock.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-0-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-0-secure.svg deleted file mode 100644 index aee38cb..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-0-secure.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-0.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-0.svg deleted file mode 100644 index 29f61b4..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-0.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-00-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-00-secure.svg deleted file mode 100644 index aee38cb..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-00-secure.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-00.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-00.svg deleted file mode 100644 index 29f61b4..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-00.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-100-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-100-secure.svg deleted file mode 100644 index 737fa8b..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-100-secure.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-100.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-100.svg deleted file mode 100644 index f89a3ff..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-100.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-25-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-25-secure.svg deleted file mode 100644 index ea84815..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-25-secure.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-25.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-25.svg deleted file mode 100644 index cab4710..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-25.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-50-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-50-secure.svg deleted file mode 100644 index ea84815..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-50-secure.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-50.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-50.svg deleted file mode 100644 index cab4710..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-50.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-75-secure.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-75-secure.svg deleted file mode 100644 index 0256b36..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-75-secure.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-75.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-75.svg deleted file mode 100644 index 96cae57..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-signal-75.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-3g.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-3g.svg deleted file mode 100644 index 59ba694..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-3g.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-4g.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-4g.svg deleted file mode 100644 index 78f7c71..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-4g.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-cdma-1x.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-cdma-1x.svg deleted file mode 100644 index e2b9c2f..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-cdma-1x.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-edge.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-edge.svg deleted file mode 100644 index 4e657af..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-edge.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-evdo.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-evdo.svg deleted file mode 100644 index cd4c906..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-evdo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-gprs.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-gprs.svg deleted file mode 100644 index 8a46c67..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-gprs.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-hspa.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-hspa.svg deleted file mode 100644 index 062d0da..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-hspa.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-lte.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-lte.svg deleted file mode 100644 index 78f7c71..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-lte.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-umts.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-umts.svg deleted file mode 100644 index e25ba98..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-tech-umts.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-active-lock.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-active-lock.svg deleted file mode 100644 index da25e2b..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-active-lock.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-lock.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-lock.svg deleted file mode 100644 index da25e2b..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-lock.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-standalone-lock.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-standalone-lock.svg deleted file mode 100644 index 4af2382..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-vpn-standalone-lock.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/gui/.local/share/icons/dots-icons/scalable/status/nm-wwan-tower.svg b/gui/.local/share/icons/dots-icons/scalable/status/nm-wwan-tower.svg deleted file mode 100644 index 7a8d631..0000000 --- a/gui/.local/share/icons/dots-icons/scalable/status/nm-wwan-tower.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/install.sh b/install.sh index 71bbf82..22e82e4 100755 --- a/install.sh +++ b/install.sh @@ -93,6 +93,29 @@ HOST="${ARGS[0]:-${RECORDED:-$(hostname -s)}}" echo "error: no host resolved — pass one (./install.sh ) or write it to ~/.config/dots-host" >&2 exit 1 } +# a host must be a plain top-level package name. common/gui are base LAYERS: as a host, the layer +# is released, restowed and then re-stowed with --override, and the name is recorded so every later +# run (and dotsync) repeats it. bin/ is tooling, never a package. Anything with a leading dot or a +# slash (`.`, `..`, `.git`, `a/b`) would let stow treat the repo root or a non-package as one — +# the same hazard as the empty HOST above. +host_ok() { # host_ok — is something install.sh may stow as a host overlay? + case "$1" in + common | gui | bin | .* | */* | -* | "") return 1 ;; + esac +} +if ! host_ok "$HOST"; then + case "$HOST" in + common | gui) + echo "error: '$HOST' is a base layer, not a host package — a terminal-only machine runs" >&2 + echo " 'touch ~/.config/dots-headless; ./install.sh' with no host argument" >&2 + ;; + *) echo "error: '$HOST' is not a host package name (top-level dir like wm, lw, fl)" >&2 ;; + esac + [ "$HOST" = "$RECORDED" ] && echo " (it is recorded in ~/.config/dots-host — rm that file first)" >&2 + exit 2 +fi +# a bad recorded name with a valid explicit host: don't release it in step 1, just replace it +host_ok "$RECORDED" || RECORDED="" DOTS="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)" command -v stow >/dev/null 2>&1 || { @@ -112,6 +135,35 @@ fi gui=true [ -e "$HEADLESS_MARKER" ] && gui=false +# Every stow call goes through stow_cmd, so --no-folding is never forgotten on one of them. It +# matters on -D as much as on -S: an unstow REFOLDS each dir it walks whose remaining links all +# come from one package — even a package that was never deployed. A bare `-D gui` on headless +# hosts collapsed ~/.config/fish/functions and fish/conf.d (gui ships files in both) into single +# links to common/, so anything written there (funcsave, fisher) landed in the repo. +stow_cmd() { stow --no-folding -d "$DOTS" -t "$HOME" "$@"; } + +FAIL=0 +# unstow one package, then prune the dirs it would have created under ~/.config if they are now +# empty. Only the package's own .config dirs are candidates (deepest first, rmdir = empty only), +# so an empty dir the user or an app made elsewhere is never touched. A failed unstow is reported +# with stow's own message and fails the run — it never passes silently as "done". +unstow_pkg() { # unstow_pkg ; returns stow's status + local p="$1" out rel + [ -d "$DOTS/$p" ] || return 0 + if ! out=$(stow_cmd $DRY -D "$p" 2>&1); then + echo " FAILED to unstow $p:" >&2 + printf '%s\n' "$out" | sed 's/^/ /' >&2 + FAIL=1 + return 1 + fi + if [ -z "$DRY" ] && [ -d "$DOTS/$p/.config" ]; then + while IFS= read -r rel; do + [ -d "$HOME/$rel" ] && [ ! -L "$HOME/$rel" ] && rmdir "$HOME/$rel" 2>/dev/null + done < <(cd "$DOTS/$p" && find .config -mindepth 1 -depth -type d) + fi + return 0 +} + echo "host=$HOST dots=$DOTS gui=$gui${DRY:+ (dry-run)}" # --- revert mode: unstow this host's packages (reverse of deploy), leave repo + real files intact --- @@ -120,18 +172,12 @@ if [ "$UNSTOW" -eq 1 ]; then [ -n "$DRY" ] && verb="would unstow" for p in "$HOST" gui common; do [ -d "$DOTS/$p" ] || continue - if stow $DRY -d "$DOTS" -t "$HOME" -D "$p" 2>/dev/null; then - echo " $verb $p" - fi - # prune the now-empty dirs this package created under ~/.config (empty only — never real files) - if [ -z "$DRY" ] && [ -d "$DOTS/$p/.config" ]; then - for d in "$DOTS/$p"/.config/*/; do - t="$HOME/.config/$(basename "$d")" - [ -d "$t" ] && find "$t" -type d -empty -delete 2>/dev/null || true - done - fi + unstow_pkg "$p" && echo " $verb $p" done - if [ -n "$DRY" ]; then + if [ "$FAIL" -ne 0 ]; then + echo "FAILED: some packages could not be unstowed (see above)." >&2 + exit 1 + elif [ -n "$DRY" ]; then echo "(dry-run) nothing changed." else echo "reverted — removed this host's stow symlinks. ~/.dots and any real files are untouched." @@ -183,13 +229,18 @@ fi # --- keep a live Hyprland session quiet while symlinks are removed/recreated --- # Step 1 releases the host overlay before restowing, and Hyprland's config watcher reloads -# the moment a sourced file vanishes (monitors.conf/host.conf, hyprland.conf:58/60 — the two -# that come from the overlay): that parse aborts and the session sits on a partial config -# until a manual Super+Shift+R. So pause the watcher and reload once at the end, when every +# the moment a watched file flickers (under the Lua chain that's ~/.config/hypr/*.lua — +# hyprland.lua/host.lua both come from packages this run re-links): every re-parse makes the +# session visibly jump/resize. So pause the watcher and reload once at the end, when every # link is back. The final reload also re-reads misc:disable_autoreload from the config, -# undoing the runtime keyword — no state is left behind. +# undoing the runtime override — no state is left behind. +# The pause command differs by config mode (same branch as HyDE's color.set.sh): in Lua mode +# `hyprctl keyword` is gone — runtime config goes through `hyprctl eval` + hl.config. if [ -z "$DRY" ] && [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] && command -v hyprctl >/dev/null 2>&1; then - hyprctl keyword misc:disable_autoreload true >/dev/null 2>&1 + case "${HYPRLAND_CONFIG:-}" in + *.lua) hyprctl eval 'hl.config({misc = {disable_autoreload = true}})' >/dev/null 2>&1 ;; + *) hyprctl keyword misc:disable_autoreload true >/dev/null 2>&1 ;; + esac trap 'hyprctl reload >/dev/null 2>&1' EXIT fi @@ -197,20 +248,17 @@ fi # The PREVIOUSLY recorded host is released too, so switching a machine to another overlay # works: its leftover links are "stowed to a different package" as far as the step-2 restows # are concerned, which would abort those whole packages. Releasing an already-released -# package is a harmless no-op, so the repeat when nothing changed costs nothing. +# package is a harmless no-op (exit 0), so a failure here is real and is reported. if [ -z "$DRY" ]; then - for p in "$HOST" "$RECORDED"; do - [ -n "$p" ] && [ -d "$DOTS/$p" ] && stow -d "$DOTS" -t "$HOME" -D "$p" 2>/dev/null - done - true # a release that found nothing to do must not fail the run + unstow_pkg "$HOST" + [ -n "$RECORDED" ] && [ "$RECORDED" != "$HOST" ] && unstow_pkg "$RECORDED" fi # stow one package; track failures instead of silently continuing and reporting success -FAIL=0 stow_pkg() { # stow_pkg