diff --git a/README.md b/README.md index 1c8888f18..4e2a8a6cc 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/cli.zsh b/lib/cli.zsh index d15b977a7..53ae58927 100644 --- a/lib/cli.zsh +++ b/lib/cli.zsh @@ -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 ' 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 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 diff --git a/tools/theme-browser.md b/tools/theme-browser.md deleted file mode 100644 index 280638642..000000000 --- a/tools/theme-browser.md +++ /dev/null @@ -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/.zsh-theme`, `$ZSH_CUSTOM/themes/.zsh-theme`, then -`$ZSH/themes/.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. diff --git a/tools/theme-browser/README.md b/tools/theme-browser/README.md new file mode 100644 index 000000000..82b501059 --- /dev/null +++ b/tools/theme-browser/README.md @@ -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 +``` diff --git a/tools/theme-browser.zsh b/tools/theme-browser/browser.zsh similarity index 100% rename from tools/theme-browser.zsh rename to tools/theme-browser/browser.zsh diff --git a/tools/theme-preview-worker.zsh b/tools/theme-browser/preview-worker.zsh similarity index 97% rename from tools/theme-preview-worker.zsh rename to tools/theme-browser/preview-worker.zsh index b0db83092..e702ad6cb 100644 --- a/tools/theme-preview-worker.zsh +++ b/tools/theme-browser/preview-worker.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 diff --git a/tools/theme-preview.zsh b/tools/theme-browser/preview.zsh similarity index 95% rename from tools/theme-preview.zsh rename to tools/theme-browser/preview.zsh index 91a6dd05f..63b096694 100644 --- a/tools/theme-preview.zsh +++ b/tools/theme-browser/preview.zsh @@ -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' diff --git a/tools/tests/theme-browser.zsh b/tools/theme-browser/tests/browser_test.zsh similarity index 99% rename from tools/tests/theme-browser.zsh rename to tools/theme-browser/tests/browser_test.zsh index b2512a8bb..950ff414e 100644 --- a/tools/tests/theme-browser.zsh +++ b/tools/theme-browser/tests/browser_test.zsh @@ -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 diff --git a/tools/tests/theme-preview.zsh b/tools/theme-browser/tests/preview_test.zsh similarity index 96% rename from tools/tests/theme-preview.zsh rename to tools/theme-browser/tests/preview_test.zsh index ba0812927..ae61914a8 100644 --- a/tools/tests/theme-preview.zsh +++ b/tools/theme-browser/tests/preview_test.zsh @@ -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)