--- 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, 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)). 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. 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`. 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](/shell/atuin#installation)). atuin comes after tv, so it keeps ctrl-r and up. ## ~/.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. `eval "$(starship init zsh)"` if starship is installed. 10. `eval "$(tv init zsh)"` if `tv` is installed (ctrl-t smart autocomplete). 11. **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. 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 ctrl-r. ## 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 ctrl-q and ctrl-s from the terminal. A generic widget `_zle-run` is registered as `run-` 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 | | --- | --- | | ctrl-e | `tv-git-files` (pick a file, open it in nvim) | | ctrl-f | `yy` | | ctrl-b | `purple` (an external command; this repo does not provide it) | | ctrl-y | `clip-last` | | ctrl-o | `gho-inline` (`ghq-repos` cable, cd) | | ctrl-p | `pj-inline` (`pj` cable) | | ctrl-t | tv smart autocomplete (from `tv init zsh`) | | down | `pj-down`: `PJ_QUIET=1 pj` on an empty prompt, otherwise `down-line-or-history` (bound to `^[[B` and `^[OB`) | | ctrl-a | `omp-session`: resume or start omp for the current repo ([Scripts](/shell/scripts#omp-session)) | | ctrl-g | `lazygit` | | ctrl-q | `exit` | | ctrl-c | At the prompt: kill the buffer, clear the screen and kitty scrollback (`ESC[2J ESC[3J`), reset the prompt | ctrl-c 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 ctrl-w (atuin AI) and ? have no zsh equivalent. ctrl-h/j/k/l belong to kitty ([kitty](/terminal/kitty)) and are not bound here. The cables are described on the [television](/tools/television) 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 | | --- | --- | | ctrl-o | Macro `\C-x1\C-x2\C-x3\C-x0` (below): pick from the `ghq-repos` cable and cd | | ctrl-e | `_kb_git_files`: `tv git-files --inline`, then `_kb_up` | | ctrl-p | `_kb_pj`: `pj` with `PJ_STATE` set to a temp file; `_kb_up` only when the state is `open` (cancelled) | | down | `PJ_QUIET=1 _kb_pj` on an empty prompt, otherwise `history-search-forward` | | ctrl-t | `_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. ctrl-o 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. down (`\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 =''`. 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 ` | `ghg-clone "$@"`, cd into the printed path, `clear && onefetch` | | `ghn ` | `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 ctrl-o does not call it | | `yy [args]` | `yazi --cwd-file=`, 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.