refactor(cli): group theme browser files

This commit is contained in:
Robby Russell
2026-09-09 05:48:02 -07:00
parent 2e0c643117
commit 3bf157dc77
9 changed files with 74 additions and 137 deletions
+1 -1
View File
@@ -181,7 +181,7 @@ omz theme preview agnoster # Print one isolated static preview
In the browser, type to filter, use Up/Down or Ctrl-P/Ctrl-N to navigate, and press Enter
for explicit use/save actions. Esc or Ctrl-C cancels without applying a theme.
See the [theme browser guide](tools/theme-browser.md) for shortcuts, preview limitations,
See the [theme browser guide](tools/theme-browser/README.md) for shortcuts, preview limitations,
and local testing instructions.
We'll admit it. Early in the Oh My Zsh world, we may have gotten a bit too theme-happy. We have over one
+4 -4
View File
@@ -79,7 +79,7 @@ function _omz {
_describe -o 'options' opts ;;
theme::(browse|preview))
local -a themes
themes=("${(@f)$(source "$ZSH/tools/theme-preview.zsh"; _omz_theme_names)}")
themes=("${(@f)$(source "$ZSH/tools/theme-browser/preview.zsh"; _omz_theme_names)}")
_describe 'theme' themes ;;
theme::(set|use))
local -aU themes
@@ -774,7 +774,7 @@ function _omz::theme::preview {
print -u2 -r -- 'Usage: omz theme preview <theme>'
return 1
fi
source "$ZSH/tools/theme-preview.zsh"
source "$ZSH/tools/theme-browser/preview.zsh"
_omz_theme_preview "$1"
}
@@ -787,8 +787,8 @@ function _omz::theme::browse {
print -u2 -r -- 'omz theme browse requires an interactive terminal; use omz theme preview <theme> instead.'
return 1
fi
source "$ZSH/tools/theme-preview.zsh"
source "$ZSH/tools/theme-browser.zsh"
source "$ZSH/tools/theme-browser/preview.zsh"
source "$ZSH/tools/theme-browser/browser.zsh"
local selection action name
selection=$(_omz_theme_browser "$1") || return $?
[[ -n $selection ]] || return 0
-118
View File
@@ -1,118 +0,0 @@
# Theme Browser
```zsh
omz theme browse
omz theme browse agn
omz theme preview agnoster
```
The browser renders only the highlighted theme, in your invoking directory with real,
synchronous Git information. Filtering is a case-insensitive literal substring match.
Names are deduplicated; resolution follows normal Oh My Zsh precedence:
`$ZSH_CUSTOM/<name>.zsh-theme`, `$ZSH_CUSTOM/themes/<name>.zsh-theme`, then
`$ZSH/themes/<name>.zsh-theme`. Nested custom themes are supported. The `random`
selector and names containing traversal components or control characters are excluded.
## Controls
| Key | Action |
| --- | --- |
| Up / Down, Ctrl-P / Ctrl-N | Previous / next theme |
| Page Up / Page Down | Move one page |
| Printable text | Filter names |
| Backspace | Delete the last filter character |
| Ctrl-U | Clear the filter |
| Enter | Open selection actions |
| Esc / Ctrl-C | Cancel without applying or saving |
| `u` in selection actions | Use the theme for this session |
| `s` in selection actions | Save as default and reload the shell |
| `b` or Enter in selection actions | Return to browsing |
Using a theme calls `omz theme use`: it does **not** remove hooks, widgets, or other
state installed by your previous theme. Saving calls `omz theme set`, edits `.zshrc`,
and reloads the shell. These actions happen only after leaving the browser and
restoring terminal state. Names containing shell punctuation cannot be saved through
the browser; configure those manually. No global aliases or keybindings are installed.
## Preview Limits
- Each preview runs in a fresh `zsh -df` worker, not your active shell. The worker
inherits the directory and exported environment, but not non-exported theme
settings, plugins, user `.zshrc`, or custom library overrides. System `zshenv`
still runs as required by zsh.
- Previews initialize prompt helpers and precmd hooks. They show a successful
previous command (status 0). Tests also exercise nonzero status rendering.
- Multiline left prompts are supported. Right prompts are deliberately labeled
and displayed separately, not positioned as ZLE would position them.
- Interactive/async themes, plugin-dependent segments, terminal queries, and
widget-driven prompts may be incomplete or unsupported. Themes still require
their usual fonts for special glyphs; the browser itself requires no fonts.
- Workers have a three-second execution limit and a 32,000-byte output limit.
Navigation cancels obsolete work. ANSI colors are retained; other terminal
controls are filtered. Long lines and tall previews are clipped in the browser.
- The browser requires an interactive terminal with alternate-screen support and
at least 40 columns by 16 rows. Smaller windows show a resize message; Esc still
exits. `omz theme preview` also works without an interactive terminal.
- This is **shell-state isolation, not a security sandbox**. Only preview trusted
themes: their code can write files, access the network, or deliberately detach
processes. The supervisor cleans up ordinary descendants, not escaped processes.
No new third-party runtime is required. The implementation uses standard zsh modules
(`zpty`, `system`, `datetime`, `zselect`, `terminfo`) and platform utilities including
`stty`, `mktemp`, `mkfifo`, and `rm`. Git segments need Git as usual. Private temporary
files are removed when previews finish or are cancelled. Full OMZ startup, update
checks, and OMZ cache initialization are not invoked by workers. The old
`tools/theme_chooser.sh` is unchanged.
## Local Testing
From this checkout, launch a disposable interactive shell without reading your `.zshrc`:
```zsh
env ZSH="$PWD" zsh -dfi
```
Inside it, load the CLI and try previews. Change directory to a Git repository to
compare clean, dirty, and untracked-file states:
```zsh
source "$ZSH/lib/cli.zsh"
omz theme preview robbyrussell
omz theme preview agnoster
omz theme preview half-life
omz theme preview dieter
omz theme browse
```
This minimal shell is intended for preview/navigation testing. For a real session-use
test, load this checkout's full OMZ configuration in a disposable shell. Do not choose
Save unless you intend to edit your actual `.zshrc`; automated browser tests stub both
actions and never edit your configuration. Exit the disposable shell when finished.
Regression suites run without additional test frameworks:
```zsh
zsh -df tools/tests/theme-preview.zsh
zsh -df tools/tests/theme-browser.zsh
```
The tests cover resolution, prompt isolation, Git context, hook/status rendering,
terminal-control filtering, failures, time/output limits, descendant cleanup,
completion candidates, lazy loading, keyboard navigation, resizing, action dispatch,
and terminal restoration using real PTYs. Human visual testing in your terminal and
font is still required before proposing a PR. Linux/older-zsh testing remains pending.
## Footprint And Startup
Runtime code is roughly 18 KiB across the three `tools/theme-*.zsh` files, plus small
CLI wrappers and completion/help entries. Tests and this guide are additional text
files, not runtime dependencies. Measure the exact current footprint with:
```zsh
wc -c tools/theme-browser.zsh tools/theme-preview.zsh tools/theme-preview-worker.zsh
```
Normal shell startup only defines the CLI wrappers: none of the three tool files is
sourced, no browser modules are loaded, and no workers or browser I/O are started.
The regression suite verifies lazy loading. Startup timing should be considered
noise-sensitive; this prototype does not claim a measurable speed improvement.
+55
View File
@@ -0,0 +1,55 @@
# Theme Browser
Preview installed themes without changing your current prompt:
```zsh
omz theme browse
omz theme browse agn
omz theme preview agnoster
```
The browser filters theme names as you type and renders the highlighted theme using
the current directory, including synchronous Git information.
## Controls
| Key | Action |
| --- | --- |
| Up / Down, Ctrl-P / Ctrl-N | Previous / next theme |
| Page Up / Page Down | Move one page |
| Printable text | Filter names |
| Backspace | Delete the last filter character |
| Ctrl-U | Clear the filter |
| Enter | Open selection actions |
| Esc / Ctrl-C | Cancel without applying or saving |
| `u` in selection actions | Use the theme for this session |
| `s` in selection actions | Save as default and reload the shell |
| `b` or Enter in selection actions | Return to browsing |
Using a theme changes the current session. It does not remove hooks or widgets installed
by the previous theme. Saving updates `ZSH_THEME` in `.zshrc` and reloads the shell.
## Limitations
- Previews run in a fresh shell, so plugin-dependent and interactive themes may be incomplete.
- Right prompts are labeled and shown separately rather than positioned by ZLE.
- The browser requires an interactive terminal with alternate-screen support.
- Theme code is isolated from your active shell, but it is not sandboxed. Preview only trusted themes.
## Local Testing
From this checkout, launch a disposable interactive shell:
```zsh
env ZSH="$PWD" zsh -dfi
source "$ZSH/lib/cli.zsh"
omz theme preview robbyrussell
omz theme browse
```
Run the regression suites with:
```zsh
zsh -df tools/theme-browser/tests/preview_test.zsh
zsh -df tools/theme-browser/tests/browser_test.zsh
```
@@ -1,4 +1,4 @@
# Invoked only by theme-preview.zsh in a fresh zsh -df process. Themes and hooks
# Invoked only by preview.zsh in a fresh zsh -df process. Themes and hooks
# are intentionally sourced/run at top level (not inside a setup function).
emulate -R zsh
if [[ $1 == --supervise ]]; then
@@ -5,7 +5,7 @@ typeset -g _OMZ_THEME_PREVIEW_DIR=${${(%):-%x}:A:h}
function _omz_theme_names() (
emulate -L zsh
setopt extendedglob
local root=${ZSH:-${_OMZ_THEME_PREVIEW_DIR:h}}
local root=${ZSH:-${_OMZ_THEME_PREVIEW_DIR:h:h}}
local custom=${ZSH_CUSTOM:-$root/custom} dir file name
local -a names files
for dir in "$custom" "$root/themes"; do
@@ -28,7 +28,7 @@ function _omz_theme_names() (
function _omz_theme_resolve() (
emulate -L zsh
setopt extendedglob
local name=$1 root=${ZSH:-${_OMZ_THEME_PREVIEW_DIR:h}}
local name=$1 root=${ZSH:-${_OMZ_THEME_PREVIEW_DIR:h:h}}
local custom=${ZSH_CUSTOM:-$root/custom} dir
if (( $# != 1 )) || [[ -z $name || $name == random || /$name/ == */(.|..)/* || $name == /* || $name == *//* || $name == *[[:cntrl:]\\]* ]]; then
print -u2 -r -- 'theme preview: invalid or non-selectable theme name'
@@ -60,7 +60,7 @@ function _omz_theme_preview() (
return 1
fi
local tmp fifo_fd pty=omz-preview-$sysparams[pid] record pgid raw='' chunk error=''
local selected=$1 custom=${ZSH_CUSTOM:-${ZSH:-$backend:h}/custom}
local selected=$1 custom=${ZSH_CUSTOM:-${ZSH:-${backend:h:h}}/custom}
local -i active=0 cancelled=0 bytes=0 count remaining result=1
local -F deadline
trap 'cancelled=130' INT
@@ -73,8 +73,8 @@ function _omz_theme_preview() (
# Exec immediately: a forked shell function could run inherited EXIT traps
# after returning. Both the launcher and theme worker must start fresh.
local -a launch=("$commands[zsh]" -df "$backend/theme-preview-worker.zsh" --supervise
"$tmp" "$backend:h" "$theme" "$sample" "$selected" "$custom")
local -a launch=("$commands[zsh]" -df "$backend/preview-worker.zsh" --supervise
"$tmp" "${backend:h:h}" "$theme" "$sample" "$selected" "$custom")
deadline=$(( EPOCHREALTIME + 3 ))
if ! zpty -b "$pty" exec "${(@q)launch}"; then
print -u2 -r -- 'theme preview: could not allocate a private PTY'
@@ -1,5 +1,5 @@
#!/usr/bin/env zsh
# Run with: zsh -df tools/tests/theme-browser.zsh
# Run with: zsh -df tools/theme-browser/tests/browser_test.zsh
# Optionally pass one group, e.g. `slow-interrupt`, to run it alone.
# Each browser runs under a real controlling PTY, with no user startup files.
emulate -R zsh
@@ -85,7 +85,7 @@ setopt err_exit pipe_fail
zmodload zsh/zpty
zmodload zsh/datetime
zmodload zsh/zselect
typeset -r repo=${0:A:h:h:h} self=${0:A}
typeset -r repo=${0:A:h:h:h:h} self=${0:A}
typeset scratch=$(mktemp -d "${TMPDIR:-/tmp}/omz-browser-test.XXXXXXXX")
scratch=${scratch:A}
trap 'zpty -d 2>/dev/null; command rm -rf -- "$scratch"' EXIT
@@ -1,9 +1,9 @@
#!/usr/bin/env zsh
# Run with: zsh -df tools/tests/theme-preview.zsh
# Run with: zsh -df tools/theme-browser/tests/preview_test.zsh
emulate -R zsh
setopt err_exit pipe_fail
typeset -r repo=${0:A:h:h:h}
source "$repo/tools/theme-preview.zsh"
typeset -r repo=${0:A:h:h:h:h}
source "$repo/tools/theme-browser/preview.zsh"
typeset scratch=$(mktemp -d "${TMPDIR:-/tmp}/omz-preview-test.XXXXXXXX")
scratch=${scratch:A}
trap 'command rm -rf -- "$scratch"' EXIT
@@ -220,7 +220,7 @@ check test -z "${child_state//[ ZN+]/}"
# Match the browser's nested-PTY invocation, preserving other owned PTYs.
zmodload zsh/zpty
zpty -b unrelated 'exec sleep 30'
typeset -a nested_command=("$commands[zsh]" -dfc 'source "$1"; _omz_theme_preview status' zsh "$repo/tools/theme-preview.zsh")
typeset -a nested_command=("$commands[zsh]" -dfc 'source "$1"; _omz_theme_preview status' zsh "$repo/tools/theme-browser/preview.zsh")
zpty -b browser exec "${(@q)nested_command}"
output=''
for attempt in {1..300}; do
@@ -239,7 +239,7 @@ zpty -d browser unrelated
# Cancellation by the browser's outer process group must also clean the
# backend's separate inner PTY group and private FIFO directory.
rm -f "$PREVIEW_PID_FILE"
nested_command=("$commands[zsh]" -dfc 'source "$1"; _omz_theme_preview slow' zsh "$repo/tools/theme-preview.zsh")
nested_command=("$commands[zsh]" -dfc 'source "$1"; _omz_theme_preview slow' zsh "$repo/tools/theme-browser/preview.zsh")
zpty -b browser exec "${(@q)nested_command}"
typeset browser_group record
for record in "${(@f)$(zpty)}"; do
@@ -266,7 +266,7 @@ zpty -d browser
export ZSH=$repo
git -C "$scratch/work" init -q
git -C "$scratch/work" symbolic-ref HEAD refs/heads/preview-branch
output=$(zsh -dfc 'builtin cd "$1"; source tools/theme-preview.zsh; builtin cd "$2"; _omz_theme_preview robbyrussell' zsh "$repo" "$scratch/work")
output=$(zsh -dfc 'builtin cd "$1"; source tools/theme-browser/preview.zsh; builtin cd "$2"; _omz_theme_preview robbyrussell' zsh "$repo" "$scratch/work")
check test "${output#*preview-branch}" != "$output"
for name in robbyrussell dieter agnoster bira nicoulaj half-life; do
output=$(builtin cd "$scratch/work"; _omz_theme_preview "$name" 7)