mtsh ==== A zsh prompt that reports the state you would otherwise have to ask for: the clock, the machine, where you are, and what git thinks of it. It draws one frame per command and it does not make you wait for it. ![The mtsh prompt across a branch switch, an edit, a build and a failing command](img/demo.svg) The recording above is generated from the theme itself by `tools/mkdemo.sh`; it is not a screenshot of an older version. What the prompt reports ----------------------- ╭─ 2026-08-23 · 14:32:07 · 10.0.0.7 · cpu 12% · 8.1G/31G · 1 user │ ~/src/linux/drivers/net │ ⎇ master #a1b2c3d · ⇡2 ahead · ✚1 ●2 ?3 · ✗ 1 · 1.24s ╰─ torvalds@linux ❯ First row, the machine: 2026-08-23 date 14:32:07 time the prompt was drawn 10.0.0.7 address of the interface that reaches the default route cpu 12% how busy the CPU was while the last command ran 8.1G/31G memory in use, out of total 1 user distinct logins on this machine Second row, the working directory, with `$HOME` written as `~`. Leading components are shortened to their first character before anything is dropped, so `/usr/local/share/zsh/site-functions` becomes `/u/l/share/zsh/site-functions` rather than losing its tail. Third row, git, then the outcome of the last command: ⎇ master the branch, or "detached" when there is no branch #a1b2c3d the commit at HEAD ⇡2 ahead position relative to the upstream branch ✚1 ●2 ?3 staged, modified, untracked ✗ 1 exit status of the last command 1.24s how long it took Fourth row is the prompt itself. The frame is deliberately open on the right. Closing it would mean knowing how many columns the content occupies, and a shell cannot know that: `${#}` counts code points, the terminal draws columns, and for characters like U+25CF those two disagree. A frame that cannot be closed correctly is better left open. Git states ---------- ⎇ master on a branch detached #a1b2c3d no branch; the commit is named instead no commits yet a repository with no HEAD rebase a rebase is in progress (also: merge, cherry-pick, revert, bisect) synced level with the upstream branch ⇡2 ahead two commits the upstream does not have ⇣3 behind three commits you do not have ⇣2 ⇡1 diverged both no upstream the branch tracks nothing stale the comparison above was computed from a fetch older than MTSH_FETCH_AGE seconds; run git fetch clean nothing to commit ✚n n paths staged ●n n paths modified but not staged ?n n untracked paths ✖n n unmerged paths The upstream is whatever the branch actually tracks, not `origin/`. Requirements ------------ zsh, git 2.11 or later (for `git status --porcelain=v2`), and a UTF-8 locale if you want the default glyphs. Developed and tested on zsh 5.9; nothing in the theme is newer than zsh 5.0. oh-my-zsh is supported but not required. No fonts need installing. Earlier versions of this theme asked for Powerline fonts; they are no longer used. Installation ------------ With oh-my-zsh: git clone https://github.com/FPGArtktic/ohmyzsh-mtsh-theme.git cp ohmyzsh-mtsh-theme/mtsh.zsh-theme ~/.oh-my-zsh/custom/themes/ sed -i 's/^ZSH_THEME=.*/ZSH_THEME="mtsh"/' ~/.zshrc exec zsh Without oh-my-zsh, put the file wherever you like and source it from the end of `~/.zshrc`: source ~/.config/zsh/mtsh.zsh-theme Sourcing the theme over a shell that is already running it replaces the hooks rather than adding a second copy, so `source` is a safe way to pick up an edit. Configuration ------------- Every knob is an environment variable read at load time. Set the ones you want before the theme is sourced; oh-my-zsh users can set them above the `source $ZSH/oh-my-zsh.sh` line. Layout: MTSH_FRAME 1 0 drops the frame and prints bare rows MTSH_BLANK_LINE 1 blank line above the frame, so command output does not run into the prompt MTSH_GLYPHS auto auto | unicode | nerd | ascii MTSH_MIN_WIDTH 48 below this, print only the prompt line MTSH_MAX_WIDTH 0 0 follows $COLUMNS MTSH_PATH_SEGMENTS 3 trailing path components kept in full Fields, all 1 or 0: MTSH_SHOW_DATE 1 MTSH_SHOW_TIME 1 MTSH_SHOW_IP 1 MTSH_SHOW_CPU 1 MTSH_SHOW_RAM 1 MTSH_SHOW_USERS 1 MTSH_SHOW_GIT 1 MTSH_SHOW_STATUS 1 exit status of the last command MTSH_SHOW_ELAPSED 1 how long the last command took MTSH_SHOW_PIPESTATUS 1 show "1|0|1" when a pipeline fails MTSH_SHOW_CONTEXT 1 ssh, virtualenv and background jobs Behaviour: MTSH_ELAPSED_MIN_MS 0 hide timings faster than this MTSH_ELAPSED_SLOW_MS 5000 colour timings slower than this MTSH_IP_TTL 30 seconds between ip(8) lookups MTSH_USERS_TTL 30 seconds between who(1) lookups MTSH_FETCH_AGE 120 when to call the fetch data stale MTSH_GIT_DIRTY 1 0 skips git status entirely MTSH_GIT_UNTRACKED normal 'no' is much faster on very large trees MTSH_GIT_BRANCH_MAX 32 branch names longer than this are cut Colours are prompt escapes, one variable per element, named after the element: MTSH_COLOR_frame MTSH_COLOR_date MTSH_COLOR_time MTSH_COLOR_ip MTSH_COLOR_cpu MTSH_COLOR_ram MTSH_COLOR_users MTSH_COLOR_path MTSH_COLOR_branch MTSH_COLOR_hash MTSH_COLOR_ok MTSH_COLOR_bad MTSH_COLOR_warn MTSH_COLOR_dim MTSH_COLOR_user MTSH_COLOR_host MTSH_COLOR_caret MTSH_COLOR_root MTSH_COLOR_ctx For example: MTSH_COLOR_path='%F{cyan}' MTSH_COLOR_branch='%F{208}' `%F{#rrggbb}` also works, on zsh 5.7 and later. A repository can opt out of the dirty-state scan the way oh-my-zsh expects, which is worth doing in a tree where `git status` is slow: git config --bool oh-my-zsh.hide-dirty true Cost ---- The prompt runs between pressing Enter and getting the shell back, so it is written not to fork. CPU and memory come from `/proc`; the clock, timing and file timestamps come from zsh modules. `$GIT_DIR` is found by walking up from `$PWD` in the shell, so a directory that is not a repository costs nothing at all. Inside a repository the entire git summary is one `git status --porcelain=v2 --branch`. `ip(8)` and `who(1)` are the only other external commands and both sit behind a TTL. Steady state is one process per prompt in a repository and none outside one. Measured on this tree, twenty prompts each: before after git repository 255.1ms 4.4ms 6000-file repo 289.8ms 8.9ms not a repository 252.7ms 0.7ms If the prompt is still slow, the cause is almost always `git status` on a very large working tree. Try `MTSH_GIT_UNTRACKED=no` first, then `MTSH_GIT_DIRTY=0`. Degradation ----------- The theme is meant to stay usable when the terminal is not ideal. Glyphs. `MTSH_GLYPHS=auto` picks `unicode` under a UTF-8 locale and `ascii` otherwise, or when `$TERM` names a console that cannot do better. Nerd Fonts cannot be detected -- no escape sequence asks a terminal whether it can draw U+E0A0 -- so `auto` never selects them; set `MTSH_GLYPHS=nerd` if you have one installed. Colour. 256-colour terminals get the full palette, everything else gets the eight named colours. Width. The frame follows `$COLUMNS`. As the terminal narrows, fields are dropped from the right: the login count goes first, the branch name is the last thing standing. Below `MTSH_MIN_WIDTH` columns only the prompt line is printed. Platform. CPU and memory need Linux `/proc`; elsewhere those two fields are omitted and everything else still works. Security -------- Branch names and directory names are attacker-controlled the moment you clone somebody else's repository, and the prompt renders both. Every string that does not originate in the theme is percent-quoted before it reaches prompt expansion, so a branch called %F{red}%n@%m prints as those fourteen characters and does not get to choose the colour of your prompt or print your login name. Releases before 2025 did not do this. Reporting bugs -------------- Open an issue with the output of zsh --version git --version echo $TERM ${LC_ALL:-${LC_CTYPE:-$LANG}} $COLUMNS and, if the problem is a misdrawn frame, a copy-paste of the text rather than a screenshot -- the characters matter and a picture loses them. If the prompt is slow, say what `MTSH_GIT_DIRTY=0` does to it, since that narrows the cause to git in one step. Submitting patches ------------------ Patches and pull requests are both fine. Either way: One logical change per commit. A commit that fixes a bug and tidies the surrounding code is two commits. If a series is easier to review split differently, split it differently. Write the subject in the imperative, prefixed with the area it touches, and keep it under about 60 characters: mtsh: quote dynamic data before prompt expansion Explain in the body why the change is right, not what the diff already shows. If the old behaviour was wrong, say what it did wrong and how to see it happen. Wrap at 72 columns. Sign off, certifying the Developer Certificate of Origin: Signed-off-by: Your Name Say how you tested it. The theme has no test suite, so state the shell version, the terminal, and the repository states you exercised; the awkward ones are a detached HEAD, an interrupted rebase, a repository with no commits, a branch containing a percent sign, and a terminal narrow enough to force truncation. If you change what the prompt prints, regenerate the recording: sh tools/mkdemo.sh It is reproducible, so an unchanged prompt produces an unchanged file. Licence ------- GNU General Public License, version 3 or later. See [LICENSE](LICENSE). SPDX-License-Identifier: GPL-3.0-or-later Author ------ Mateusz Okulanis ,