Initial import: chezmoi source for CachyOS + Umbriel + Noctalia

Imported from prdlk/dotfiles master (efdd827). Drops the niri, Hyprland/Omarchy,
macOS, and Blume docs-site layers; docs/ is plain Markdown.
This commit is contained in:
Prad Nukala committed 2026-11-02 09:14:11 -05:00
commit ee355bcfd5
152 files changed
+11564

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# Shell
fish, bash, and zsh with shared aliases, the starship prompt, atuin history, and the ~/.local/bin scripts.
fish is the main interactive shell. bash and zsh are set up to feel the same: they share aliases, the prompt, atuin history, and ports of the cd-style helper functions. General-purpose tools are POSIX or bash scripts in `~/.local/bin`, so each has a single implementation that all three shells call.
| Page | Covers |
| --- | --- |
| [fish](fish.md) | `~/.config/fish`: `config.fish`, every `conf.d/` file (PATH, env, aliases, ssh-agent, tool init, secrets by name), vi-mode key bindings, the cyberdream theme, completions, the nvm.fish plugin, and every function in `functions/` |
| [bash and zsh](bash-zsh.md) | `~/.bashrc`, `~/.zshrc`, zsh and bash key bindings, `~/.config/shell/aliases.sh`, per-shell function ports |
| [Aliases](aliases.md) | Every alias from `.chezmoidata/aliases.yaml` plus the fish-only and shell-specific ones |
| [Scripts](scripts.md) | Every general-purpose executable in `~/.local/bin` (`pj`, `ghn`, `gop`, `mtime-sort`, `atuin-pick`, …) |
| [Starship](starship.md) | `~/.config/starship.toml`: format, modules, symbols, cyberdream palette |
| [atuin](atuin.md) | `~/.config/atuin`: history search settings, AI inline integration, cyberdream theme |
## Shared across shells
| Concern | fish | bash | zsh |
| --- | --- | --- | --- |
| Aliases | `conf.d/20-aliases.fish` (hand-maintained) | `~/.config/shell/aliases.sh` (rendered from YAML) | same as bash |
| Prompt | starship + transient prompt | starship | starship + transient prompt |
| History | atuin (hishtory also sourced if installed) | atuin | atuin, plus suvadu if installed |
| Key bindings | vi mode plus custom binds | atuin defaults only | emacs keymap plus ports of the fish binds |
| cd-style helpers | `functions/*.fish` | `~/.config/bash/functions/*.bash` | `~/.config/zsh/functions/*.zsh` |
| Directory jumping | `cd` wrapper with zoxide fallback, `zoxide init` | `cd` wrapper | `cd` wrapper |
## Theme
fish syntax colors, `FZF_DEFAULT_OPTS`, the starship palette, and the atuin theme all use **cyberdream** ([cyberdream.nvim](https://github.com/scottmckendry/cyberdream.nvim)). In fish, `40-tools.fish` also replays quickshell's wallpaper-generated terminal color sequences if that file exists.
## Role gating
Everything in this section applies on every role (`desktop`, `laptop`, `server`). The desktop helpers in `~/.local/bin` belong to the [desktop layer](../desktop/README.md) and apply on `desktop`/`laptop` only.
## Packages
`.chezmoidata/packages.yaml` supplies the shell tooling on Arch (pacman): `fish`, `starship`, `atuin`, `zoxide`, `fzf`, `fd`, `ripgrep`, `eza`, `bat`, `jq`, `gum`, `glow`, `yazi`, `lazygit`, and `zsh-syntax-highlighting`. The packages script runs `pacman -S --needed`. See [chezmoi scripts](../chezmoi/scripts.md).
+123
View File
@@ -0,0 +1,123 @@
# Aliases
Every alias and alias-like helper across fish, bash, and zsh, and where each one is defined.
## Single source
`.chezmoidata/aliases.yaml` holds the shell-agnostic aliases as a list of sections, each with `name`/`cmd` items. chezmoi loads the file as template data ([chezmoi data](../chezmoi/data.md)), and `dot_config/shell/aliases.sh.tmpl` ranges over it to produce `~/.config/shell/aliases.sh`, which `~/.bashrc` and `~/.zshrc` source:
**.chezmoidata/aliases.yaml**
```yaml
aliases:
- section: "Editors / file managers"
items:
- { name: "e", cmd: "nvim" }
# …
```
**~/.config/shell/aliases.sh**
```text
# Editors / file managers
alias e='nvim'
```
To add an alias for bash and zsh, add it to the YAML and run `chezmoi apply`.
> [!NOTE]
> The YAML and template comments say fish renders the same data through `fish/conf.d/20-aliases.fish.tmpl`. That template no longer exists. `dot_config/fish/conf.d/20-aliases.fish` is a plain, hand-maintained fish file, so a YAML change does not reach fish. The fish column below shows where the two sets differ.
## Shared aliases
A ✓ means the alias exists in that shell with the command shown. Where fish uses a different command, the fish command is shown instead.
### Editors / file managers
| Alias | bash / zsh (YAML) | fish (`20-aliases.fish`) |
| --- | --- | --- |
| `e` | `nvim` | ✓ |
| `e.d` | `cd ~/.config/dotstate/storage/Rogz/.config && yazi` | not defined |
| `e.f` | `cd ~/.config/fish && yazi` | `cd ~/.local/share/chezmoi/dot_config/fish && yazi` |
| `e.h` | not defined | `cd ~/.local/share/chezmoi/dot_config/hypr && yazi` (this repo has no `hypr` directory) |
| `e.k` | `yazi ~/.config/kitty` | ✓ |
| `e.n` | not defined | `yazi ~/.local/share/chezmoi/dot_config/niri` (this repo has no `niri` directory) |
| `e.v` | `yazi ~/.config/nvim/lua` | ✓ |
| `e.y` | `yazi ~/.config/yazi` | ✓ |
| `e.t` | `nvim ~/.config/tmux/tmux.conf` | ✓ |
| `hx` | `helix` | ✓ (listed under Tools in fish) |
The fish `e.f`, `e.h`, `e.k`, and `e.n` aliases open the chezmoi source tree directly. In symlink mode, `~/.config/<tool>` files are symlinks into that tree, so both versions edit the same files. yazi and tmux are external checkouts in `~/.config`, so `e.y` and `e.t` open them there.
### Quick directory jumps
| Alias | bash / zsh (YAML) | fish |
| --- | --- | --- |
| `cdc` | `cd ~/.config && yazi` | ✓ |
| `cdd` | `cd ~/Developer && yazi` | not defined |
| `cdk` | `cd ~/Documents/Knowledge && obsidian` | ✓ |
| `cdl` | `cd ~/.local && yazi` | ✓ |
| `cds` | `cd ~/.local/share/services && ls` | ✓ |
| `cdw` | `cd ~/Downloads && yazi` | ✓ |
| `,,` | not defined | `cd ~/.local/share/chezmoi && yazi` |
### Tools
| Alias | Command | fish |
| --- | --- | --- |
| `dd` | `lazydocker` | ✓ |
| `gg` | `lazygit` | ✓ |
| `ff` | `exec fish` | ✓ |
| `npx` | `bunx` | ✓ |
| `sm` | `sudo micro` | ✓ |
| `sd` | `ssh prad@<lan-host>` (a machine on the local network) | ✓ |
| `oc` | `opencode` | ✓ |
| `dss` | `dotstate sync` | ✓ |
| `fabric` | `fabric-ai` | ✓ |
| `bk` | `bead-kanban` | ✓ |
| `sua` | `shelly upgrade-all -n` | ✓ |
| `lc` | `leetcode` | ✓ |
| `tk` | `task` | ✓ |
| `..` | not defined | `cd ..` |
`dd` shadows coreutils `dd` in interactive shells. Use `command dd` for the real one.
## fish-only aliases
### Bun
| Alias | Command |
| --- | --- |
| `br` | `bun run` |
| `br.d` | `bun run dev` |
| `br.b` | `bun run build` |
| `br.t` | `bun run test` |
| `bi` | `bun install` |
| `bg` | `bun install -g` |
| `bx` | `bunx` |
`bg` replaces fish's job-control builtin. Use `builtin bg` to resume a background job.
## Shell-specific extras
All three shells define these outside the YAML: in `20-aliases.fish` for fish, and in the tail of `aliases.sh.tmpl` for bash and zsh.
| Name | Kind | Definition |
| --- | --- | --- |
| `cpy` | alias | `wl-copy` if available |
| `pst` | alias | `wl-paste` if available, else `pbpaste` |
| `q` | function | `exit`. Runs `ya emit quit` first when `$YAZI_ID` is set, so the parent yazi also quits |
| `celar`, `claer` | alias | `clear` (interactive only) |
| `ls` | alias | `eza --icons` when eza is installed (interactive only) |
## Abbreviations
No fish `abbr` definitions are committed. Abbreviations live in `fish_variables`, which chezmoi ignores.
## Related helpers
The following are functions or scripts, not aliases, and are documented elsewhere:
- `c`, `cd`, `y`/`yy`, `clip-last`, `ghg`, `ghn`, `gho`: per-shell functions ([fish](fish.md#functions), [bash and zsh](bash-zsh.md#per-shell-functions)).
- `pnpmf`, `pnpmfd`, `pnpmfr`, `pnpmfw`, `pnpmfx`: fish workspace helpers ([fish](fish.md#pnpm-filterfish)).
- `ai`, `pj`, `gop`, `fab`, `gdc`, and the other `~/.local/bin` tools: [Scripts](scripts.md).
+128
View File
@@ -0,0 +1,128 @@
# atuin
Shell history search with a daemon, directory-scoped up-arrow, tmux popups, a CherryBlossom theme, and the atuin AI inline command generator.
[atuin](https://atuin.sh) replaces shell history search in fish, bash, and zsh. It also provides `atuin ai inline`, a natural-language command generator that the fish config binds to keys.
| Source (repo) | Target | Notes |
| --- | --- | --- |
| `dot_config/atuin/private_config.toml` | `~/.config/atuin/config.toml` | `private_`: written as a regular `0600` file, not a symlink |
| `dot_config/atuin/themes/cherry-blossom.toml`, `dot_config/atuin/themes/cyberdream.toml` | `~/.config/atuin/themes/*.toml` | symlinks |
chezmoi runs in symlink mode, but `private_` files are written as real files, so edits to `~/.config/atuin/config.toml` do not reach the repo on their own. Copy them back with `chezmoi re-add` (or `czs`, see [fish](fish.md#functions)). None of the files are templates, and they apply on every role.
## Installation
`atuin` is listed in `.chezmoidata/packages.yaml` for pacman, and the shells use that package's binary (`/usr/bin/atuin` on Arch). Each shell runs `atuin init` only if `atuin` is on `PATH`.
> [!NOTE]
> **No standalone install**
>
> An earlier setup also kept atuin's standalone installer layout in `~/.atuin/bin` (with a cargo-dist `atuin-receipt.json` in the repo) and prepended it to `PATH` from `conf.d/atuin.env.fish` and `. "$HOME/.atuin/bin/env"` in `.bashrc`/`.zshrc`. That copy fell behind the package: the daemon, from the newer package, migrated the history database, and every command the shells ran through the old binary failed with `migration 20260818000000 was previously applied but is missing in the resolved migrations`. The standalone install, its receipt, and the `PATH` lines are gone. Do not reinstall atuin with its curl installer on this setup.
## Shell integration
| Shell | Where | What |
| --- | --- | --- |
| fish | `config.fish`, interactive only | `atuin init fish \| source`, and a copy of the `?` binding (atuin 18.21 removed the `atuin ai init` command that used to emit it) |
| fish | `functions/fish_user_key_bindings.fish` | <kbd>ctrl-w</kbd> runs `atuin ai inline` |
| bash | end of `~/.bashrc` | `eval "$(atuin init bash)"` |
| zsh | end of `~/.zshrc` | `eval "$(atuin init zsh)"` |
In fish, `40-tools.fish` sources hishtory before atuin, but atuin's init runs later in `config.fish` and takes <kbd>ctrl-r</kbd>. zsh also loads `suv init zsh` (suvadu) if installed.
### Key bindings
| Key | Shells | Action |
| --- | --- | --- |
| <kbd>ctrl-r</kbd> | all | Interactive search (`_atuin_search`) |
| <kbd>up</kbd> | all | Search opened from the up arrow. `filter_mode_shell_up_key_binding = "directory"` limits it to history from the current directory |
| <kbd>?</kbd> | fish, vi normal mode | On an empty buffer, runs `atuin ai inline --hook` |
| <kbd>ctrl-w</kbd> | fish, insert and normal mode | `atuin ai inline`, then repaint |
| <kbd>ctrl+shift+p</kbd> | kitty | [`atuin-pick`](scripts.md#atuin-pick): directory-filtered search in a split, running the pick in a fresh fish |
Under tmux, atuin's `[tmux]` settings open the search in a tmux popup (80% wide, 60% tall) instead of inline.
## AI inline
Commit `a82921c` ("add Atuin AI inline integration and keybinding") added the <kbd>ctrl-w</kbd> bind to `fish_user_key_bindings.fish`, `atuin ai init fish | source` to `config.fish`, and a copy of the `_atuin_ai_question_mark` function that command printed, with `bind "?" _atuin_ai_question_mark`. atuin 18.21 removed `atuin ai init`, so `config.fish` keeps only the copied function and binding; `atuin ai inline --hook` still works.
The config also sets `[ai] enabled = true`.
`_atuin_ai_question_mark` works like this:
- **Buffer empty (or just `?`):** clears the line, prints OSC `133;C` to `/dev/tty` to close the terminal's semantic prompt zone (so shell-integrated terminals do not erase the TUI during a prompt reflow), then runs `atuin ai inline --hook` with stdout and stderr swapped. The TUI draws on the terminal, and the result comes back tagged:
| Output prefix | Effect |
| --- | --- |
| `__atuin_ai_print__:` | Print the text, repaint |
| `__atuin_ai_cancel__` | Repaint only |
| `__atuin_ai_execute__:` | Put the command on the line and execute it |
| `__atuin_ai_insert__:` | Put the command on the line for editing |
| anything else, non-empty | Put it on the line for editing |
- **Buffer not empty:** inserts a literal `?` under emacs bindings. Under vi or hybrid bindings, which this config uses, it does nothing.
`bind "?"` in `config.fish` has no `-M` flag, so it applies to fish's `default` mode, which is vi **normal** mode here. In insert mode <kbd>?</kbd> types a question mark. To ask for a command, press <kbd>Esc</kbd> and then <kbd>?</kbd> on an empty line, or press <kbd>ctrl-w</kbd> from either mode. The <kbd>ctrl-w</kbd> bind calls `atuin ai inline` without `--hook` and replaces fish's default <kbd>ctrl-w</kbd> word deletion.
bash and zsh have no AI binding.
## Configuration
`config.toml` is atuin's generated sample with a few keys set. The rest are commented-out defaults. The settings that take effect:
| Key | Value | Effect |
| --- | --- | --- |
| `filter_mode_shell_up_key_binding` | `"directory"` | Up-arrow search shows only the current directory's history |
| `dotfiles.enabled` | `true` | Enables atuin's dotfiles feature (aliases and vars managed by atuin) |
| `daemon.enabled` | `true` | Uses the background daemon |
| `daemon.autostart` | `true` | atuin starts and manages the daemon itself |
| `pty_proxy.enabled` | `false` | `atuin init` does not re-exec the shell inside `atuin pty-proxy` |
| `theme.name` | `"cherry-blossom"` | Loads `themes/cherry-blossom.toml` |
| `tmux.enabled` | `true` | Search opens in a tmux popup when inside tmux (tmux 3.2+) |
| `tmux.width` / `tmux.height` | `"80%"` / `"60%"` | Popup size |
| `ai.enabled` | `true` | Enables `atuin ai` |
No sync server, key, or session is configured in the repo. atuin keeps its encryption key and session under `~/.local/share/atuin`, outside chezmoi, and uses the default sync server.
> [!WARNING]
> **Misplaced keys**
>
> Several keys sit after the `[dotfiles]` table header, so TOML puts them inside `[dotfiles]` rather than at the top level, and atuin ignores them. `atuin config get --resolved` confirms it:
>
> | Key as written | Intended | Resolved |
> | --- | --- | --- |
> | `search_mode` | `"daemon-fuzzy"` | `fuzzy` |
> | `enter_accept` | `true` | `false` |
> | `workspaces` | `true` | `false` |
> | `style` | `"auto"` | `compact` |
> | `inline_height_shell_up_key_binding` | `20` | unknown key |
> | `auto_sync` | `true` | `true` (the default) |
> | `update_check` | `true` | `true` (the default) |
>
> `syntax_highlight = true` has the same problem: it sits under `[ai]`, while the comment block above it documents the `[ui]` table.
>
> To apply these settings, move the keys above `[dotfiles]` (and `syntax_highlight` under `[ui]`) in `dot_config/atuin/private_config.toml`.
## Theme
`themes/cherry-blossom.toml` defines the selected theme, from the CherryBlossom palette (`palettes/CherryBlossom.json` in [prdlk/noctalia](https://github.com/prdlk/noctalia)) shared with Ghostty, tmux, lazygit, and omp:
| Key | Color |
| --- | --- |
| `AlertInfo` | `#a78266` |
| `AlertWarn` | `#ae825f` |
| `AlertError` | `#bf615d` |
| `Annotation` | `#a6958f` |
| `Guidance` | `#8f9aa0` |
| `Important` | `#cf716f` |
| `Title` | `#cf716f` |
| `Muted` | `#a6958f` |
| `SyntaxCommand` | `#cf716f` |
| `SyntaxFlag` | `#a78266` |
| `SyntaxString` | `#a6b084` |
| `SyntaxVariable` | `#c17e4a` |
| `SyntaxOperator` | `#ad6756` |
| `SyntaxComment` | `#a6958f` |
`themes/cyberdream.toml` is still shipped (the palette shared with [fish](fish.md#theme) and [Starship](starship.md#palette)) but not selected.
+156
View File
@@ -0,0 +1,156 @@
# bash and zsh
Secondary shells that share fish's aliases, starship prompt, atuin history, and cd-style helper functions.
bash and zsh are set up so a shell other than fish feels the same: the same aliases, the same starship prompt, atuin history, ports of the fish helpers that have to change the shell's own directory, and the same picker key bindings.
| Source (repo) | Target |
| --- | --- |
| `dot_bashrc` | `~/.bashrc` |
| `dot_zshrc` | `~/.zshrc` |
| `dot_config/zsh/keybinds.zsh` | `~/.config/zsh/keybinds.zsh` |
| `dot_config/bash/keybinds.bash` | `~/.config/bash/keybinds.bash` |
| `dot_config/shell/aliases.sh.tmpl` | `~/.config/shell/aliases.sh` (template) |
| `dot_config/bash/functions/*.bash` | `~/.config/bash/functions/` |
| `dot_config/zsh/functions/*.zsh` | `~/.config/zsh/functions/` |
`aliases.sh.tmpl` is the only template. It renders the alias list from `.chezmoidata/aliases.yaml` ([chezmoi data](../chezmoi/data.md)). The other files apply as-is on every role.
## Installation
bash is part of the base system. zsh itself is not listed in `.chezmoidata/packages.yaml`, but `zsh-syntax-highlighting`, `starship`, `atuin`, `eza`, and `zoxide` are. `suvadu` (the `suv` history tool) is declared under `packages.shared.cargo` and installed by [15-tools](../chezmoi/scripts.md#15-tools).
## ~/.bashrc
Interactive shells only (`[[ $- != *i* ]] && return`). In order:
1. **PATH.** Prepends `~/.local/bin` if missing, sources `~/.cargo/env` if present, and appends `~/.maestro/bin` if it exists. Exports `GHQ_ROOT` and `CODE_ROOT` as `~/Code`.
2. **Go.** `GOPATH=~/.local/share/go`, `GOBIN=$GOPATH/bin`, and `GOBIN` prepended to `PATH`. mise provides `GOROOT`. The same two values are also in `~/.config/go/env` (`dot_config/go/env.tmpl`), Go's own config file, so every `go` invocation uses them even without these variables (cron, `sudo -u`, `env -i`, GUI apps).
3. **Android SDK.** `ANDROID_HOME` and `ANDROID_SDK_ROOT` set to `~/.local/share/android`. `platform-tools`, `cmdline-tools/latest/bin`, and `emulator` are appended if they exist. Exports `GITHUB_TOKEN` from `gh auth token` unless it is already set.
4. **Aliases.** Sources `~/.config/shell/aliases.sh`.
5. **Functions.** Sources every `~/.config/bash/functions/*.bash`.
6. **Flyline.** If `~/.local/lib/libflyline.so` exists, loads it with `enable -f … flyline` for richer line editing.
7. Sources `~/.config/envman/load.sh` if it is non-empty.
8. **Prompt.** `eval "$(starship init bash)"` if starship is installed.
9. **television.** `eval "$(tv init bash)"` if `tv` is installed (the `_tv` completion), then sources `~/.config/bash/keybinds.bash`.
10. **atuin.** `eval "$(atuin init bash)"` if atuin is installed (the package's binary; see [atuin](atuin.md#installation)). atuin comes after tv, so it keeps <kbd>ctrl-r</kbd> and <kbd>up</kbd>.
## ~/.zshrc
1. **History.** `HISTFILE=~/.zsh_history`, `HISTSIZE=10000`, `SAVEHIST=10000`, `setopt SHARE_HISTORY HIST_IGNORE_DUPS`.
2. **Completion.** `autoload -Uz compinit && compinit`.
3. The same PATH, Go, Android, and `GITHUB_TOKEN` blocks as `.bashrc`, including `GHQ_ROOT` and `CODE_ROOT`.
4. Sources `~/.config/shell/aliases.sh`.
5. Sources every `~/.config/zsh/functions/*.zsh`.
6. Sources `~/.config/zsh/keybinds.zsh`.
7. `eval "$(suv init zsh)"` if suvadu is installed.
8. Sources `~/.config/envman/load.sh` if it is non-empty.
9. **Prompt.** If starship is installed: `eval "$(starship init zsh)"`, `ZLE_RPROMPT_INDENT=0` (the right-prompt clock sits flush with the edge), and a transient prompt. A `line-finish` zle hook swaps `PROMPT`/`RPROMPT` to the `transient` and `transient_right` starship profiles (`❯`, colored by the last exit status, and the time) and redraws, so past prompts collapse as in fish; a `precmd` hook restores the full prompt. Starship's zsh init also sets `PROMPT2`, the `∙` continuation prompt for unfinished lines. See [Starship](starship.md#shell-integration).
**Window title.** A `precmd` hook ports `fish_title`: it sets the terminal title to `%~`, prefixed with the first 10 characters of the hostname over SSH, and inside tmux also sets the window name (`ESC k`), like fish's `50-terminal-title.fish`. Unlike fish's `prompt_pwd`, the path is not abbreviated.
10. `eval "$(tv init zsh)"` if `tv` is installed (<kbd>ctrl-t</kbd> smart autocomplete).
11. **Syntax highlighting.** Sources the first `zsh-syntax-highlighting.zsh` found in `/usr/share/zsh/plugins/zsh-syntax-highlighting/`, or `/usr/local/share/zsh-syntax-highlighting/`. The comment says this must stay last, because it wraps every zle widget defined before it.
12. **atuin.** `eval "$(atuin init zsh)"` if atuin is installed. This line comes after the syntax-highlighting block, so despite the comment, syntax highlighting is not actually last. atuin loads after tv, so it keeps <kbd>ctrl-r</kbd>.
## zsh key bindings
`keybinds.zsh` ports `fish_user_key_bindings.fish` to zle. Unlike fish, zsh uses the **emacs** keymap: `bindkey -e` pins it so that `$EDITOR=nvim` does not switch zsh into vi mode. `unsetopt flow_control` frees <kbd>ctrl-q</kbd> and <kbd>ctrl-s</kbd> from the terminal.
A generic widget `_zle-run` is registered as `run-<cmd>` for `purple`, `clip-last`, `omp`, `yy`, and `lazygit`. It clears the pending display (`zle -I`), runs the command with the tty on stdin, then `zle reset-prompt`. Widgets run in the shell process, so directory changes made by `gho` and `yy` persist.
The tv pickers have their own widgets, because `tv --inline` opens on the line below the prompt and leaves the cursor there on exit. `zle -I` followed by `zle reset-prompt` redraws the prompt where zle last drew it, wiping anything printed below; `zle -I` alone draws a fresh prompt under the output.
| Widget | Runs | Then |
| --- | --- | --- |
| `tv-git-files` | `tv git-files --inline` | `\033[A`, `reset-prompt` |
| `gho-inline` | `gho` | cwd unchanged: `\033[A`, `reset-prompt`. After a cd: nothing, so the new prompt lands below onefetch |
| `pj-inline`, `pj-down` | `_zle-pj`: `pj` with `PJ_STATE` set to a temp file (`PJ_QUIET=1` for `pj-down`) | `open` (cancelled): `\033[A`, `reset-prompt`. `ran`: nothing, so the task output stays. Empty (no task sources): `reset-prompt` |
| Key | Action |
| --- | --- |
| <kbd>ctrl-e</kbd> | `tv-git-files` (pick a file, open it in nvim) |
| <kbd>ctrl-f</kbd> | `yy` |
| <kbd>ctrl-b</kbd> | `purple` (an external command; this repo does not provide it) |
| <kbd>ctrl-y</kbd> | `clip-last` |
| <kbd>ctrl-o</kbd> | `gho-inline` (`ghq-repos` cable, cd) |
| <kbd>ctrl-p</kbd> | `pj-inline` (`pj` cable) |
| <kbd>ctrl-t</kbd> | tv smart autocomplete (from `tv init zsh`) |
| <kbd>down</kbd> | `pj-down`: `PJ_QUIET=1 pj` on an empty prompt, otherwise `down-line-or-history` (bound to `^[[B` and `^[OB`) |
| <kbd>ctrl-a</kbd> | `omp-session`: resume or start omp for the current repo ([Scripts](scripts.md#omp-session)) |
| <kbd>ctrl-n</kbd> | `omp`: always a new omp session (replaces zsh's `down-line-or-history` on that key; <kbd>down</kbd> still walks history) |
| <kbd>ctrl-g</kbd> | `lazygit` |
| <kbd>ctrl-q</kbd> | `exit` |
| <kbd>ctrl-c</kbd> | At the prompt: kill the buffer, clear the screen and kitty scrollback (`ESC[2J ESC[3J`), reset the prompt |
<kbd>ctrl-c</kbd> is the tty interrupt character, so `bindkey '^C'` would never fire. The binding is a `TRAPINT` function instead. At the prompt it runs `zle .kill-buffer`, prints the clear sequence, and runs `zle .reset-prompt`. While a command is running, SIGINT keeps its normal behavior (returns `128+SIGINT`).
The fish-only binds <kbd>ctrl-w</kbd> (atuin AI) and <kbd>?</kbd> have no zsh equivalent. <kbd>ctrl-h/j/k/l</kbd> belong to kitty ([kitty](../terminal/kitty.md)) and are not bound here. The cables are described on the [television](../tools/television.md) page.
## bash key bindings
`keybinds.bash` gives bash the same picker keys as fish and zsh. bash keeps readline's default emacs keymap. Every picker runs `tv --inline`; when nothing was printed below the prompt, `_kb_up` (`\033[A` on `/dev/tty`) steps back so readline redraws the prompt in place after `bind -x`.
| Key | Action |
| --- | --- |
| <kbd>ctrl-o</kbd> | Macro `\C-x1\C-x2\C-x3\C-x0` (below): pick from the `ghq-repos` cable and cd |
| <kbd>ctrl-e</kbd> | `_kb_git_files`: `tv git-files --inline`, then `_kb_up` |
| <kbd>ctrl-p</kbd> | `_kb_pj`: `pj` with `PJ_STATE` set to a temp file; `_kb_up` only when the state is `open` (cancelled) |
| <kbd>down</kbd> | `PJ_QUIET=1 _kb_pj` on an empty prompt, otherwise `history-search-forward` |
| <kbd>ctrl-t</kbd> | `_kb_tv_autocomplete`: runs `tv --autocomplete-prompt … --inline` on the text left of the cursor, `_kb_up`, and inserts the picks there, each `printf %q`-quoted |
`bind -x` runs in the shell process, so a `cd` persists, but readline keeps drawing the prompt it started with. <kbd>ctrl-o</kbd> is therefore a macro:
1. `\C-x1` runs `_kb_gho`: stashes `READLINE_LINE`, runs `tv ghq-repos --inline`, `_kb_up`, and cds into the pick. It does not call the `gho` function. After a cd it empties the line and rebinds `\C-x2` to `accept-line` and `\C-x3` to `bind -x _kb_gho_show`; when cancelled it rebinds both to `redraw-current-line`.
2. `\C-x2` accepts the empty line, so bash prints a fresh prompt for the new cwd. Nothing reaches history.
3. `\C-x3` runs `_kb_gho_show` (`clear`, then `onefetch` in git repos); readline redraws the prompt below the output.
4. `\C-x0` runs `_kb_restore`, which puts the typed text back.
<kbd>down</kbd> (`\e[B` and `\eOB`) is the macro `\C-x4\C-x5`. `bind -x` cannot call readline functions, so `_kb_down` (`\C-x4`) rebinds `\C-x5` to `history-search-forward` when the line is non-empty, or runs `PJ_QUIET=1 _kb_pj` and rebinds `\C-x5` to `redraw-current-line`.
## Shared aliases: aliases.sh
`~/.config/shell/aliases.sh` is rendered from `.chezmoidata/aliases.yaml`. Each YAML `section` becomes a comment, and each item becomes `alias <name>='<cmd>'`. The template then appends shell-specific extras that are not in the YAML:
**~/.config/shell/aliases.sh**
```sh
# Clipboard
if command -v wl-copy >/dev/null 2>&1; then
alias cpy='wl-copy'
alias pst='wl-paste'
fi
q() {
if [ -n "$YAZI_ID" ] && command -v ya >/dev/null 2>&1; then
ya emit quit 2>/dev/null
fi
exit
}
alias celar='clear'
alias claer='clear'
if command -v eza >/dev/null 2>&1; then
alias ls='eza --icons'
fi
```
- **Clipboard detection.** `cpy`/`pst` map to Wayland's `wl-copy`/`wl-paste` when available.
- **`q`.** Exits the shell. If the shell was spawned from yazi's own `q` binding (`$YAZI_ID` set), it runs `ya emit quit` first so the parent yazi closes too instead of bouncing back.
- **Typo guards and eza.** `celar`/`claer` → `clear`, and `ls` → `eza --icons` when eza is installed. The file is sourced only by interactive shells, so scripts keep the real `ls`.
The full alias table, including the differences from fish, is on the [Aliases](aliases.md) page.
## Per-shell functions
Each directory holds one function per file. bash and zsh source every file at startup; the bash and zsh versions have identical bodies.
| Function | Behavior |
| --- | --- |
| `c [dir]` | `clear` with no argument, `cd "$1"` with one |
| `cd [dir]` | No argument: `$HOME`. `-`: previous dir. Otherwise `builtin cd`, falling back to `zoxide query -- "$1"`. Records the result with `zoxide add "$PWD"` and preserves cd's exit status |
| `clip-last` | Re-runs the previous history entry (`fc -ln -1 -1`, skipping itself if the line editor already recorded `clip-last`), pipes the output to `wl-copy`, and sends a `notify-send -a shell` notification if available |
| `ghg <repo>` | `ghg-clone "$@"`, cd into the printed path, `clear && onefetch` |
| `ghn <name>` | `command ghn "$@"`, cd into the printed path |
| `gho` | `dir=$(tv ghq-repos --inline)`, return if empty, cd into it, `clear`, and `onefetch` if it is a git repo. No cursor handling; bash <kbd>ctrl-o</kbd> does not call it |
| `yy [args]` | `yazi --cwd-file=<tmp>`, then cd to the directory written there if it changed |
The `ghg` helper script is named `ghg-clone` because an unrelated cargo binary already uses the name `ghg`. The [Scripts](scripts.md#why-some-helpers-are-shell-functions) page explains why these helpers are shell functions and not scripts.
+318
View File
@@ -0,0 +1,318 @@
# fish
The primary interactive shell — conf.d layout, environment, vi-mode key bindings, the cyberdream theme, completions, and every autoloaded function.
fish is the primary interactive shell. The config is split into autoloaded files: `conf.d/` for startup snippets, `functions/` for one function per file, `completions/` for completion scripts, and `themes/` for the color theme. `config.fish` runs last and holds the prompt and atuin setup.
| Source (repo) | Target |
| --- | --- |
| `dot_config/fish/config.fish` | `~/.config/fish/config.fish` |
| `dot_config/fish/conf.d/*.fish` | `~/.config/fish/conf.d/` |
| `dot_config/fish/functions/*.fish` | `~/.config/fish/functions/` |
| `dot_config/fish/completions/*.fish` | `~/.config/fish/completions/` |
| `dot_config/fish/themes/cyberdream.theme` | `~/.config/fish/themes/cyberdream.theme` |
| `dot_config/fish/fish_plugins` | `~/.config/fish/fish_plugins` |
| `dot_config/fish/auto-Niri.fish` | not applied: it is listed in `.chezmoiignore.tmpl` (see below) |
`dot_config/fish` is a git submodule ([prdlk/fish](https://github.com/prdlk/fish)). Edit inside the submodule, commit there, then commit the updated pointer in the chezmoi repo. None of these files are templates. chezmoi runs in symlink mode, so each target is a symlink back into the source tree and edits to `~/.config/fish/...` land in the submodule directly. `~/.config/fish/fish_variables` (universal variables, including the selected theme) is listed in `.chezmoiignore.tmpl` and stays local to each machine. `~/.config/fish/conf.d/secrets.fish` is local-only: it is listed in `.chezmoiignore.tmpl` and in the fish repo's `.gitignore`, and never committed.
## Installation
`fish` is in `.chezmoidata/packages.yaml` under `packages.arch.pacman`, and `.chezmoiscripts/run_onchange_before_10-packages.sh.tmpl` installs it with pacman. The tools that fish initializes (starship, zoxide, atuin, fzf, eza, gum, yazi, television, mise) come from the same manifest; Bun is a global mise tool (`packages.shared.mise`) and rustup comes from [15-tools](../chezmoi/scripts.md#15-tools). hishtory, envman, and the Go, Android, and Foundry toolchains are not in the manifest; fish initializes each one only if it is present.
### Plugins
`fish_plugins` is a [fisher](https://github.com/jorgebucaran/fisher) manifest with one entry:
**~/.config/fish/fish_plugins**
```text
jorgebucaran/nvm.fish
```
The plugin's files are committed to the repo (`conf.d/nvm.fish`, `functions/nvm.fish`, `functions/_nvm_*.fish`, `completions/nvm.fish`), so fish has `nvm` without fisher. The plugin stores Node builds in `$XDG_DATA_HOME/nvm` (default `~/.local/share/nvm`) and downloads from `https://nodejs.org/dist`. If `nvm_default_version` is set, it runs `nvm use --silent $nvm_default_version` in each interactive shell.
## Startup order
fish sources `conf.d/*.fish` in name order and then `config.fish`. The numbered files load first:
| File | Purpose |
| --- | --- |
| `00-path.fish` | `PATH` entries and `PNPM_HOME` |
| `10-env.fish` | Non-secret environment, greeting, fzf colors |
| `20-aliases.fish` | Aliases, clipboard aliases, `q`, typo guards |
| `30-ssh-agent.fish` | One ssh-agent shared across shells |
| `40-tools.fish` | mise, television, starship, zoxide, entire, hishtory, unsloth |
| `50-terminal-title.fish` | tmux/screen window name from `$PWD` |
| `inir-env.fish` | `INIR_VENV` and `ILLOGICAL_IMPULSE_VIRTUAL_ENV` → `~/.local/state/quickshell/.venv` |
| `inir-path.fish` | Prepends `~/.local/bin` to `PATH` if missing |
| `nvm.fish` | nvm.fish plugin events and default-version activation |
| `opencode-background-subagents.fish` | `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`, `OPENCODE_ENABLE_EXA=1` |
| `pnpm-filter.fish` | `pnpmf*` workspace helpers |
| `rustup.fish` | Sources `~/.cargo/env.fish` |
| `secrets.fish` | API credentials (names below) |
| `uv.env.fish` | Sources `~/.local/bin/env.fish` (written by the uv installer) |
### 00-path.fish
Runs for every shell, including non-interactive ones, so scripts get the same `PATH`.
| Entry | Position |
| --- | --- |
| `~/.bun/bin`, `~/.local/share/go/bin`, `~/.maestro/bin`, `~/.grok/bin`, `~/.opencode/bin` | prepended |
| `~/.foundry/bin` | appended |
| `~/.local/share/android/platform-tools`, `cmdline-tools/latest/bin`, `emulator` | appended |
| `~/local/share/pnpm` | appended (note the missing dot; this path does not match `PNPM_HOME`) |
| `$PNPM_HOME/bin` | prepended; `PNPM_HOME=~/.local/share/pnpm` |
`~/.local/bin` comes from `inir-path.fish` and `~/.cargo/bin` from `rustup.fish`.
### 10-env.fish
| Variable | Value |
| --- | --- |
| `fish_greeting` | empty (no greeting) |
| `FISH_BOTTOM_PROMPT` | `0` unless inherited. `1` pins the prompt to the bottom row |
| `HSA_OVERRIDE_GFX_VERSION` | `11.5.0` (ROCm) |
| `ROCR_VISIBLE_DEVICES`, `HIP_VISIBLE_DEVICES` | `0` |
| `__EGL_VENDOR_LIBRARY_FILENAMES`, `VK_DRIVER_FILES` | Mesa's `50_mesa.json` and RADV's `radeon_icd.json`, only when `/dev/dri/by-path/pci-0000:c4:00.0-render` (the Strix Halo iGPU) exists. Keeps the session off the compute-only eGPU, see [Thunderbolt eGPU](../desktop/README.md#thunderbolt-egpu-compute-only) |
| `EGET_BIN` | `~/.local/bin` |
| `GHQ_ROOT`, `CODE_ROOT` | `~/Code` |
| `ANDROID_HOME`, `ANDROID_SDK_ROOT` | `~/.local/share/android` |
| `GITEA_LOGIN` | `prad.codes` |
| `GITLAB_HOST` | `git.sonr.org` |
| `GITHUB_TOKEN` | `gh auth token`, unless already set and only if `gh` is logged in |
| `EDITOR`, `VISUAL` | `nvim` |
| `BROWSER` | `/usr/bin/brave-origin` if that binary exists |
| `CHROME_EXECUTABLE` | `/usr/bin/chromium` if it exists |
| `PUPPETEER_EXECUTABLE_PATH` | `~/.local/bin/chrome-headless-shell` |
| `ZEAL_DOCSETS` | the Zeal flatpak docset directory |
| `SHARP_IGNORE_GLOBAL_LIBVIPS` | `1` |
| `FZF_DEFAULT_OPTS` | cyberdream fzf theme (below) |
`GOPATH`/`GOBIN` are not set here. The comment explains why: mise's fish hook re-applies its `[env]` on every prompt, so Go paths are pinned in `~/.config/mise/config.toml`, and mise provides `GOROOT`. `~/.config/go/env` holds the same values for any `go` run outside a shell.
**~/.config/fish/conf.d/10-env.fish**
```fish
set -gx FZF_DEFAULT_OPTS "
--height=~40%
--layout=default
--info=inline-right
--border=rounded
--prompt=' '
--pointer='▶'
--marker='✓'
--color=fg:#ffffff,fg+:#ffffff,bg:-1,bg+:#3c4048
--color=hl:#5ef1ff,hl+:#5ef1ff
--color=info:#7b8496,header:#7b8496,border:#7b8496
--color=prompt:#5ef1ff,pointer:#ffaecf,marker:#5eff6c,spinner:#f1ff5e
--color=gutter:-1,query:#ffffff
"
```
### 20-aliases.fish
Editor, Bun, directory-jump, and tool aliases, followed by:
- `cpy`/`pst`: `wl-copy`/`wl-paste` if `wl-copy` exists (the shared fish config also has a `pbcopy`/`pbpaste` fallback that never triggers here).
- `q`: exits the shell. If `$YAZI_ID` is set and `ya` exists, it first runs `ya emit quit` so the yazi that spawned the shell quits too.
- Interactive only: `celar` and `claer` → `clear`, and `ls` → `eza --icons` when eza is installed. `clear` itself is not aliased, because an alias would shadow `functions/clear.fish`.
The full list is on the [Aliases](aliases.md) page. This file is plain fish, not rendered from `.chezmoidata/aliases.yaml`, so its alias set differs from bash/zsh in a few places.
### 30-ssh-agent.fish
Interactive shells without `SSH_AUTH_SOCK` source `~/.ssh/agent.env.fish` and probe with `ssh-add -l`. Exit code 2 means no agent is reachable. In that case it starts `ssh-agent -c`, rewrites its csh `setenv` lines into `set -gx`, saves them to `~/.ssh/agent.env.fish`, and sources the file. An `SSH_AUTH_SOCK` provided from outside (systemd unit, forwarding, keyring) is left alone.
### 40-tools.fish
- `mise activate fish` runs for every shell, so scripts see mise shims too.
- Interactive only, in order:
1. Print `~/.local/state/quickshell/user/generated/terminal/sequences.txt` if present. These are escape sequences that apply wallpaper-generated Material You terminal colors from quickshell.
2. `tv init fish` (television), if installed.
3. `starship init fish`, `zoxide init fish`, and `entire completion fish`, each only if installed.
4. `tv init fish` a second time. The comment says it runs before hishtory so hishtory keeps <kbd>ctrl-r</kbd>.
5. Source `~/.hishtory/config.fish` if present.
6. Activate the unsloth studio venv (`~/.unsloth/studio/unsloth_studio/bin/activate.fish`) if present.
atuin is initialized later, in `config.fish`, and binds <kbd>ctrl-r</kbd> itself.
### 50-terminal-title.fish
Defines `terminal_title`, which runs on every `PWD` change and prints `ESC k <prompt_pwd> ESC \`. That sequence sets the tmux or screen window name. `functions/fish_title.fish` sets the regular terminal title.
### pnpm-filter.fish
pnpm workspace helpers. `__pnpm_pick_pkg` lists workspace packages with `pnpm m ls --depth -1 --json | jq`, and you pick one with `gum filter`.
| Function | Does |
| --- | --- |
| `pnpmf <deps…>` | Pick a package, then `pnpm --filter <pkg> add <deps>` inside a `gum spin` |
| `pnpmfd <deps…>` | Same with `add -D` |
| `pnpmfr <deps…>` | `pnpm --filter <pkg> remove <deps>` |
| `pnpmfw` | Pick a target package and a sibling, then `pnpm --filter <pkg> add <sibling> --workspace` |
| `pnpmfx` | Pick a package, then pick one of its `package.json` scripts with `gum filter` and run it |
### secrets.fish
Exports credentials as global variables. The values are machine-specific and are not documented here. Variable names:
`PGDATABASE` (local Postgres connection string), `GITLAB_TOKEN`, `OPPER_API_KEY`, `OPENAI_API_KEY`, `GROQ_API_KEY`, `WEBAWESOME_NPM_TOKEN`, `TAVILY_API_KEY`, `FINANCIAL_DATASETS_API_KEY`, `PIMLICO_API_KEY`, `WAKATIME_API_KEY`, `LINEAR_CLI_PROFILE`, `LINEAR_API_KEY`, `LINEAR_TEAM_ID`.
> [!WARNING]
> Put your own values in `~/.config/fish/conf.d/secrets.fish` on each machine. Do not reuse credentials from another clone.
## config.fish
The header comment says the file should stay empty, but it holds the following:
1. **Starship with transient prompt.** `starship_transient_prompt_func` renders `starship module character` and `starship_transient_rprompt_func` renders `starship module time`. Then `starship init fish | source` and `enable_transience` run, so past prompts collapse to `❯` with the time on the right. See [Starship](starship.md).
2. **Bottom-pinned prompt.** In interactive shells with `FISH_BOTTOM_PROMPT=1`, `tput cup $LINES 0` moves the cursor to the last row. The comment calls `1` the default, but `10-env.fish` sets `0`, so the prompt stays at the top unless you export `FISH_BOTTOM_PROMPT=1`.
3. `fish_add_path ~/.grok/bin` (grok installer block).
4. Sources `~/.config/envman/load.fish` if it is non-empty.
5. **atuin.** Interactive shells run `atuin init fish | source`. See [atuin](atuin.md).
6. `fish_add_path ~/.cmux/bin` and `~/.cache/.bun/bin`.
7. Sources the first readable `~/.config/herdr/plugins/github/herdr-automatic-rename-*/shell/hook.fish`. This is herdr's automatic pane-rename hook ([herdr](../terminal/herdr.md)).
8. A copy of the `_atuin_ai_question_mark` function that `atuin ai init fish` used to emit (atuin 18.21 removed that command), followed by `bind "?" _atuin_ai_question_mark`.
## Key bindings
`functions/fish_user_key_bindings.fish` turns on **vi mode** (`fish_vi_key_bindings`) first, then adds custom binds in both `insert` and `default` (normal) modes, so they work in either mode.
| Key | Action |
| --- | --- |
| <kbd>ctrl-e</kbd> | `_tv_inline git-files`, then repaint: pick a file in the repo and open it in nvim ([television](../tools/television.md#git-files)) |
| <kbd>ctrl-f</kbd> | `yy`: yazi, then cd to the directory yazi exited in |
| <kbd>ctrl-b</kbd> | `purple`, then repaint (an external command; this repo does not provide it) |
| <kbd>ctrl-y</kbd> | `clip-last`: re-run the last command and copy its output |
| <kbd>ctrl-o</kbd> | `_gho_bind`: `gho` (pick a ghq repo or Obsidian vault from the `ghq-repos` cable and cd into it), then repaint |
| <kbd>ctrl-p</kbd> | `_pj_bind`: `pj`, the task runner on the `pj` cable ([Scripts](scripts.md#pj)), then repaint |
| <kbd>ctrl-a</kbd> | `omp-session`, then repaint: resume or start omp for the current repo by branch and session age, opening the `omp-sessions` cable only when nothing qualifies ([Scripts](scripts.md#omp-session)) |
| <kbd>ctrl-n</kbd> | `omp`, then repaint: always a new omp session |
| <kbd>ctrl-w</kbd> | `atuin ai inline`: natural-language command overlay |
| <kbd>ctrl-z</kbd> | `_jobs_bind`: with one background job, `fg` it; with several, pick one from the `jobs` cable ([television](../tools/television.md#jobs)) and `fg` that; with none, nothing. While a job holds the terminal, ctrl-z is still the tty's suspend, so the key toggles suspend and resume |
| <kbd>ctrl-g</kbd> | `lazygit` |
| <kbd>down</kbd> | `_pj_bind_down`: opens the `pj` cable on an idle, empty prompt, otherwise fish's `down-or-search` |
| <kbd>ctrl-c</kbd> | Clears the command line, runs `clear` (screen and kitty scrollback), repaints |
| <kbd>ctrl-q</kbd> | `exit` |
Bindings added by tool init scripts:
| Key | Mode | Action | Source |
| --- | --- | --- | --- |
| <kbd>ctrl-r</kbd> | default and insert | `_atuin_search` | `atuin init fish` |
| <kbd>up</kbd> | default and insert | `_atuin_bind_up` (atuin history, directory filter) | `atuin init fish` |
| <kbd>?</kbd> | default (vi normal) | `_atuin_ai_question_mark`: on an empty buffer, opens `atuin ai inline` | `config.fish` |
| <kbd>ctrl-t</kbd> | insert | `tv_smart_autocomplete` | `tv init fish` |
`_pj_bind_down` hands the key to `down-or-search` when the buffer is non-empty, the completion pager is open, or history search is active. Otherwise it runs `_pj_bind --quiet` (`PJ_QUIET=1`), so pressing down outside a project does nothing and prints no error.
The pickers run `tv --inline`, which opens on the line below the prompt and leaves the cursor there when it exits. The wrappers put the prompt back before `commandline -f repaint`, writing the cursor moves to `/dev/tty`:
| Wrapper | Runs | Then |
| --- | --- | --- |
| `_tv_inline ARGS` | `tv ARGS --inline` | `\033[A` (cursor up), returns tv's status |
| `_gho_bind` | `gho` | `\033[A` if the cwd did not change; `\n` after a cd, so the prompt lands below onefetch |
| `_pj_bind [--quiet]` | `pj` with `PJ_STATE` set to a temp file | `\033[A` for `open` (cancelled) or empty (no task sources); `\n` for `ran` (the task printed output) |
| `_jobs_bind` | `fg` of the only job, or `jobs` with `TV_JOBS` set to a temp file holding the job table | `\033[A` (via `_tv_inline`) when the picker is cancelled |
`_jobs_bind` runs `fg` by replacing the command line with ` __jobs_bind_fg <id>` (leading space: kept out of history) and executing it, not from inside the binding. A binding runs while fish's reader still owns the terminal with extended keys on, so a job started with `&` that had never held the terminal got ctrl-z and ctrl-c as escape sequences instead of signals. `__jobs_bind_fg` erases the echoed prompt line (`\033[A\033[2K` on `/dev/tty`) and runs `fg` with its "Send job … to foreground" message sent to `/dev/null`, so resuming leaves nothing on screen. Text already typed is put back at the next prompt by a one-shot `fish_prompt` handler.
`omp-session` (<kbd>ctrl-a</kbd>) does its own `\033[A` when its `omp-sessions` picker is cancelled, so the binding only repaints.
zsh and bash bind <kbd>ctrl-o</kbd>, <kbd>ctrl-e</kbd>, <kbd>ctrl-p</kbd>, <kbd>down</kbd>, and <kbd>ctrl-t</kbd> the same way ([bash and zsh](bash-zsh.md)). Inside television, <kbd>ctrl-e</kbd> runs the channel's `edit` action and <kbd>ctrl-h</kbd> its `goto_parent_dir` action (`~/.config/television/config.toml`). The config and cables are on the [television](../tools/television.md) page.
> [!NOTE]
> Under kitty, <kbd>ctrl-g</kbd> (lazygit overlay), <kbd>ctrl-y</kbd> (yazi split), and <kbd>ctrl-h/j/k/l</kbd> (split navigation) are mapped in `kitty.conf` and never reach fish. The fish binds for those keys only apply in other terminals. See [kitty](../terminal/kitty.md).
The prompt shows `❮` in green in vi normal mode (starship's `vimcmd_symbol`).
## Theme
`themes/cyberdream.theme` is a fish theme file with colors from [cyberdream.nvim](https://github.com/scottmckendry/cyberdream.nvim) and preferred background `#16181a`. The repo ships the file but does not select it. The active theme is stored in `fish_variables`, which chezmoi ignores, so pick it once per machine with `fish_config theme save cyberdream`.
| Variable | Color |
| --- | --- |
| `fish_color_normal` | `#ffffff` |
| `fish_color_command` | `#5ef1ff` |
| `fish_color_param`, `fish_color_escape` | `#ffaecf` |
| `fish_color_keyword`, `fish_color_host` | `#5eff6c` |
| `fish_color_quote`, `fish_color_option`, `fish_color_host_remote` | `#f1ff5e` |
| `fish_color_redirection`, `fish_color_operator` | `#5ea1ff` |
| `fish_color_end` | `#bd5eff` |
| `fish_color_comment`, `fish_color_gray`, `fish_color_autosuggestion` | `#7b8496` |
| `fish_color_error`, `fish_color_cancel`, `fish_color_status` | `#ff6e5e` |
| `fish_color_selection`, `fish_color_search_match` | background `#3c4048` |
| `fish_color_cwd` | `#ffbd5e` |
| `fish_color_user` | `#5ef5d2` |
| `fish_pager_color_progress`, `fish_pager_color_description` | `#7b8496` |
| `fish_pager_color_prefix` | `#5ea1ff` |
| `fish_pager_color_completion` | `#ffffff` |
`FZF_DEFAULT_OPTS` in `10-env.fish` uses the same palette.
## Completions
| File | Command | Mechanism |
| --- | --- | --- |
| `ccc.fish` | `ccc` | Typer-generated: calls `ccc` with `_CCC_COMPLETE=complete_fish` |
| `grok.fish` | `grok` | Static clap-style completion script (global options, subcommands) |
| `mise.fish` | `mise` | `mise completion fish \| source` (generated at load time) |
| `nvm.fish` | `nvm` | From the nvm.fish plugin |
| `omp.fish` | `omp` | `omp completions fish \| source` (generated at load time) |
| `ostt.fish` | `ostt` | Static clap-style completion script |
## Functions
Every file in `functions/` is autoloaded by name. Several also exist as POSIX scripts in `~/.local/bin` for bash and zsh. In fish the function shadows the script. See [Scripts](scripts.md) for why the cd-style helpers must stay shell functions.
| Function | Purpose |
| --- | --- |
| `_nvm_index_update` | nvm.fish internal: downloads `index.tab` from `$nvm_mirror` into `$nvm_data/.index` |
| `_nvm_list` | nvm.fish internal: lists installed Node versions plus `system` |
| `_nvm_version_activate` | nvm.fish internal: sets `nvm_current_version` and prepends its `bin` to `PATH` |
| `_nvm_version_deactivate` | nvm.fish internal: removes a version's `bin` from `PATH` |
| `_gho_bind` | <kbd>ctrl-o</kbd> handler: `gho`, then cursor fix and repaint (see [Key bindings](#key-bindings)) |
| `_pj_bind` | <kbd>ctrl-p</kbd> handler: `pj` with `PJ_STATE`, then cursor fix and repaint. `--quiet` sets `PJ_QUIET=1` |
| `_jobs_bind` | <kbd>ctrl-z</kbd> handler: `fg` the only job or pick one from the `jobs` cable (see [Key bindings](#key-bindings)) |
| `_pj_bind_down` | Down-arrow handler: `_pj_bind --quiet` on an empty idle prompt, else `down-or-search` |
| `_tv_inline` | `tv $argv --inline`, then moves the cursor up one line on `/dev/tty` |
| `ai` | `gum choose` among `claude`, `opencode`, `codex`, `hermes`, `fractal`. `claude` runs with `--dangerously-skip-permissions` |
| `c` | `clear` with no argument, `cd <dir>` with one |
| `c2p` | fzf-pick a `.hbs` template under `~/.config/code2prompt` and run `code2prompt --template` |
| `cd` | `builtin cd`. If that fails, `zoxide query` the argument. Every successful cd is recorded with `zoxide add` |
| `clear` | Prints `ESC[2J ESC[3J ESC[1;1H` to clear the screen and kitty scrollback, then re-pins the prompt if `FISH_BOTTOM_PROMPT=1`. Falls back to `command clear` when not interactive |
| `clip-last` | Re-runs `$history[1]`, pipes the output to `cpy`, and sends a `notify-send` notification |
| `cp-appid` | fzf-pick a `.desktop` app ID and copy it with `wl-copy` (desktop helper) |
| `czs` | chezmoi sync: `chezmoi status`, `chezmoi re-add` of drifted files, commit the source repo, then `git pull --rebase --autostash` and `git push` if ahead. Optional targets scope it (for example `czs ~/.config/fish`). Messages print with a `chs:` prefix |
| `extract_agents <dir>` | Copies every `AGENTS.md` under the current tree into `<dir>`, preserving paths |
| `fab` | fzf-pick a Fabric pattern from `~/.config/fabric/pattern_explainations.json` (description in the preview, <kbd>ctrl-/</kbd> toggles it) and run it, feeding piped stdin if any |
| `fish_title` | Terminal title: `prompt_pwd`, or `<host (10 chars)>:<pwd>` over SSH |
| `fish_user_key_bindings` | Vi mode plus the custom binds above |
| `gdc` | `gum confirm`, then `git reset --hard HEAD` and `git clean -fd` |
| `gh-sonr-org-migrate` | Script body (see the warning below) that moves every repo from `*sonr*` GitHub orgs into `sonr-io` and deletes the emptied orgs |
| `ghg <repo>` | `ghg-clone <repo>` (ghq get), cd into the clone, `clear`, `onefetch` |
| `ghn <name>` | Runs `~/.local/bin/ghn` to create and clone a private repo, then cds into it |
| `gho` | `tv ghq-repos --inline` (repo/vault picker); returns if nothing was picked, else cds into the result, `clear`, and `onefetch` inside git repos |
| `git-summary [flags]` | `code2prompt` over the repo root, including only `AGENTS.md` files, using `~/.config/code2prompt/agents-md.hbs`. Extra flags pass through |
| `gtm [repo]` | Migrates GitHub repos to a prad.codes (Gitea) org with `tea repos migrate`. With no argument, fzf multi-selects from `gh repo list`. You pick the target org and migration options (wiki, issues, labels, pull-requests, releases, milestones, lfs, mirror) with gum, private repos stay private, and it offers to clone the results with `ghg-clone`. Fish only |
| `launch_omp_livediff_overlay` | Uses `kitty @` to overlay `omp` on the current window with a `livediff` vsplit beside it. Needs `KITTY_LISTEN_ON` |
| `lc-elir [root]` | fzf multi-select numbered LeetCode solution files (`1234.slug.js`/`.ts`) and run `omp -p "/leetcode-eli5 <file>"` on each |
| `mdcp <url>` | `curl.md <url>`, copy the result with `cpy`, and preview it with `mdfried` |
| `mdp` | fzf-pick a Markdown file up to 3 levels deep (mdfried preview) and open it in `mdfried` |
| `nvm` | nvm.fish: `install`, `use`, `list`, `list-remote`, `current`, `uninstall`. Reads `.nvmrc`/`.node-version` |
| `y` | yazi with `--cwd-file`, then cd to the directory yazi exited in |
| `yy` | Same as `y`. Used by the <kbd>ctrl-f</kbd> bind |
Functions defined inside `conf.d/` and `config.fish` rather than `functions/`: `q` (20-aliases), `terminal_title` (50-terminal-title), the `pnpmf*` helpers and `__pnpm_pick_pkg` (pnpm-filter), the `_nvm_install`/`_nvm_update`/`_nvm_uninstall` event handlers (nvm), and `starship_transient_prompt_func`, `starship_transient_rprompt_func`, and `_atuin_ai_question_mark` (config.fish).
> [!WARNING]
> **gh-sonr-org-migrate**
>
> `functions/gh-sonr-org-migrate.fish` is a script, not a function definition. Running `gh-sonr-org-migrate` in fish autoloads the file, which runs the whole fish migration flow. Because the file defines no function of that name, fish then falls through to the bash port at `~/.local/bin/gh-sonr-org-migrate` and starts a second run. Cancel the second run at its first `gum` prompt, or call the bash script by its full path.
## auto-Niri.fish
`auto-Niri.fish` ships in the shared fish submodule and starts niri from tty1. This repo does not run niri, so `.chezmoiignore.tmpl` lists `.config/fish/auto-Niri.fish` and it is never applied to `$HOME`. It is not in `conf.d/`, so fish would not load it automatically either way.
+460
View File
@@ -0,0 +1,460 @@
# Scripts
The shell-agnostic executables in ~/.local/bin — usage, behavior, and dependencies for each.
`dot_local/bin/` holds standalone `sh`, `bash`, and `python3` executables. The `executable_` prefix is a chezmoi attribute that sets the executable bit and is dropped from the target name, so `dot_local/bin/executable_pj` becomes `~/.local/bin/pj`. bash, zsh, and fish all put `~/.local/bin` on `PATH`, so every shell runs the same implementation.
This page covers the general-purpose scripts. The Umbriel scripts in the same directory (`umbriel-launch`, `umbriel-smart-move`, `umbriel-float-square`) are documented on the [Umbriel](../desktop/umbriel.md) page.
## Why some helpers are shell functions
A script runs as a child process, and a child cannot change its parent shell's working directory or read the parent's in-memory history. Helpers that need to do either are therefore defined as shell functions, ported three times: `fish/functions/*.fish`, `bash/functions/*.bash`, and `zsh/functions/*.zsh`.
| Helper | Why it must be a function |
| --- | --- |
| `yy` (and fish's `y`) | cds into the directory yazi exited in |
| `c` | cds when given an argument |
| `cd` | Replaces the `cd` builtin with a zoxide fallback |
| `ghg`, `ghn`, `gho` | cd into a repo that was picked or cloned |
| `clip-last` | Reads the shell's own history (`$history[1]` in fish, `fc` in bash/zsh) |
For `ghg` and `ghn`, the work lives in a script that prints a directory on stdout (and everything else on stderr). The shell function captures that path and cds into it. `ghg`'s script is named `ghg-clone` because an unrelated cargo binary already uses the name `ghg`. `gho` has no script: the function reads the path from `tv ghq-repos` ([television](../tools/television.md#ghq-repos)).
Several scripts below also exist as fish functions of the same name (`ai`, `c2p`, `extract_agents`, `fab`, `gdc`, `git-summary`, `lc-elir`, `mdcp`, `mdp`, `launch_omp_livediff_overlay`, `gh-sonr-org-migrate`). In fish the function runs instead of the script. Both versions behave the same. See [fish functions](fish.md#functions).
## Overview
| Script | Language | One line |
| --- | --- | --- |
| [`ai`](#ai) | sh | Pick an AI CLI with gum and launch it |
| [`atuin-pick`](#atuin-pick) | python3 | atuin history picker for a kitty or tmux split |
| [`brave-new-tab`](#brave-new-tab) | sh | Open a URL in a new Brave Origin tab |
| [`c2p`](#c2p) | sh | code2prompt with an fzf-picked template |
| [`extract_agents`](#extract_agents) | sh | Copy all `AGENTS.md` files into a directory tree |
| [`fab`](#fab) | sh | fzf picker for Fabric patterns |
| [`gdc`](#gdc) | sh | Discard all git changes after confirmation |
| [`gh-sonr-org-migrate`](#gh-sonr-org-migrate) | bash | Consolidate `*sonr*` GitHub orgs into `sonr-io` |
| [`ghg-clone`](#ghg-clone) | sh | `ghq get` and print the clone path |
| [`ghn`](#ghn) | sh | Create a private repo, clone it, print the path |
| [`git-summary`](#git-summary) | sh | Aggregate a repo's `AGENTS.md` files with code2prompt |
| [`gop`](#gop) | sh | Open the current repo's web page |
| [`kitty-day-save`](#kitty-day-save) | bash | Save kitty's daily session right before shutdown/reboot |
| [`launch_omp_livediff_overlay`](#launch_omp_livediff_overlay) | sh | omp overlay plus a livediff split in kitty |
| [`lc-elir`](#lc-elir) | sh | Run `/leetcode-eli5` via omp on picked solutions |
| [`mandible-pick`](#mandible-pick) | bash | fzf-pick a command, then open it in mandible |
| [`mdcp`](#mdcp) | sh | Fetch a URL as Markdown, copy it, preview it |
| [`mdp`](#mdp) | sh | fzf Markdown picker with mdfried preview |
| [`mtime-sort`](#mtime-sort) | sh + perl | Sort path lines newest-first by mtime |
| [`omp-session`](#omp-session) | python3 | Resume or start omp for the current repo; backend of the `omp-sessions` cable |
| [`pj`](#pj) | bash | Task runner across every project task source, on a tv cable |
| [`projects-sync`](#projects-sync) | bash | Symlink every `~/Code` repo into `~/Projects` |
| [`pj-palette`](#pj-palette) | bash | pj's tasks plus quick actions as a tmux-palette source |
| [`tmux-sessions`](#tmux-sessions) | bash | Session/window/zoxide switcher as a tmux-palette source |
| [`yazi-nvim-open`](#yazi-nvim-open) | sh | yazi opener that targets the host Neovim |
## ai
```sh
ai
```
Shows `gum choose --header 'AI CLI'` with `claude`, `opencode`, `codex`, `hermes`, and `fractal`, then `exec`s the choice. `claude` runs as `claude --dangerously-skip-permissions`; the others run as the bare binary name. Esc or Ctrl-C exits 0.
**Needs:** `gum` and the chosen CLI.
## atuin-pick
```sh
atuin-pick
```
Opens atuin's interactive search (`atuin search -i --filter-mode directory`), limited to history from the current directory. It then `exec`s an interactive `fish`. If you picked a command, the new fish runs `echo '❯ <cmd>'; <cmd>` via `-C` (`--init-command`), so aliases resolve, a `cd` persists, and the prompt appears below the output. Esc starts a plain fish.
It is written for launch contexts where atuin's shell init never ran. It runs `atuin` from `PATH` and seeds `ATUIN_SESSION` from `atuin uuid` if unset.
**Launched by:** <kbd>ctrl+shift+p</kbd> in kitty. `kitty.conf` opens it in a horizontal split (`launch_hs atuin-pick`). Under the kitty-tmux profile, `tmux.conf` runs it with `tmux split-window -v -l 25%` in the current pane's directory. See [kitty](../terminal/kitty.md) and [atuin](atuin.md).
**Needs:** `python3`, `atuin`, `fish`.
## brave-new-tab
```sh
brave-new-tab <url>
```
Runs `brave-origin --new-tab <url>` detached with `setsid -f`. With Brave Origin running, the URL opens as a tab in its last active window; otherwise Brave Origin starts with it. Exits 2 unless given exactly one argument.
A URL without a scheme gets one from its host (port and path ignored): `localhost`, `*.localhost`, `*.local`, and `127.*` get `http://`, everything else `https://`. URLs containing `://`, and `about:` URLs, pass through unchanged.
| Argument | Opens |
| --- | --- |
| `example.com/path` | `https://example.com/path` |
| `localhost:3000` | `http://localhost:3000` |
| `printer.local` | `http://printer.local` |
| `http://example.com` | `http://example.com` |
**Needs:** `brave-origin` (`brave-origin-bin`), `setsid` (util-linux).
## c2p
```sh
c2p
```
Lists `*.hbs` files under `~/.config/code2prompt` with `fd`, lets you pick one with fzf, and `exec`s `code2prompt --template <file>`.
**Needs:** `fd`, `fzf`, `code2prompt`.
## extract_agents
```sh
extract_agents <output-dir>
```
Creates `<output-dir>` and copies every `AGENTS.md` below the current directory into it with `cp --parents`, keeping the relative paths.
**Needs:** `fd`, GNU `cp` (`--parents`).
## fab
```sh
fab
some-command | fab
```
Reads `~/.config/fabric/pattern_explainations.json`. fzf lists pattern names, the preview pane (right, 60%, wrapped) shows each description, and <kbd>ctrl-/</kbd> toggles the preview. The picked pattern runs as `fabric-ai --pattern <name>`, falling back to `fabric` if `fabric-ai` is not installed. Piped stdin is buffered to a temp file and fed to the pattern; the temp file is removed on exit.
**Needs:** `jq`, `fzf`, `fabric-ai` or `fabric`, and the pattern JSON.
## gdc
```sh
gdc
```
Asks `gum confirm "Discard all uncommitted changes? This is irreversible."`. On yes, runs `git reset --hard HEAD` and `git clean -fd`.
**Needs:** `gum`, `git`.
## gh-sonr-org-migrate
```sh
gh-sonr-org-migrate
```
A bash port of `fish/functions/gh-sonr-org-migrate.fish`, with the same flow:
1. **Preflight**: Checks that `gh`, `gum`, and `jq` exist and that `gh auth status` succeeds.
2. **Discover**: Lists your GitHub orgs whose login contains `sonr`, excluding the target `sonr-io`. You pick which to migrate with `gum choose --no-limit`.
3. **Transfer**: For each repo in each org, runs `gh api -X POST repos/<org>/<repo>/transfer -f new_owner=sonr-io`. If the name already exists in `sonr-io`, `gum input` asks for a new name (default `<org>-<repo>`). An empty name skips the repo.
4. **Settle**: Waits 30 seconds (`gum spin`), because GitHub transfers are asynchronous.
5. **Delete**: For every selected org that had no failed transfers and now has zero repos, asks `gum confirm` and then `gh api -X DELETE orgs/<org>`.
**Needs:** `gh` authenticated as owner of every org involved, `gum`, `jq`.
> [!WARNING]
> In fish, run this script by its full path (`~/.local/bin/gh-sonr-org-migrate`). The fish file of the same name is a script body with no function in it, so typing just the name runs the fish flow and then this script. See [fish](fish.md#functions).
## ghg-clone
```sh
ghg-clone <repo>
```
Runs `ghq get <repo>`, with its output sent to stderr. It then works out the on-disk path: it strips the scheme, any `user@`, the scp-style colon, and a trailing `.git`, and prefixes `github.com/` when the input has no host. It checks that `$(ghq root)/<host>/<user>/<repo>` exists, runs `zoxide add` on it if zoxide is installed, and prints the path.
Accepts `https://…`, `ssh://…`, `git@host:user/repo`, `host/user/repo`, or `user/repo`.
**Mirrors.** After the clone, it sets up a [prad.codes](../tools/git.md) mirror when the repo has none. All of it talks on stderr and never fails the clone:
| Cloned from | Mirror | Skipped when |
| --- | --- | --- |
| `github.com/<owner>/<name>` | Pull mirror into a prad.codes org, every 8h (`tea repos migrate --mirror --mirror-interval 8h`, authenticated with `gh auth token`, private if the GitHub repo is). `gum choose` picks the org, preselected from GitHub's primary language: Go → `go`, Python → `py`, Rust → `rs`, Dart → `dart`, TeX → `tex`, JS/TS/Astro/Svelte/Vue/HTML/CSS → `js`, Markdown/MDX → `md`, anything else → `env`. Esc skips | You are not an admin of the GitHub repo, or a prad.codes repo already pulls from it (`original_url`) |
| `prad.codes/<org>/<name>` | Push mirror to `github.com/<you>/<name>` on every push and every 8h, with a first sync right away. After a `gum confirm`, it creates the GitHub repo (same visibility) if missing; an existing one is overwritten by the mirror pushes, which the prompt says | The repo is itself a pull mirror, or already has a push mirror |
Both mirrors store your `gh` OAuth token in prad.codes as the remote credential. Without a terminal (no `gum` prompt), nothing is mirrored.
**Used by:** the `ghg` shell function, which cds into the path, clears the screen, and runs `onefetch`. The fish function `gtm` also uses it to clone migrated repos.
**Needs:** `ghq` (`GHQ_ROOT=~/Code`, exported by fish, zsh, and bash), optional `zoxide`. Mirrors need `gh`, `tea` (logged in to prad.codes), `jq`, and `gum`; without any of them the step is skipped.
## ghn
```sh
ghn <repo-name>
```
Creates a new **private** repo and clones it:
- **prad.codes** (Gitea): pick an org from `tea organizations list`, then `tea repos create --private` and `ghq get https://prad.codes/<org>/<name>`.
- **github.com**: pick the owner `prdlk` or `wltbs`, then `gh repo create <owner>/<name> --private` and `ghq get`.
You pick the host with `gum choose`. The script prints the clone directory, and the `ghn` shell function cds into it.
**Needs:** `gum`, `ghq`, and `tea` or `gh`.
## git-summary
```sh
git-summary [code2prompt flags…]
```
From anywhere inside a git repo, runs `code2prompt <repo-root> --include AGENTS.md --include "**/AGENTS.md" --template ~/.config/code2prompt/agents-md.hbs`. Extra flags such as `--output-file` or `--tokens` are passed through.
**Needs:** `git`, `code2prompt`, and the `agents-md.hbs` template.
## gop
```sh
gop # origin, else upstream, else first remote
gop upstream # a named remote
gop owner/repo # github.com/owner/repo
gop <url> # any https/ssh/scp remote URL
```
Opens a repo's web page for any forge whose URL layout is `https://<host>/<owner>/<repo>`, such as github.com and prad.codes. It reduces any remote form to `<host>/<owner>/<repo>`: it drops credentials, drops the port on `ssh://` and `git://` remotes, keeps the port and plain `http` on `http://` remotes, and strips `.git`. It then runs `$BROWSER` (default `xdg-open`) on the result.
**Needs:** `git`, a browser or `xdg-open`.
## kitty-day-save
```sh
kitty-day-save # hold a logind delay lock, flush on shutdown, exit
kitty-day-save --now # flush every running kitty immediately
```
Run by `kitty-day-save.service` (`dot_config/systemd/user/`, `WantedBy=graphical-session.target`, `Restart=always` after 10 s). It wraps itself in `systemd-inhibit --what=shutdown --mode=delay` and waits on `gdbus monitor` for login1's `PrepareForShutdown (true,)`. logind sends that before anything is killed and waits for the lock (`InhibitDelayMaxSec`, 5 s by default), so every kitty still has all its windows. It then sends `kitten day_session.py flush` over remote control to each `@kitty-<pid>` socket in `/proc/net/unix` (3 s timeout each) and exits, which releases the lock. The kitty that owns today's session saves it and holds further saves for 60 s; other kitties ignore the flush. See [kitty daily sessions](../terminal/kitty.md#daily-sessions).
**Needs:** `systemd-inhibit`, `gdbus`, `kitten`. Linux desktop/laptop roles only (`.chezmoiignore.tmpl`).
## launch_omp_livediff_overlay
```sh
launch_omp_livediff_overlay
```
Inside kitty with remote control enabled, runs `kitty @ launch --type=overlay-main --cwd=current omp`, then opens `livediff` in a `vsplit` next to that window with `--keep-focus`. It relies on `$KITTY_LISTEN_ON` rather than a hard-coded socket, because kitty appends `-PID` to `listen_on` socket names. It exits with an error if the variable is unset.
**Needs:** kitty with `allow_remote_control`/`listen_on` ([kitty](../terminal/kitty.md)), `omp` ([AI agents](../ai/README.md)), `livediff`.
## lc-elir
```sh
lc-elir [root]
```
Finds numbered LeetCode solution files (`^\d+\..+\.(js|ts)$`, for example `1365.some-slug.js`) under `root` (default `.`). fzf multi-selects them, showing the last three path segments with a `bat` preview of the first 40 lines. Each pick runs `omp -p "/leetcode-eli5 <file>"`.
**Needs:** `fd`, `fzf`, `bat`, `omp`, and the `/leetcode-eli5` omp command.
## mandible-pick
```sh
mandible-pick
```
`mandible` (installed with cargo, `~/.cargo/bin`) needs a tool to open, so this asks for one first. fzf lists the executables installed in the `PATH` directories, without shell builtins, keywords, or aliases. <kbd>enter</kbd> opens the highlighted command. If the query contains a space (`git remote add`) or matches nothing, <kbd>enter</kbd> uses it exactly as typed, so mandible opens at that subcommand path. Then it `exec`s `mandible <tool> [subcommand…]`. <kbd>esc</kbd> exits quietly.
**Used by:** the tmux-palette **mandible** item (alias `man`), which runs it in a popup. See [tmux](../terminal/tmux.md#command-palette).
**Needs:** `mandible`, `fzf` 0.51+ (`--with-shell`, `transform`, `accept-or-print-query`).
## mdcp
```sh
mdcp <url>
```
Fetches the URL as Markdown with `curl.md`, copies it with `wl-copy`, then pastes it back into `mdfried` for a rendered preview.
**Needs:** `curl.md`, `mdfried`, and `wl-clipboard`.
## mdp
```sh
mdp
```
Lists `*.md` files up to 3 directories deep with `fd`. fzf shows an `mdfried` preview (right, 60%, wrapped), and the pick opens in `mdfried`.
**Needs:** `fd`, `fzf`, `mdfried`.
## projects-sync
```sh
projects-sync
```
Mirrors every repo under the ghq root (`$GHQ_ROOT`, default `~/Code`, laid out `<host>/<owner>/<repo>`) as `~/Projects/<repo>` → the repo. Repos with the same name under different owners become `<owner>-<repo>`. Links pointing into `~/Code` whose repo is gone are removed; real files, real directories, and links pointing elsewhere in `~/Projects` are never touched.
Run by `projects-sync.timer` (`dot_config/systemd/user/`, 30 s after login and every 5 minutes), which [`30-user-services`](../chezmoi/scripts.md#30-user-services) enables. ghq clones land two levels below `~/Code`, out of reach of a non-recursive `.path` unit, hence the poll.
**Needs:** GNU `find`. Linux desktop/laptop roles only (`.chezmoiignore.tmpl`).
## mtime-sort
```sh
… | mtime-sort [--repo]
```
Reads lines on stdin and prints them newest-first by the mtime of each line's last tab-separated field (the whole line when it has no tab). With `--repo`, it stats `<path>/.git/index` first, so a repo sorts by its last git activity. Lines whose path does not exist are dropped. Ties keep input order.
**Used by:** every custom [television](../tools/television.md) cable except `agents` and `omp-sessions`, and `pj --list`. tv layers match quality and frecency on top of this order.
**Needs:** `perl`.
## omp-session
```sh
omp-session
```
Starts or resumes omp for the current git repo by fixed rules, so <kbd>ctrl-a</kbd> never asks when the answer is obvious.
| HEAD | Does |
| --- | --- |
| Default branch | Resumes the newest session on it when that session was written less than 3 hours ago. Otherwise opens the `omp-sessions` [television](../tools/television.md#omp-sessions) cable over all of the repo's sessions; <kbd>enter</kbd> resumes the pick, <kbd>esc</kbd> returns to the prompt. With no sessions at all it starts a new one |
| Any other branch | Resumes the newest session on that branch, or starts a new one |
| Detached | Opens the `omp-sessions` cable (a new session when there are none) |
| Outside a git repo | Plain `omp` |
Resuming runs `omp --resume <session file>`; omp switches to the session's own cwd. `omp-session --new` runs plain `omp`; it is the cable's <kbd>ctrl-n</kbd> action and marks `OMP_SESSION_STATE` like a resume does, so the prompt is not moved after a new session.
**Sessions.** Every `*/*.jsonl` under `$PI_CODING_AGENT_DIR/sessions` (default `~/.omp/agent/sessions`) whose `session` header `cwd` is the worktree root or below it, excluding worktrees nested inside it. Session age is the file's mtime (the last write).
**Branches.** omp does not record the git branch. A session's branch is the one HEAD was on when the file was last written, replayed from the worktree's `git reflog` (`checkout: moving from A to B`, `Branch: renamed`, `rebase (finish): returning to`). Sessions older than the oldest reflog move belong to the branch that move left; with no moves they all belong to the current branch. The default branch is `origin/HEAD`, else `init.defaultBranch`, `main`, or `master`, whichever exists.
**Entry points.** The cable calls back into the script:
| Command | Does |
| --- | --- |
| `omp-session --list` | Prints `<age> <branch> <title>[ · <subdir>]<TAB><session file>` per session, newest first. The title is omp's session title, else the first user message |
| `omp-session --preview FILE` | Title, last-write time, cwd, then the text of every user and assistant message (thinking, tool calls, and tool results skipped) |
| `omp-session --resume FILE` | Writes `ran` to `$OMP_SESSION_STATE` when set, then `exec omp --resume FILE` (enter) |
**Prompt redraw.** The launcher runs `tv omp-sessions --inline` with `OMP_SESSION_STATE` set to a temp file. When the picker is cancelled (no `ran`), it prints `\033[A` to `/dev/tty` so the shell redraws the prompt in place; the key bindings just repaint.
**Launched by:** <kbd>ctrl-a</kbd> in fish and zsh. <kbd>ctrl-n</kbd> at the prompt runs `omp` directly, not this script.
**Needs:** `python3`, `git`, `omp`, `tv`.
## pj
```sh
pj [extra args…]
```
A task runner that searches every task source in the project and lets you pick one in the `pj` [television](../tools/television.md#pj) cable.
**Sources.** Each is listed only when its runner is installed:
| Source | Found in | Runner |
| --- | --- | --- |
| npm scripts | `package.json` | package manager from `packageManager`, else from the lockfile (`bun.lock[b]`, `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, searched upward), else `npm`, then `<pm> run` |
| Make | `GNUmakefile`, `makefile`, `Makefile` | `make`. Targets come from a text parse of rules and `.PHONY`; `make -p` is avoided because it would run `$(shell …)` |
| just | `justfile`, `.justfile`, `Justfile`, `JUSTFILE` | `just` |
| mise | `mise.toml`, `.mise.toml`, `mise/config.toml`, `.mise/config.toml`, `.config/mise/config.toml`, or a `mise-tasks`/`.mise/tasks`/`mise/tasks` dir | `mise run`. Hidden tasks are skipped, and tasks inherited from parent directories are filtered out |
| devbox | `devbox.json` `shell.scripts` | `devbox run` |
| poe | `pyproject.toml` `[tool.poe.tasks]` | `poe`, else `uv run poe` |
| pdm | `[tool.pdm.scripts]` (skips `_*` and `post_*`) | `pdm run` |
| rye | `[tool.rye.scripts]` | `rye run` |
| taskipy | `[tool.taskipy.tasks]` | `task`, else `uv run task` |
| hatch | `[tool.hatch.envs.<env>.scripts]` as `env:name` | `hatch run` |
| console scripts | `[tool.poetry.scripts]`, `[project.scripts]` | `poetry run` for Poetry projects, else `uv run` |
**Scanning.** Walks from `$PWD` up to the git toplevel, inclusive. Outside a repo it stops at the first directory that produced tasks.
**Picker.** `pj` writes the records to a temp file and runs `tv pj --inline --source-command "cat <tmp>" --input-header <project root>`. Rows are tagged with the tool (`npm`, `pnpm`, `bun`, `make`, `just`, `mise`, `poe`, `py`, …), so typing a tool name filters to it. A directory column appears when more than one project contributed tasks. Tasks are sorted by their manifest's mtime, newest first, through `mtime-sort`. The preview shows the task body: the npm script string, the Make recipe, `just --show`, `mise tasks info`, the devbox script, or the pyproject entry.
**Entry points.** The cable calls back into the script:
| Command | Does |
| --- | --- |
| `pj --list` | Prints tab-separated records: display, kind, target, root, runner, manifest |
| `pj --preview KIND TARGET ROOT FILE` | Prints the task body for the preview pane |
| `pj --run KIND TARGET ROOT RUNNER` | Runs the task (enter) |
**Run.** cds into the task's directory and `exec`s the runner. Extra arguments given to `pj` travel to the task through `$PJ_EXTRA` (shell-quoted), with `--` inserted for `npm run`, `mise`, and `devbox`. Tasks named `dev`, `start`, `serve`, `watch`, or namespaced variants like `dev:web` are wrapped in `openlogs` (found on `PATH`, via `mise which`, or in mise's node installs) for a live log tail. Without openlogs they run unwrapped, with a note.
**Env.** `PJ_QUIET=1` suppresses the "no task sources found" error (exit status stays 1). The <kbd>down</kbd> bindings use it. `PJ_STATE=FILE` makes `pj` write `open` to `FILE` before launching the picker, and `pj --run` writes `ran`; the file stays empty when there are no task sources. The key bindings read it to tell no tasks, a cancelled picker, and a task that printed output apart and redraw the prompt to match. `pj` itself does no cursor handling, so typing `pj` behaves like any command.
**Launched by:** <kbd>ctrl-p</kbd>, and <kbd>down</kbd> on an empty prompt, in fish, zsh, and bash.
**Needs:** `bash`, `tv`, `mtime-sort`, `jq`, `python3` 3.11+ (for `tomllib`), plus whichever runners the project uses.
## pj-palette
```sh
pj-palette
pj-palette --run KIND TARGET ROOT RUNNER
pj-palette --cmd ROOT CMD…
pj-palette --bg NAME [ROOT CMD…]
pj-palette --omp
```
The source of the **Project Tasks** [tmux command palette](../terminal/tmux.md#command-palette) (`palettes/tasks.json`). Open it with tmux `prefix t`, Ghostty <kbd>ctrl+shift+p</kbd> or <kbd>ctrl+shift+space</kbd>, or the "Project Tasks" item in the main palette.
With no arguments it reads the active pane's directory (`tmux display -p '#{pane_current_path}'`) and prints palette items as JSON, grouped by category:
| Group | Items | Shown when |
| --- | --- | --- |
| Running | Every background task session still alive (any project), by session name | A long-running item was started and not stopped |
| Quick | `<pm> install` (alias `install`, `i`), `<pm> run dev` (`dev`) | `package.json` at the project root. The package manager comes from `packageManager`, then the lockfile, as in `pj` |
| Quick | `uv sync` (`sync`), `uv run python` (`py`, `repl`) | `pyproject.toml` and `uv` installed |
| Quick | `make` (default target; `m`, `build`) | A Makefile and `make` |
| Quick | `go run .`, `cargo run` | `go.mod` / `Cargo.toml` and the toolchain |
| Quick | `docker compose up` (`up`, `compose`) | `compose.y(a)ml` or `docker-compose.y(a)ml` at the project root and `docker` |
| Tasks: `<tool>` | Every record from `pj --list`: npm/bun/pnpm/yarn scripts, Make, just, mise, devbox, pyproject task tables and console scripts | Whatever `pj` finds walking up to the git root |
The project root is the git toplevel, else the pane's directory.
**Run.** One-off items open a full-width split along the bottom of the window (`split-window -v -f -l 25%`), 25% of its height, in the task's directory. The command is passed to `split-window` as separate arguments, so no shell reparses it. `--run` calls `pj --run`, which keeps pj's `openlogs` wrapping of `dev`/`start`/`serve`/`watch` tasks, and `--cmd` runs a quick action. While the command runs, <kbd>ctrl-c</kbd> stops only the command (the script traps `SIGINT`), so the output stays. Once the command exits, the script prints `[exit N] enter or ctrl-c to close`, and <kbd>enter</kbd> or <kbd>ctrl-c</kbd> closes the pane; other keys are ignored.
**Background.** Long-running items, marked `· background`, are `<pm> run dev`, `docker compose up`, and pj tasks named `dev`/`start`/`serve`/`watch`/`preview` or with one of those as a `:` segment (`dev:web`, `api:dev`). They run through `--bg` in a detached session named `<session>:<tool-and-command>` (`leetcode:bun-run-dev`, `leetcode:docker-compose-up`), shown in a rounded 90%×80% popup attached to it. <kbd>q</kbd> detaches the popup and leaves the task running: the session's `key-table` is `pj-task`, which binds only `q` and <kbd>ctrl-c</kbd>, so other keys reach the task instead of root-table bindings (prefix bindings still work). <kbd>ctrl-c</kbd> runs `confirm-before` (`Stop <session>? (y/n)`); <kbd>y</kbd> sends ctrl-c to the task, which stops it and ends the session, closing the popup, and any other key cancels. If the task exits on its own, the `[exit N]` prompt keeps its output until <kbd>enter</kbd>. Picking the item again, or its **Running** entry, reopens the popup instead of starting a second copy. The session has `status off`, `detach-on-destroy on`, and `@pj_task` set to the task's root, which is how the Running group finds it. Session names contain `:`, so the script targets them by id (`$N`).
**omp.** tmux `prefix a` (Ghostty <kbd>ctrl+shift+a</kbd>) runs `pj-palette --bg <session>:omp <pane cwd> omp`, so omp gets the same kind of background session and Running entry. Its popup docks in the bottom-right corner of the client, 35% wide and 60% tall. omp runs directly (no `[exit N]` prompt) and needs <kbd>q</kbd>, so its session uses the `pj-app` key table instead: <kbd>ctrl-c</kbd> hides the popup, <kbd>ctrl-q</kbd> kills the session (quits omp). `prefix a` inside any session with `@pj_task` also hides, which makes <kbd>ctrl+shift+a</kbd> a toggle. The popup border shows only the session name.
**omp palette.** `--omp` feeds `palettes/omp.json`, opened by the main palette's **omp** item. It lists every running `<session>:omp` across tmux sessions, with a green icon and `open`/`hidden` state: **This session** first (or a grey `not running · start in <cwd>` item that starts it), then **Running** for the other tmux sessions' agents, then a **Quit** item per session (`kill-session`).
**Needs:** `pj` and its dependencies, `jq`, tmux.
## tmux-sessions
```sh
tmux-sessions
tmux-sessions --new QUERY
tmux-sessions --dir DIR
```
The source of the **Sessions** [tmux command palette](../terminal/tmux.md#command-palette) (`palettes/sessions.json`), which replaced tmux-sessionx. Open it with tmux `prefix o`, Ghostty <kbd>ctrl+shift+o</kbd> or <kbd>super+enter</kbd>, or the "Sessions" item in the main palette.
With no arguments it prints palette items as JSON, grouped by category:
| Group | Items | Action |
| --- | --- | --- |
| Sessions | Every session but the current one, the client's last session first. Attached ones have a green icon | `switch-client` |
| Windows | Every window of every session, as `<session>:<index> · <cwd>` | `switch-client` to that window |
| Zoxide | `zoxide query -l` | `--dir` |
| Config | Directories in `~/.config` that zoxide does not list | `--dir` |
| Rename | One per session | `command-prompt` prefilled with the name |
| Kill | One per session except the current one | `kill-session` |
pj-palette's background task sessions (`@pj_task`) are left out. Sessions are targeted by id, since names may contain `:`.
**`--new QUERY`** is the palette's `query` item, the typed text listed after the matches. It switches to the session named QUERY if there is one. Otherwise it starts a session named QUERY in QUERY itself if that is a directory (via `--dir`), else in `zoxide query QUERY`'s match (QUERY's words are separate keywords), else in tmux's default directory. Typed text that matches nothing therefore lands on the `query` item, the same as sessionx's zoxide mode.
**`--dir DIR`** switches to, or starts in DIR, the session named after DIR's basename with dots removed (sessionx's rule).
**Needs:** tmux, `jq`, `zoxide`.
## yazi-nvim-open
```sh
yazi-nvim-open [vsplit|split|tabedit] <file>…
```
yazi's `edit` opener. When run inside a Neovim terminal (`$NVIM` set), each file's absolute path goes to the **host** Neovim through `nvim --server "$NVIM" --remote-expr 'v:lua.EditFromYazi(mode, path)'` (defined in the nvim config's `yazi_session.lua`). Outside Neovim it `exec`s a normal `nvim`, and the split modes do nothing. Set `YAZI_OPEN_TRACE=<file>` to log each call.
**Used by:** `yazi.toml` (the `edit` opener) and `keymap.toml` (<kbd>ctrl-v</kbd> vsplit, <kbd>ctrl-x</kbd> split). See [yazi](../tools/yazi.md).
**Needs:** `nvim`, `realpath`.
+91
View File
@@ -0,0 +1,91 @@
# Starship
The cross-shell prompt — a minimal left prompt, a clock on the right, and a cyberdream palette.
[Starship](https://starship.rs) renders the prompt in fish, bash, and zsh from one config file.
| Source (repo) | Target |
| --- | --- |
| `dot_config/starship.toml` | `~/.config/starship.toml` |
The file is not a template and applies on every role. `starship` is in `.chezmoidata/packages.yaml` for pacman and is installed by `.chezmoiscripts/run_onchange_before_10-packages.sh.tmpl`.
## Shell integration
| Shell | Init |
| --- | --- |
| fish | `conf.d/40-tools.fish` runs `starship init fish`, then `config.fish` runs it again and calls `enable_transience` |
| bash | `eval "$(starship init bash)"` in `~/.bashrc` |
| zsh | `eval "$(starship init zsh)"` in `~/.zshrc`, plus a transient-prompt hook |
With transience in fish, past prompts in the scrollback collapse to the output of `starship_transient_prompt_func` (`starship module character`, the `❯` alone) and `starship_transient_rprompt_func` (`starship module time`). See [fish](fish.md#configfish).
zsh has no built-in transience, so `~/.zshrc` collapses past prompts itself, rendering the `transient` (`$character`) and `transient_right` (`$time`) entries under `[profiles]` with `starship prompt --profile`. It uses profiles rather than `starship module` because only `starship prompt` wraps color escapes in zsh's `%{…%}`; without them zsh miscounts the right prompt's width. zsh also gets starship's default `∙` continuation prompt (`PROMPT2`). See [bash and zsh](bash-zsh.md#zshrc).
## Layout
**~/.config/starship.toml**
```toml
"$schema" = 'https://starship.rs/config-schema.json'
palette = "cyberdream"
format = """
$username\
$hostname\
$directory\
$git_branch\
$git_state\
$git_status\
$cmd_duration\
$python\
$character"""
right_format = """$time"""
```
The left prompt is a single line with no line break before `$character`. Only the nine listed modules can appear; every other starship module is left out of `format`. `$username` and `$hostname` keep their defaults, so they appear only over SSH or as root.
## Modules
| Module | Format | Style | Notes |
| --- | --- | --- | --- |
| `directory` | default | `blue` | |
| `git_branch` | `[$branch]($style)` | `bright-black` | Branch name only, no symbol or "on" |
| `git_state` | `\([$state( $progress_current/$progress_total)]($style)\) ` | `bright-black` | For example `(REBASING 2/5)` |
| `git_status` | `[[(*$conflicted$untracked$modified$staged$renamed$deleted)](218) ($ahead_behind$stashed)]($style)` | `cyan` | See below |
| `cmd_duration` | `[$duration]($style) ` | `yellow` | Default threshold |
| `python` | `[$virtualenv]($style) ` | `bright-black` | `detect_extensions = []`, `detect_files = []` |
| `time` | `[$time]($style)` | `bright-black` | `disabled = false`, `time_format = '%-I:%M %p'` (for example `3:07 PM`) |
| `character` | default | | Symbols below |
**git_status.** The `conflicted`, `untracked`, `modified`, `staged`, `renamed`, and `deleted` symbols are all set to a zero-width space. Any dirty state therefore shows a single `*` in 256-color `218` (pink) instead of per-state counts. `stashed` is `≡`. `ahead_behind` keeps starship's default arrows and is drawn in `cyan`.
**python.** With both detect lists empty, the module does not trigger on `.py` files or `pyproject.toml`. It shows only the name of an active virtualenv.
### Prompt character
| State | Symbol |
| --- | --- |
| Last command succeeded | `❯` in `purple` |
| Last command failed | `❯` in `red` |
| Vi normal mode (fish) | `❮` in `green` |
fish runs in vi mode ([key bindings](fish.md#key-bindings)), so `vimcmd_symbol` appears there. zsh uses the emacs keymap and never shows it.
## Palette
`palette = "cyberdream"` selects the `[palettes.cyberdream]` table, colors from [cyberdream.nvim](https://github.com/scottmckendry/cyberdream.nvim). Every named color in the module styles resolves through this table.
| Name | Normal | `bright-*` | `dim-*` |
| --- | --- | --- | --- |
| black | `#1e2124` | `#4a4d4f` | `#1c1f22` |
| red | `#ff6e5e` | `#ff8b7e` | `#d05c50` |
| green | `#5eff6c` | `#7eff89` | `#4fd05b` |
| yellow | `#f1ff5e` | `#f3ff7e` | `#c5d050` |
| blue | `#5ea1ff` | `#7eb3ff` | `#4f85d1` |
| purple | `#bd5eff` | `#ff7ef3` | `#d050c6` |
| cyan | `#5ef1ff` | `#7ef3ff` | `#4fc5d1` |
| white | `#ffffff` | `#ffffff` | `#d0d0d1` |
`bright-black` (`#4a4d4f`) is the muted gray used for the branch, git state, virtualenv, and clock. The `dim-*` entries are defined but no module uses them.