docs: add comprehensive documentation for AI agents, chezmoi workflow, shell setup, desktop configuration, terminal multiplexers, and developer tools

This commit is contained in:
Prad Nukala committed 2026-09-28 11:08:17 -04:00
1 parent 41437d4abd
commit 1da988c876
47 files changed
+5485

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
---
title: Aliases
description: 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)), and `dot_config/shell/aliases.sh.tmpl` ranges over it to produce `~/.config/shell/aliases.sh`, which `~/.bashrc` and `~/.zshrc` source:
```yaml .chezmoidata/aliases.yaml
aliases:
- section: "Editors / file managers"
items:
- { name: "e", cmd: "nvim" }
# …
```
```text ~/.config/shell/aliases.sh
# 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` |
| `e.k` | `yazi ~/.config/kitty` | `yazi ~/.local/share/chezmoi/dot_config/kitty` |
| `e.n` | `yazi ~/.config/niri` | `yazi ~/.local/share/chezmoi/dot_config/niri` |
| `e.v` | `yazi ~/.config/nvim/lua` | ✓ |
| `e.y` | `yazi ~/.config/yazi` | `yazi ~/.local/share/chezmoi/dot_config/yazi` |
| `e.t` | `nvim ~/.config/tmux/tmux.conf` | `nvim ~/.local/share/chezmoi/dot_config/tmux/tmux.conf` |
| `hx` | `helix` | ✓ (listed under Tools in fish) |
The fish editor 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.
### 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, else `pbcopy` |
| `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](/shell/fish#functions), [bash and zsh](/shell/bash-zsh#per-shell-functions)).
- `pnpmf`, `pnpmfd`, `pnpmfr`, `pnpmfw`, `pnpmfx`: fish workspace helpers ([fish](/shell/fish#pnpm-filterfish)).
- `ai`, `pj`, `gop`, `fab`, `gdc`, and the other `~/.local/bin` tools: [Scripts](/shell/scripts).
+144
View File
@@ -0,0 +1,144 @@
---
title: atuin
description: Shell history search with a daemon, directory-scoped up-arrow, tmux popups, a cyberdream 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/cyberdream.toml` | `~/.config/atuin/themes/cyberdream.toml` | symlink |
| `dot_config/atuin/atuin-receipt.json` | `~/.config/atuin/atuin-receipt.json` | symlink |
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](/shell/fish#functions)). None of the files are templates, and they apply on every role.
## Installation
`atuin` is listed in `.chezmoidata/packages.yaml` for pacman and Homebrew. The shells also expect atuin's standalone installer layout in `~/.atuin/bin`:
- fish: `conf.d/atuin.env.fish` sources `~/.atuin/bin/env.fish`.
- bash and zsh: `. "$HOME/.atuin/bin/env"` just before `atuin init`.
Neither line checks that the file exists, so a machine without `~/.atuin/bin` prints a sourcing error at shell start.
### atuin-receipt.json
The receipt that atuin's [cargo-dist](https://opensource.axo.dev/cargo-dist/) installer writes. It records how atuin was installed, which the self-updater (`atuin update`) needs:
| Field | Value |
| --- | --- |
| `binaries` | `["atuin"]` |
| `install_prefix` | `~/.atuin/bin` (stored as an absolute path under `/home/prad`) |
| `install_layout` | `flat` |
| `modify_path` | `true` |
| `provider` | cargo-dist `0.31.0` |
| `source` | GitHub release `atuinsh/atuin` |
| `version` | `18.19.0` |
## Shell integration
| Shell | Where | What |
| --- | --- | --- |
| fish | `config.fish`, interactive only | `atuin init fish \| source`, `atuin ai init fish \| source`, and the `?` binding |
| 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`](/shell/scripts#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` and the following to `config.fish`:
1. `atuin ai init fish | source`, in the interactive block next to `atuin init fish`.
2. A copy of the `_atuin_ai_question_mark` function that `atuin ai init fish` prints, and `bind "?" _atuin_ai_question_mark`.
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 do not load `atuin ai init`.
## 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` | `"cyberdream"` | Loads `themes/cyberdream.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/cyberdream.toml` defines a theme named `cyberdream`, using the palette shared with [fish](/shell/fish#theme) and [Starship](/shell/starship#palette):
| Key | Color |
| --- | --- |
| `AlertInfo` | `#5ef1ff` |
| `AlertWarn` | `#f1ff5e` |
| `AlertError` | `#ff6e5e` |
| `Annotation` | `#7b8496` |
| `Guidance` | `#5ea1ff` |
| `Important` | `#ff5ef1` |
| `Title` | `#5ef1ff` |
| `Muted` | `#7b8496` |
| `SyntaxCommand` | `#5ea1ff` |
| `SyntaxFlag` | `#5ef1ff` |
| `SyntaxString` | `#5eff6c` |
| `SyntaxVariable` | `#ff5ef1` |
| `SyntaxOperator` | `#ff5ea0` |
| `SyntaxComment` | `#7b8496` |
+124
View File
@@ -0,0 +1,124 @@
---
title: bash and zsh
description: 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, and ports of the fish helpers that have to change the shell's own directory. zsh also gets ports of the fish key bindings.
| Source (repo) | Target |
| --- | --- |
| `dot_bashrc` | `~/.bashrc` |
| `dot_zshrc` | `~/.zshrc` |
| `dot_config/zsh/keybinds.zsh` | `~/.config/zsh/keybinds.zsh` |
| `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)). 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, for both pacman and Homebrew. `suvadu` (the `suv` history tool) is declared under `packages.shared.cargo`, but none of the apply scripts install cargo packages, so install it manually if you want it.
## ~/.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.
2. **Go.** `GOPATH=~/.local/share/go`, `GOBIN=$GOPATH/bin`, and `GOBIN` prepended to `PATH`. mise provides `GOROOT`.
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.
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. **atuin.** Sources `~/.atuin/bin/env`, then `eval "$(atuin init bash)"`.
bash has no custom key bindings. atuin's init binds <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, and Android blocks as `.bashrc`.
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. `eval "$(starship init zsh)"` if starship is installed.
10. **Syntax highlighting.** Sources the first `zsh-syntax-highlighting.zsh` found in `/usr/share/zsh/plugins/zsh-syntax-highlighting/`, `/opt/homebrew/share/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.
11. **atuin.** Sources `~/.atuin/bin/env` and runs `eval "$(atuin init zsh)"`. These lines come after the syntax-highlighting block, so despite the comment, syntax highlighting is not actually last.
## 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`, `gho`, `pj`, `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.
| Key | Action |
| --- | --- |
| <kbd>ctrl-e</kbd> | `yy` (yazi, cd to its exit dir) |
| <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` (repo or vault picker, cd) |
| <kbd>ctrl-p</kbd> | `pj` (task runner) |
| <kbd>ctrl-a</kbd> | `omp` |
| <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), <kbd>down</kbd> (`pj`), and <kbd>?</kbd> have no zsh equivalent. <kbd>ctrl-h/j/k/l</kbd> belong to kitty ([kitty](/terminal/kitty)) and are not bound here.
## 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:
```sh ~/.config/shell/aliases.sh
# Clipboard
if command -v wl-copy >/dev/null 2>&1; then
alias cpy='wl-copy'
alias pst='wl-paste'
elif command -v pbcopy >/dev/null 2>&1; then
alias cpy='pbcopy'
alias pst='pbpaste'
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, else macOS `pbcopy`/`pbpaste`.
- **`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](/shell/aliases) 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` or `pbcopy`, 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` | `command gho`, cd into the printed path, `clear`, and `onefetch` if it is a git repo |
| `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](/shell/scripts#why-some-helpers-are-shell-functions) page explains why these helpers are shell functions and not scripts.
+300
View File
@@ -0,0 +1,300 @@
---
title: fish
description: 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` | `~/.config/fish/auto-Niri.fish` (Linux `desktop`/`laptop` only) |
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 repo directly. `~/.config/fish/fish_variables` (universal variables, including the selected theme) is listed in `.chezmoiignore.tmpl` and stays local to each machine.
## Installation
`fish` is in `.chezmoidata/packages.yaml` under `packages.arch.pacman` and `packages.darwin.brew`. `.chezmoiscripts/run_onchange_before_10-packages.sh.tmpl` installs it with pacman on Arch and Homebrew on macOS. The tools that fish initializes (starship, zoxide, atuin, fzf, eza, gum, yazi) come from the same manifest. mise, television, hishtory, envman, and the Bun, 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:
```text ~/.config/fish/fish_plugins
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` |
| `atuin.env.fish` | Sources `~/.atuin/bin/env.fish` (puts atuin's install dir on `PATH`) |
| `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` |
| `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` |
| `EDITOR`, `VISUAL` | `nvim` |
| `BROWSER` | `/usr/bin/brave-origin-nightly` 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`.
```fish ~/.config/fish/conf.d/10-env.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, else `pbcopy`/`pbpaste`.
- `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](/shell/aliases) 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](/shell/starship).
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` and `atuin ai init fish | source`. See [atuin](/shell/atuin).
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)).
8. A copy of the `_atuin_ai_question_mark` function that `atuin ai init fish` emits, 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> | `yy`: yazi, then cd to the directory yazi exited in |
| <kbd>ctrl-f</kbd> | `yy` (same as <kbd>ctrl-e</kbd>) |
| <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`: pick a ghq repo or Obsidian vault and cd into it |
| <kbd>ctrl-p</kbd> | `pj`: fzf task runner ([Scripts](/shell/scripts#pj)) |
| <kbd>ctrl-a</kbd> | `omp` (the coding agent, see [AI agents](/ai)) |
| <kbd>ctrl-w</kbd> | `atuin ai inline`: natural-language command overlay |
| <kbd>ctrl-g</kbd> | `lazygit` |
| <kbd>down</kbd> | `_pj_bind_down`: runs `pj` 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_QUIET=1 pj`, so pressing down outside a project does nothing and prints no error.
:::note
Under kitty, <kbd>ctrl-g</kbd> (lazygit overlay), <kbd>ctrl-a</kbd> (omp split), <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).
:::
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) |
| `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](/shell/scripts) 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` |
| `_pj_bind_down` | Down-arrow handler: `pj` on an empty idle prompt, else `down-or-search` |
| `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, see [niri](/desktop/niri)) |
| `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` | Runs `~/.local/bin/gho` (repo/vault picker), 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-e</kbd>/<kbd>ctrl-f</kbd> binds |
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
```fish ~/.config/fish/auto-Niri.fish
# Auto start Niri on tty1
if test -z "$DISPLAY" ;and test "$XDG_VTNR" -eq 1
mkdir -p ~/.cache
exec niri-session > ~/.cache/niri.log 2>&1
end
```
This file is applied only on Linux with role `desktop` or `laptop`. It is not in `conf.d/`, so fish does not load it automatically, and nothing in the repo sources it. To start niri on login to tty1, source it from a login-shell snippet. See [niri](/desktop/niri).
+40
View File
@@ -0,0 +1,40 @@
---
title: Shell
description: fish, bash, and zsh with shared aliases, the starship prompt, atuin history, and the ~/.local/bin scripts.
sidebar:
label: Overview
---
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](/shell/fish) | `~/.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](/shell/bash-zsh) | `~/.bashrc`, `~/.zshrc`, zsh key bindings, `~/.config/shell/aliases.sh`, per-shell function ports |
| [Aliases](/shell/aliases) | Every alias from `.chezmoidata/aliases.yaml` plus the fish-only and shell-specific ones |
| [Scripts](/shell/scripts) | Every general-purpose executable in `~/.local/bin` (`pj`, `gho`, `ghn`, `gop`, `atuin-pick`, …) |
| [Starship](/shell/starship) | `~/.config/starship.toml`: format, modules, symbols, cyberdream palette |
| [atuin](/shell/atuin) | `~/.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 |
| 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`, `mac`, `server`) except `~/.config/fish/auto-Niri.fish`. That file is applied only on Linux `desktop`/`laptop` machines, along with the rest of the [desktop layer](/desktop).
## Packages
`.chezmoidata/packages.yaml` supplies the shell tooling on Arch (pacman) and macOS (Homebrew): `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` on Arch and `brew install` on macOS. See [chezmoi scripts](/chezmoi/scripts).
+7
View File
@@ -0,0 +1,7 @@
import { defineMeta } from "blume";
export default defineMeta({
title: "Shell",
icon: "terminal",
pages: ["index", "fish", "bash-zsh", "aliases", "scripts", "starship", "atuin"],
});
+294
View File
@@ -0,0 +1,294 @@
---
title: Scripts
description: 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 desktop scripts in the same directory are documented with their compositor:
- `niri-launch`, `niri-launch-ai`, `niri-launch-browser`, `niri-launch-terminal`, `niri-launch-webapp`, `cp-appid`: [niri](/desktop/niri)
- `omarchy-*`: [Hyprland](/desktop/hyprland)
## 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 the script picked or cloned |
| `clip-last` | Reads the shell's own history (`$history[1]` in fish, `fc` in bash/zsh) |
For `ghg`, `ghn`, and `gho`, 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`.
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](/shell/fish#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 |
| [`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 |
| [`gho`](#gho) | sh | Pick a ghq repo or Obsidian vault, 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 |
| [`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 |
| [`mdcp`](#mdcp) | sh | Fetch a URL as Markdown, copy it, preview it |
| [`mdp`](#mdp) | sh | fzf Markdown picker with mdfried preview |
| [`pj`](#pj) | bash | fzf task runner across every project task 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 resolves `atuin` from `PATH` or `~/.atuin/bin/atuin`, 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) and [atuin](/shell/atuin).
**Needs:** `python3`, `atuin`, `fish`.
## 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:
<Steps>
<Step title="Preflight">Checks that `gh`, `gum`, and `jq` exist and that `gh auth status` succeeds.</Step>
<Step title="Discover">Lists your GitHub orgs whose login contains `sonr`, excluding the target `sonr-io`. You pick which to migrate with `gum choose --no-limit`.</Step>
<Step title="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.</Step>
<Step title="Settle">Waits 30 seconds (`gum spin`), because GitHub transfers are asynchronous.</Step>
<Step title="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>`.</Step>
</Steps>
**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](/shell/fish#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`.
**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` in fish), optional `zoxide`.
## 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`.
## gho
```sh
gho
```
An fzf picker with two lists. <kbd>tab</kbd> switches between them:
- `repo:`: `ghq list` plus the chezmoi source tree, listed as `github.com/prdlk/dotfiles` and mapped back to `~/.local/share/chezmoi`. `--tiebreak=end` favors matches at the end of the name.
- `vault:`: the Obsidian vaults `~/Documents/Obsidian/Personal`, `Walletbase`, and `Research`.
It prints the chosen directory. fzf's bind snippets run under `SHELL=/bin/sh`, so they behave the same whatever the login shell. The `gho` shell function cds into the result, clears the screen, and runs `onefetch` in git repos.
**Launched by:** <kbd>ctrl-o</kbd> in fish and zsh.
**Needs:** `ghq`, `fzf`, `onefetch` (for the wrapper).
## 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`.
## 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)), `omp` ([AI agents](/ai)), `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.
## mdcp
```sh
mdcp <url>
```
Fetches the URL as Markdown with `curl.md`, copies it with `wl-copy` or `pbcopy`, then pastes it back into `mdfried` for a rendered preview.
**Needs:** `curl.md`, `mdfried`, and `wl-clipboard` or `pbcopy`.
## 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`.
## pj
```sh
pj [extra args…]
```
A task runner that searches every task source in the project and lets you pick one with fzf.
**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.** 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. The header shows the nearest project root. The preview (top, 6 lines) shows the task body: the npm script string, the Make recipe, `just --show`, `mise tasks info`, the devbox script, or the pyproject entry.
**Run.** cds into the task's directory and `exec`s the runner. Extra arguments are forwarded, 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. The fish <kbd>down</kbd> binding uses it.
**Launched by:** <kbd>ctrl-p</kbd> in fish and zsh, and <kbd>down</kbd> on an empty fish prompt.
**Needs:** `bash`, `fzf`, `jq`, `python3` 3.11+ (for `tomllib`), plus whichever runners the project uses.
## 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).
**Needs:** `nvim`, `realpath`.
+88
View File
@@ -0,0 +1,88 @@
---
title: Starship
description: 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 Homebrew 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` |
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](/shell/fish#configfish).
## Layout
```toml ~/.config/starship.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](/shell/fish#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.