mirror of
https://github.com/robbyrussell/oh-my-zsh.git
synced 2026-09-21 18:46:06 +02:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76439de772 | ||
|
|
3bf157dc77 | ||
|
|
2e0c643117 |
@@ -171,6 +171,19 @@ adds any) and extra goodies that are included in that particular plugin.
|
||||
|
||||
### Themes
|
||||
|
||||
Preview installed themes in your current directory without changing your prompt or `.zshrc`:
|
||||
|
||||
```zsh
|
||||
omz theme browse # Interactive browser
|
||||
omz theme browse agn # Start with a theme-name filter
|
||||
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/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
|
||||
hundred and fifty themes now bundled. Most of them have
|
||||
[screenshots](https://github.com/ohmyzsh/ohmyzsh/wiki/Themes) on the wiki (We are working on updating this!).
|
||||
|
||||
+46
-1
@@ -51,7 +51,7 @@ function _omz {
|
||||
_describe 'command' subcmds ;;
|
||||
pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request')
|
||||
_describe 'command' subcmds ;;
|
||||
theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
|
||||
theme) subcmds=('browse:Browse theme previews' 'preview:Preview a theme without applying it' 'list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
|
||||
_describe 'command' subcmds ;;
|
||||
esac
|
||||
elif (( CURRENT == 4 )); then
|
||||
@@ -77,6 +77,10 @@ function _omz {
|
||||
local -a opts
|
||||
opts=('--enabled:List enabled plugins only')
|
||||
_describe -o 'options' opts ;;
|
||||
theme::(browse|preview))
|
||||
local -a themes
|
||||
themes=("${(@f)$(source "$ZSH/tools/theme-browser/preview.zsh"; _omz_theme_names)}")
|
||||
_describe 'theme' themes ;;
|
||||
theme::(set|use))
|
||||
local -aU themes
|
||||
themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
|
||||
@@ -749,6 +753,8 @@ Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
|
||||
|
||||
Available commands:
|
||||
|
||||
browse [filter] Browse installed themes without applying them
|
||||
preview <theme> Preview a theme in the current directory
|
||||
list List all available Oh My Zsh themes
|
||||
set <theme> Set a theme in your .zshrc file
|
||||
use <theme> Load a theme
|
||||
@@ -763,6 +769,45 @@ EOF
|
||||
$0::$command "$@"
|
||||
}
|
||||
|
||||
function _omz::theme::preview {
|
||||
if (( $# != 1 )); then
|
||||
print -u2 -r -- 'Usage: omz theme preview <theme>'
|
||||
return 1
|
||||
fi
|
||||
source "$ZSH/tools/theme-browser/preview.zsh"
|
||||
_omz_theme_preview "$1"
|
||||
}
|
||||
|
||||
function _omz::theme::browse {
|
||||
if (( $# > 1 )); then
|
||||
print -u2 -r -- 'Usage: omz theme browse [filter]'
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -o interactive || ! -t 0 || ! -t 1 ]]; then
|
||||
print -u2 -r -- 'omz theme browse requires an interactive terminal; use omz theme preview <theme> instead.'
|
||||
return 1
|
||||
fi
|
||||
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
|
||||
action=${selection%%$'\n'*}
|
||||
name=${selection#*$'\n'}
|
||||
_omz_theme_resolve "$name" >/dev/null || return 1
|
||||
case $action in
|
||||
use) _omz::theme::use "$name" ;;
|
||||
set)
|
||||
# The existing set command interpolates the name into shell and awk text.
|
||||
[[ $name != *[^a-zA-Z0-9_./-]* ]] || {
|
||||
print -u2 -r -- 'Cannot save this theme name safely; set ZSH_THEME manually.'
|
||||
return 1
|
||||
}
|
||||
_omz::theme::set "$name" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function _omz::theme::list {
|
||||
local -a custom_themes builtin_themes
|
||||
custom_themes=("$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
|
||||
|
||||
+4
-10
@@ -14,16 +14,10 @@ FX=(
|
||||
reverse "%{[07m%}" no-reverse "%{[27m%}"
|
||||
)
|
||||
|
||||
# Build the tables with array operations instead of a 256-iteration loop
|
||||
() {
|
||||
setopt localoptions noksharrays
|
||||
local -a codes fg bg
|
||||
codes=({000..255})
|
||||
fg=( "%{"$'\e'"[38;5;"${^codes}"m%}" )
|
||||
bg=( "%{"$'\e'"[48;5;"${^codes}"m%}" )
|
||||
FG=( ${codes:^fg} )
|
||||
BG=( ${codes:^bg} )
|
||||
}
|
||||
for color in {000..255}; do
|
||||
FG[$color]="%{[38;5;${color}m%}"
|
||||
BG[$color]="%{[48;5;${color}m%}"
|
||||
done
|
||||
|
||||
# Show all 256 colors with color number
|
||||
function spectrum_ls() {
|
||||
|
||||
+1
-6
@@ -31,12 +31,7 @@ function title {
|
||||
else
|
||||
# Try to use terminfo to set the title if the feature is available
|
||||
if (( ${+terminfo[fsl]} && ${+terminfo[tsl]} )); then
|
||||
# Emit the capabilities with echoti so terminfo does its own parameter
|
||||
# substitution: some terminals (vt320) take a column argument, and
|
||||
# running prompt expansion over the capability string mangles it.
|
||||
echoti tsl 0
|
||||
print -Pn "$1"
|
||||
echoti fsl
|
||||
print -Pn "${terminfo[tsl]}$1${terminfo[fsl]}"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
@@ -241,7 +241,7 @@ function aws_regions() {
|
||||
function aws_profiles() {
|
||||
aws --no-cli-pager configure list-profiles 2> /dev/null && return
|
||||
[[ -r "${AWS_CONFIG_FILE:-$HOME/.aws/config}" ]] || return 1
|
||||
command grep -Eo '^[[:space:]]*\[[[:space:]]*(profile[[:space:]]+)?[^][:space:]]+[[:space:]]*\]' "${AWS_CONFIG_FILE:-$HOME/.aws/config}" | command sed -E 's/^[[:space:]]*\[[[:space:]]*(profile[[:space:]]+)?([^][:space:]]+)[[:space:]]*\]$/\2/'
|
||||
grep --color=never -Eo '\[.*\]' "${AWS_CONFIG_FILE:-$HOME/.aws/config}" | sed -E 's/^[[:space:]]*\[(profile)?[[:space:]]*([^[:space:]]+)\][[:space:]]*$/\2/g'
|
||||
}
|
||||
|
||||
function _aws_regions() {
|
||||
|
||||
@@ -99,21 +99,18 @@ $ find . -type f 2>/dev/null
|
||||
## File extension aliases
|
||||
|
||||
These are special aliases that are triggered when a file name is passed as the command. For example,
|
||||
if the pdf file extension is aliased to `open_command`, running `file.pdf` opens that file with
|
||||
whatever application your system uses for PDFs.
|
||||
if the pdf file extension is aliased to `acroread` (a popular Linux pdf reader), when running `file.pdf`
|
||||
that file will be open with `acroread`.
|
||||
|
||||
### Reading Docs
|
||||
|
||||
These use `open_command`, which picks the right opener for your platform: `open` on macOS,
|
||||
`xdg-open` on Linux, `cygstart` on Cygwin.
|
||||
|
||||
| Alias | Command | Description |
|
||||
| ----- | -------------- | ----------------------------------------------- |
|
||||
| pdf | `open_command` | Opens up a .pdf file in your default viewer |
|
||||
| ps | `open_command` | Opens up a .ps file in your default viewer |
|
||||
| dvi | `open_command` | Opens up a .dvi file in your default viewer |
|
||||
| chm | `open_command` | Opens up a .chm file in your default viewer |
|
||||
| djvu | `open_command` | Opens up a .djvu file in your default viewer |
|
||||
| Alias | Command | Description |
|
||||
| ----- | ---------- | ---------------------------------- |
|
||||
| pdf | `acroread` | Opens up a document using acroread |
|
||||
| ps | `gv` | Opens up a .ps file using gv |
|
||||
| dvi | `xdvi` | Opens up a .dvi file using xdvi |
|
||||
| chm | `xchm` | Opens up a .chm file using xchm |
|
||||
| djvu | `djview` | Opens up a .djvu file using djview |
|
||||
|
||||
### Listing files inside a packed file
|
||||
|
||||
|
||||
@@ -71,9 +71,12 @@ if is-at-least 4.2.0; then
|
||||
_media_fts=(ape avi flv m4a mkv mov mp3 mpeg mpg ogg ogm rm wav webm)
|
||||
for ft in $_media_fts; do alias -s $ft=mplayer; done
|
||||
|
||||
# read documents with the platform's default application
|
||||
_doc_fts=(pdf ps dvi chm djvu)
|
||||
for ft in $_doc_fts; do alias -s $ft=open_command; done
|
||||
#read documents
|
||||
alias -s pdf=acroread
|
||||
alias -s ps=gv
|
||||
alias -s dvi=xdvi
|
||||
alias -s chm=xchm
|
||||
alias -s djvu=djview
|
||||
|
||||
#list what's inside packed file
|
||||
alias -s zip="unzip -l"
|
||||
|
||||
@@ -1,33 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
emacsfun() {
|
||||
local cmd frames arg tty
|
||||
local cmd frames
|
||||
|
||||
# Build the Emacs Lisp command to check for suitable frames
|
||||
# See https://www.gnu.org/software/emacs/manual/html_node/elisp/Frames.html#index-framep
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-t|--tty|-nw) tty=1; break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$tty" ]; then
|
||||
cmd="(memq 't (mapcar 'framep (frame-list)))" # if != nil, there are tty frames
|
||||
else
|
||||
cmd="(delete 't (mapcar 'framep (frame-list)))" # if != nil, there are graphical terminals (x, w32, ns)
|
||||
fi
|
||||
|
||||
# A tty frame needs the client to stay attached, so drop the --no-wait the
|
||||
# `emacs` alias adds. Otherwise emacsclient returns at once and nothing opens.
|
||||
if [ -n "$tty" ]; then
|
||||
for arg do
|
||||
shift
|
||||
case "$arg" in
|
||||
--no-wait) continue ;;
|
||||
esac
|
||||
set -- "$@" "$arg"
|
||||
done
|
||||
fi
|
||||
case "$*" in
|
||||
*-t*|*--tty*|*-nw*) cmd="(memq 't (mapcar 'framep (frame-list)))" ;; # if != nil, there are tty frames
|
||||
*) cmd="(delete 't (mapcar 'framep (frame-list)))" ;; # if != nil, there are graphical terminals (x, w32, ns)
|
||||
esac
|
||||
|
||||
# Check if there are suitable frames
|
||||
frames="$(emacsclient -a '' -n -e "$cmd" 2>/dev/null |sed 's/.*\x07//g' )"
|
||||
|
||||
@@ -15,6 +15,9 @@ This plugin does not add any aliases.
|
||||
This plugin caches the completion script and is automatically updated when the
|
||||
plugin is loaded, which is usually when you start up a new terminal emulator.
|
||||
|
||||
The completion script is stored at `$ZSH_CACHE_DIR/completions/_gh`. If an
|
||||
update fails, the plugin keeps the last successfully generated completion
|
||||
script.
|
||||
The cache is stored at:
|
||||
|
||||
- `$ZSH/plugins/gh/_gh` completions script
|
||||
|
||||
- `$ZSH_CACHE_DIR/gh_version` version of GitHub CLI, used to invalidate
|
||||
the cache.
|
||||
|
||||
@@ -13,12 +13,6 @@ fi
|
||||
|
||||
zmodload -F zsh/files b:zf_mv
|
||||
() {
|
||||
local completion_file="$ZSH_CACHE_DIR/completions/_gh"
|
||||
local completion_tmp="${completion_file}.$$.${RANDOM}"
|
||||
|
||||
if gh completion --shell zsh >| "$completion_tmp" && [[ -s "$completion_tmp" ]]; then
|
||||
zf_mv -f -- "$completion_tmp" "$completion_file"
|
||||
fi
|
||||
|
||||
command rm -f -- "$completion_tmp"
|
||||
local TMPPREFIX="$ZSH_CACHE_DIR/completions/_gh"
|
||||
zf_mv -f -- =( gh completion --shell zsh ) "$TMPPREFIX"
|
||||
} &|
|
||||
|
||||
+36
-34
@@ -1,43 +1,45 @@
|
||||
# github plugin
|
||||
|
||||
> [!WARNING]
|
||||
> This plugin is deprecated and will be removed in a future release. It supports
|
||||
> the legacy [`hub`](https://github.com/mislav/hub) CLI, not the current GitHub
|
||||
> CLI. For `gh` completion, use the [`gh` plugin](../gh) instead.
|
||||
This plugin supports working with GitHub from the command line. It provides a few things:
|
||||
|
||||
The plugin provides compatibility for existing `hub` users by:
|
||||
* Sets up the `hub` wrapper and completions for the `git` command if you have [`hub`](https://github.com/github/hub) installed.
|
||||
* Completion for the [`github` Ruby gem](https://github.com/defunkt/github-gem).
|
||||
* Convenience functions for working with repos and URLs.
|
||||
|
||||
- aliasing `git` to `hub` when `hub` is installed;
|
||||
- adding completion for `hub`; and
|
||||
- defining convenience functions for creating repositories.
|
||||
### Functions
|
||||
|
||||
## Migrating to GitHub CLI
|
||||
* `empty_gh` - Creates a new empty repo (with a `README.md`) and pushes it to GitHub
|
||||
* `new_gh` - Initializes an existing directory as a repo and pushes it to GitHub
|
||||
* `exist_gh` - Takes an existing repo and pushes it to GitHub
|
||||
|
||||
Install [GitHub CLI](https://cli.github.com/), replace `github` with `gh` in
|
||||
your plugin list, and restart your shell:
|
||||
|
||||
## Installation
|
||||
|
||||
[Hub](https://github.com/github/hub) needs to be installed if you want to use it. On OS X with Homebrew, this can be done with `brew install hub`. The `hub` completion definition needs to be added to your `$FPATH` before initializing OMZ.
|
||||
|
||||
The [`github` Ruby gem](https://github.com/defunkt/github-gem) needs to be installed if you want to use it.
|
||||
|
||||
### Configuration
|
||||
|
||||
These settings affect `github`'s behavior.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
* `$GITHUB_USER`
|
||||
* `$GITHUB_PASSWORD`
|
||||
|
||||
#### Git configuration options
|
||||
|
||||
* `github.user` - GitHub username for repo operations
|
||||
|
||||
See `man hub` for more details.
|
||||
|
||||
### Homebrew installation note
|
||||
|
||||
If you have installed `hub` using Homebrew, its completions may not be on your `$FPATH` if you are using the system `zsh`. Homebrew installs `zsh` completion definitions to `/usr/local/share/zsh/site-functions`, which will be on `$FPATH` for the Homebrew-installed `zsh`, but not for the system `zsh`. If you want it to work with the system `zsh`, add this to your `~/.zshrc` before it sources `oh-my-zsh.sh`.
|
||||
|
||||
```zsh
|
||||
plugins=(... gh)
|
||||
if (( ! ${fpath[(I)/usr/local/share/zsh/site-functions]} )); then
|
||||
FPATH=/usr/local/share/zsh/site-functions:$FPATH
|
||||
fi
|
||||
```
|
||||
|
||||
The `gh` plugin supplies completion for the installed GitHub CLI. It does not
|
||||
alias `git`, because `gh` is not a Git wrapper.
|
||||
|
||||
The closest replacement for creating a GitHub repository from an existing
|
||||
local repository is:
|
||||
|
||||
```zsh
|
||||
gh repo create --source=. --push
|
||||
```
|
||||
|
||||
## Deprecated functions
|
||||
|
||||
- `empty_gh` - creates a new repository with a `README.md` and pushes it to GitHub
|
||||
- `new_gh` - initializes an existing directory and pushes it to GitHub
|
||||
- `exist_gh` - pushes an existing Git repository to GitHub
|
||||
|
||||
These functions remain available during the deprecation period and require
|
||||
`hub`. They will be removed with the plugin.
|
||||
|
||||
If you continue using `hub`, see its documentation for authentication and
|
||||
configuration details.
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
print -Pru2 -- '%F{yellow}[oh-my-zsh] The %Bgithub%b plugin is deprecated and will be removed in a future release.'
|
||||
print -Pru2 -- 'It supports the legacy %Bhub%b CLI. Use the %Bgh%b plugin for GitHub CLI completion.%f'
|
||||
|
||||
# Set up hub wrapper for git, if it is available; https://github.com/github/hub
|
||||
if (( $+commands[hub] )); then
|
||||
alias git=hub
|
||||
@@ -29,7 +26,7 @@ empty_gh() { # [NAME_OF_REPO]
|
||||
# This function will add all non-hidden files to git.
|
||||
new_gh() { # [DIRECTORY]
|
||||
emulate -L zsh
|
||||
local repo="${1:-.}"
|
||||
local repo="$1"
|
||||
cd "$repo" \
|
||||
|| return
|
||||
|
||||
@@ -45,7 +42,7 @@ new_gh() { # [DIRECTORY]
|
||||
|| return
|
||||
hub create \
|
||||
|| return
|
||||
git push -u origin HEAD \
|
||||
git push -u origin master \
|
||||
|| return
|
||||
}
|
||||
|
||||
@@ -55,19 +52,18 @@ new_gh() { # [DIRECTORY]
|
||||
# to your GitHub.
|
||||
exist_gh() { # [DIRECTORY]
|
||||
emulate -L zsh
|
||||
local repo="${1:-.}"
|
||||
cd "$repo" \
|
||||
|| return
|
||||
local repo=$1
|
||||
cd "$repo"
|
||||
|
||||
hub create \
|
||||
|| return
|
||||
git push -u origin HEAD
|
||||
git push -u origin master
|
||||
}
|
||||
|
||||
# git.io "GitHub URL"
|
||||
#
|
||||
# Shorten GitHub url, example:
|
||||
# https://github.com/nvogel/dotzsh > https://git.io/8nU25w
|
||||
# https://github.com/nvogel/dotzsh > https://git.io/8nU25w
|
||||
# source: https://github.com/nvogel/dotzsh
|
||||
# documentation: https://github.com/blog/985-git-io-github-url-shortener
|
||||
#
|
||||
@@ -78,3 +74,4 @@ git.io() {
|
||||
}
|
||||
|
||||
# End Functions #############################################################
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
# herdr plugin
|
||||
|
||||
This plugin adds completion, aliases, an interactive session picker and a prompt
|
||||
function for [herdr](https://herdr.dev), the runtime your
|
||||
coding agents live on. herdr keeps agent terminals running in the background across
|
||||
projects and machines, so you can close your terminal, reconnect later, and pick up
|
||||
where the agents left off.
|
||||
|
||||
To use it, add `herdr` to the plugins array in your zshrc file:
|
||||
|
||||
```zsh
|
||||
plugins=(... herdr)
|
||||
```
|
||||
|
||||
Install herdr first if you have not already:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://herdr.dev/install.sh | sh # or: brew install herdr
|
||||
```
|
||||
|
||||
## Learning herdr from the terminal
|
||||
|
||||
The fastest way to explore herdr is tab completion. Every command group and flag shows a
|
||||
short description, so you can discover what is available without leaving the shell:
|
||||
|
||||
```
|
||||
$ herdr <TAB>
|
||||
agent -- Control and inspect agent panes
|
||||
session -- Manage named persistent sessions
|
||||
workspace -- Manage workspaces over the socket API
|
||||
worktree -- Manage Git worktree-backed workspaces
|
||||
...
|
||||
$ herdr agent <TAB>
|
||||
list -- List agents
|
||||
prompt -- Submit a prompt to an agent
|
||||
start -- Start a supported interactive agent in an existing pane
|
||||
wait -- Wait until an agent reaches one of the requested states
|
||||
...
|
||||
```
|
||||
|
||||
A few other ways to learn as you go:
|
||||
|
||||
- `herdr <group>` with no subcommand prints that group's help, for example `herdr pane`.
|
||||
- `herdr --skill` prints the instructions herdr gives to agents that drive it. It is a
|
||||
good tour of the full command surface.
|
||||
- Most control commands return JSON, so you can pipe them into `jq` in scripts.
|
||||
|
||||
## Common workflows
|
||||
|
||||
### Start and reconnect
|
||||
|
||||
```sh
|
||||
herdr # launch, or attach to the default session
|
||||
herdr --session work # use or create a named session
|
||||
herdr session list # see running and stopped sessions
|
||||
herdr session attach work # reconnect to a named session
|
||||
herdr status # check the local client and running server
|
||||
herdr update # install the latest release
|
||||
```
|
||||
|
||||
### Organize work into workspaces and tabs
|
||||
|
||||
```sh
|
||||
herdr workspace list
|
||||
herdr workspace create --cwd ~/projects/app
|
||||
herdr tab create
|
||||
herdr pane split --direction right
|
||||
herdr worktree create --branch feature/login # open a Git worktree in its own workspace
|
||||
```
|
||||
|
||||
### Run and watch agents
|
||||
|
||||
```sh
|
||||
herdr agent list # what is running, and is it idle, working or blocked
|
||||
herdr agent start reviewer --kind claude --pane w1:p2
|
||||
herdr agent prompt reviewer "review the last commit" --wait
|
||||
herdr agent read reviewer # read the agent's terminal output
|
||||
herdr agent wait reviewer --until idle
|
||||
herdr agent attach reviewer # jump straight into that terminal
|
||||
```
|
||||
|
||||
### Work across machines
|
||||
|
||||
```sh
|
||||
herdr --remote user@build-box # attach through SSH with your local keybindings
|
||||
```
|
||||
|
||||
### Keep the server healthy
|
||||
|
||||
```sh
|
||||
herdr config check # validate config.toml
|
||||
herdr server reload-config # apply config changes without restarting panes
|
||||
herdr server stop
|
||||
herdr channel set preview # switch between stable and preview releases
|
||||
herdr integration status # which agent CLIs have herdr hooks installed
|
||||
```
|
||||
|
||||
See the [CLI reference](https://herdr.dev/docs/cli-reference/) for every command and flag.
|
||||
|
||||
## Aliases
|
||||
|
||||
| Alias | Command |
|
||||
| ------- | ----------------------------- |
|
||||
| `hrdr` | `herdr` |
|
||||
| `hrdrst` | `herdr status` |
|
||||
| `hrdrup` | `herdr update` |
|
||||
| `hrdrsl` | `herdr session list` |
|
||||
| `hrdrsa` | `herdr session attach` |
|
||||
| `hrdrr` | `herdr --remote` |
|
||||
| `hrdral` | `herdr agent list` |
|
||||
| `hrdrwl` | `herdr workspace list` |
|
||||
| `hrdrwc` | `herdr workspace create` |
|
||||
| `hrdrwt` | `herdr worktree create` |
|
||||
| `hrdrps` | `herdr pane split` |
|
||||
| `hrdrrc` | `herdr server reload-config` |
|
||||
|
||||
Tab completion works through the aliases too, so `hrdr agent <TAB>` completes the same
|
||||
way `herdr agent <TAB>` does.
|
||||
|
||||
## Session picker
|
||||
|
||||
`hrdrs` lists your herdr sessions and attaches to the one you pick. It uses
|
||||
[fzf](https://github.com/junegunn/fzf) when it is installed and falls back to a numbered
|
||||
menu otherwise, so it works everywhere.
|
||||
|
||||
```
|
||||
$ hrdrs
|
||||
name status directory
|
||||
1) default running /Users/me/.config/herdr
|
||||
2) work stopped /Users/me/projects/app
|
||||
Attach to session: 2
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
This plugin provides the `herdr_prompt_info` function to show the current pane ID in
|
||||
your prompt when the shell is running inside herdr. It reads environment variables only,
|
||||
so it adds no cost to prompt rendering.
|
||||
|
||||
For example:
|
||||
```
|
||||
PROMPT="%~$ "
|
||||
RPROMPT='$(herdr_prompt_info)'
|
||||
```
|
||||
changes your prompt inside a herdr pane to:
|
||||
```
|
||||
~/projects/app$ ▋ w1:p4
|
||||
```
|
||||
and shows nothing outside herdr.
|
||||
|
||||
Wrap it with `ZSH_THEME_HERDR_PROMPT_PREFIX` and `ZSH_THEME_HERDR_PROMPT_SUFFIX`, for
|
||||
example:
|
||||
```
|
||||
ZSH_THEME_HERDR_PROMPT_PREFIX="%{$fg[cyan]%}[herdr "
|
||||
ZSH_THEME_HERDR_PROMPT_SUFFIX="]%{$reset_color%}"
|
||||
```
|
||||
|
||||
## Cache
|
||||
|
||||
This plugin caches the completion script and automatically updates it when the plugin is
|
||||
loaded, which is usually when you start a new terminal emulator.
|
||||
|
||||
The cache is stored at `$ZSH_CACHE_DIR/completions/_herdr`.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Aliases, helpers and completion for herdr (https://herdr.dev), the runtime for coding agents.
|
||||
|
||||
# PROMPT
|
||||
# Defined before the herdr check so themes can reference it on machines without herdr.
|
||||
function herdr_prompt_info {
|
||||
[[ "$HERDR_ENV" == 1 ]] || return
|
||||
echo "${ZSH_THEME_HERDR_PROMPT_PREFIX}${HERDR_PANE_ID:gs/%/%%}${ZSH_THEME_HERDR_PROMPT_SUFFIX}"
|
||||
}
|
||||
|
||||
if (( ! $+commands[herdr] )); then
|
||||
return
|
||||
fi
|
||||
|
||||
# ALIASES
|
||||
alias hrdr='herdr'
|
||||
alias hrdrst='herdr status'
|
||||
alias hrdrup='herdr update'
|
||||
alias hrdrsl='herdr session list'
|
||||
alias hrdrsa='herdr session attach'
|
||||
alias hrdrr='herdr --remote'
|
||||
alias hrdral='herdr agent list'
|
||||
alias hrdrwl='herdr workspace list'
|
||||
alias hrdrwc='herdr workspace create'
|
||||
alias hrdrwt='herdr worktree create'
|
||||
alias hrdrps='herdr pane split'
|
||||
alias hrdrrc='herdr server reload-config'
|
||||
|
||||
# FUNCTIONS
|
||||
# Pick a session and attach to it, with fzf if available or a numbered menu otherwise.
|
||||
function hrdrs {
|
||||
setopt localoptions extendedglob
|
||||
local -a lines
|
||||
lines=("${(@f)$(herdr session list 2>/dev/null)}")
|
||||
lines=("${(@)lines%%[[:space:]]#[^[:space:]]#}") # drop the socket column
|
||||
if (( $#lines < 2 )); then
|
||||
print "hrdrs: no herdr sessions found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local choice
|
||||
if (( $+commands[fzf] )); then
|
||||
choice=$(print -l -- "${lines[@]}" | fzf --header-lines=1 --prompt='herdr session> ') || return
|
||||
else
|
||||
print -- "${lines[1]}"
|
||||
local PS3="Attach to session: "
|
||||
select choice in "${lines[@]:1}"; do
|
||||
[[ -n "$choice" ]] && break
|
||||
done
|
||||
fi
|
||||
[[ -n "$choice" ]] || return 1
|
||||
|
||||
herdr session attach "${${(z)choice}[1]}"
|
||||
}
|
||||
|
||||
# COMPLETION
|
||||
# If the completion file doesn't exist yet, we need to autoload it and
|
||||
# bind it to `herdr`. Otherwise, compinit will have already done that.
|
||||
if [[ ! -f "$ZSH_CACHE_DIR/completions/_herdr" ]]; then
|
||||
typeset -g -A _comps
|
||||
autoload -Uz _herdr
|
||||
_comps[herdr]=_herdr
|
||||
fi
|
||||
|
||||
zmodload -F zsh/files b:zf_mv
|
||||
() {
|
||||
local TMPPREFIX="$ZSH_CACHE_DIR/completions/_herdr"
|
||||
zf_mv -f -- =( herdr completion zsh < /dev/null 2> /dev/null ) "$TMPPREFIX"
|
||||
} &|
|
||||
@@ -55,7 +55,7 @@ plugins=(... jj)
|
||||
| jjopl | `jj op log` |
|
||||
| jjor | `jj op restore` |
|
||||
| jjrb | `jj rebase` |
|
||||
| jjrbm | `jj rebase -o "trunk()"` |
|
||||
| jjrbm | `jj rebase -d "trunk()"` |
|
||||
| jjrs | `jj restore` |
|
||||
| jjrt | `cd "$(jj root \|\| echo .)"` |
|
||||
| jjs | `jj show` |
|
||||
@@ -120,10 +120,10 @@ that.
|
||||
|
||||
### Git async-prompt compatibility
|
||||
|
||||
If you use a wrapper function that calls `git_prompt_info` (as shown above), it won't work with the default
|
||||
git async-prompt mode. This is because async-prompt only registers its background worker when it detects
|
||||
`$(git_prompt_info)` literally in your prompt variables. A wrapper like `$(_my_theme_vcs_info)` won't match,
|
||||
so the async output stays empty.
|
||||
If you use a wrapper function that calls `git_prompt_info` (as shown above), it won't work with
|
||||
the default git async-prompt mode. This is because async-prompt only registers its background worker
|
||||
when it detects `$(git_prompt_info)` literally in your prompt variables. A wrapper like
|
||||
`$(_my_theme_vcs_info)` won't match, so the async output stays empty.
|
||||
|
||||
To fix this, add one of the following to your `.zshrc` **before** Oh My Zsh is sourced:
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ alias jjop='jj op'
|
||||
alias jjopl='jj op log'
|
||||
alias jjor='jj op restore'
|
||||
alias jjrb='jj rebase'
|
||||
alias jjrbm='jj rebase -o "trunk()"'
|
||||
alias jjrbm='jj rebase -d "trunk()"'
|
||||
alias jjrs='jj restore'
|
||||
alias jjrt='cd "$(jj root || echo .)"'
|
||||
alias jjs='jj show'
|
||||
|
||||
@@ -23,7 +23,6 @@ plugins=(... kubectl)
|
||||
| kcsc | `kubectl config set-context` | Set a context entry in kubeconfig |
|
||||
| kcdc | `kubectl config delete-context` | Delete the specified context from the kubeconfig |
|
||||
| kccc | `kubectl config current-context` | Display the current-context |
|
||||
| kcrc | `kubectl config rename-context` | Rename a context from the kubeconfig file |
|
||||
| kcgc | `kubectl config get-contexts` | List of contexts available |
|
||||
| | | **General aliases** |
|
||||
| kdel | `kubectl delete` | Delete resources by filenames, stdin, resources and names, or by resources and label selector |
|
||||
@@ -131,7 +130,7 @@ plugins=(... kubectl)
|
||||
| kdss | `kubectl describe statefulset` | Describe statefulset resource in detail |
|
||||
| kdelss | `kubectl delete statefulset` | Delete the statefulset |
|
||||
| ksss | `kubectl scale statefulset` | Scale a statefulset |
|
||||
| krsss | `kubectl rollout status statefulset` | Check the rollout status of a statefulset |
|
||||
| krsss | `kubectl rollout status statefulset` | Check the rollout status of a deployment |
|
||||
| krrss | `kubectl rollout restart statefulset` | Rollout restart a statefulset |
|
||||
| | | **Service Accounts management** |
|
||||
| kdsa | `kubectl describe sa` | Describe a service account in details |
|
||||
|
||||
@@ -36,7 +36,6 @@ alias kcuc='kubectl config use-context'
|
||||
alias kcsc='kubectl config set-context'
|
||||
alias kcdc='kubectl config delete-context'
|
||||
alias kccc='kubectl config current-context'
|
||||
alias kcrc='kubectl config rename-context'
|
||||
|
||||
# List all contexts
|
||||
alias kcgc='kubectl config get-contexts'
|
||||
|
||||
@@ -139,6 +139,7 @@ function rdsl() { _rails_db_command db:schema:load "$@" }
|
||||
|
||||
# legacy stuff
|
||||
alias sc='ruby script/console'
|
||||
alias sd='ruby script/destroy'
|
||||
alias sd='ruby script/server --debugger'
|
||||
alias sg='ruby script/generate'
|
||||
alias sp='ruby script/plugin'
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
# Load bracketed-paste-magic if zsh version is >= 5.1
|
||||
autoload -Uz is-at-least
|
||||
if is-at-least 5.1; then
|
||||
set zle_bracketed_paste # Explicitly restore this zsh default
|
||||
autoload -Uz bracketed-paste-magic
|
||||
zle -N bracketed-paste bracketed-paste-magic
|
||||
return ### The rest of this file is NOT executed on zsh version >= 5.1 ###
|
||||
|
||||
@@ -21,18 +21,6 @@ To enable **agent forwarding support** add the following to your zshrc file:
|
||||
zstyle :omz:plugins:ssh-agent agent-forwarding yes
|
||||
```
|
||||
|
||||
### `honor-existing`
|
||||
|
||||
If your session already provides an ssh-agent — a desktop keyring, `gpg-agent`,
|
||||
KeePassXC, or an agent forwarded over SSH — the plugin normally ignores it and
|
||||
starts one of its own, which leaves you with two agents. Set this to reuse the
|
||||
running agent instead, and only start a new one when `$SSH_AUTH_SOCK` is empty
|
||||
or does not answer:
|
||||
|
||||
```zsh
|
||||
zstyle :omz:plugins:ssh-agent honor-existing yes
|
||||
```
|
||||
|
||||
### `helper`
|
||||
|
||||
To set an **external helper** to ask for the passwords and possibly store
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
# Get the filename to store/lookup the environment from
|
||||
ssh_env_cache="$HOME/.ssh/environment-$SHORT_HOST"
|
||||
|
||||
# Test if $SSH_AUTH_SOCK points at a listening agent
|
||||
function _is_agent_running() {
|
||||
local REPLY
|
||||
[[ -n "$SSH_AUTH_SOCK" && -S "$SSH_AUTH_SOCK" ]] || return 1
|
||||
zmodload zsh/net/socket 2>/dev/null || return 1
|
||||
zsocket "$SSH_AUTH_SOCK" 2>/dev/null || return 1
|
||||
exec {REPLY}>&- # close the probe connection
|
||||
return 0
|
||||
}
|
||||
|
||||
function _start_agent() {
|
||||
# Reuse an agent the session already provides, if enabled
|
||||
if zstyle -t :omz:plugins:ssh-agent honor-existing && _is_agent_running; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if ssh-agent is already running
|
||||
if [[ -f "$ssh_env_cache" ]]; then
|
||||
. "$ssh_env_cache" > /dev/null
|
||||
|
||||
# Test if $SSH_AUTH_SOCK is visible
|
||||
_is_agent_running && return 0
|
||||
zmodload zsh/net/socket
|
||||
if [[ -S "$SSH_AUTH_SOCK" ]] && zsocket "$SSH_AUTH_SOCK" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -d "$HOME/.ssh" ]]; then
|
||||
@@ -136,4 +124,4 @@ if ! zstyle -t :omz:plugins:ssh-agent lazy; then
|
||||
fi
|
||||
|
||||
unset agent_forwarding ssh_env_cache
|
||||
unfunction _start_agent _add_identities _is_agent_running
|
||||
unfunction _start_agent _add_identities
|
||||
|
||||
@@ -71,6 +71,7 @@ Related: [#9798](https://github.com/ohmyzsh/ohmyzsh/pull/9798).
|
||||
| Alias | Commands | Description |
|
||||
| ----- | ------------------- | ---------------------------------------- |
|
||||
| zar | `sudo zypper ar` | add a repository |
|
||||
| zcl | `sudo zypper clean` | clean cache |
|
||||
| zlr | `zypper lr` | list repositories |
|
||||
| zmr | `sudo zypper mr` | modify repositories |
|
||||
| znr | `sudo zypper nr` | rename repositories (for the alias only) |
|
||||
|
||||
@@ -38,6 +38,7 @@ alias zwp='zypper --no-refresh wp'
|
||||
|
||||
#Repositories commands
|
||||
alias zar='sudo zypper ar'
|
||||
alias zcl='sudo zypper clean'
|
||||
alias zlr='zypper lr'
|
||||
alias zmr='sudo zypper mr'
|
||||
alias znr='sudo zypper nr'
|
||||
|
||||
@@ -45,7 +45,6 @@ plugins=(... systemd)
|
||||
| `sc-set-environment` | `sudo systemctl set-environment` | Set one or more systemd manager environment variables |
|
||||
| `sc-unset-environment` | `sudo systemctl unset-environment` | Unset one or more systemd manager environment variables |
|
||||
| `sc-edit` | `sudo systemctl edit` | Edit a drop-in snippet or a whole replacement file with `--full` |
|
||||
| `sc-edit-full` | `sudo systemctl edit --full` | Edit the whole unit file instead of a drop-in snippet |
|
||||
| `sc-enable-now` | `sudo systemctl enable --now` | Enable and start unit(s) |
|
||||
| `sc-disable-now` | `sudo systemctl disable --now` | Disable and stop unit(s) |
|
||||
| `sc-mask-now` | `sudo systemctl mask --now` | Mask and stop unit(s) |
|
||||
|
||||
@@ -97,10 +97,6 @@ alias scu-mask-now="scu-mask --now"
|
||||
alias scu-failed='systemctl --user --failed'
|
||||
alias sc-failed='systemctl --failed'
|
||||
|
||||
# --full commands
|
||||
alias sc-edit-full="sc-edit --full"
|
||||
alias scu-edit-full="scu-edit --full"
|
||||
|
||||
function systemd_prompt_info {
|
||||
local unit
|
||||
for unit in "$@"; do
|
||||
|
||||
@@ -314,11 +314,8 @@ prompt_dir() {
|
||||
|
||||
# Virtualenv: current working virtualenv
|
||||
prompt_virtualenv() {
|
||||
# Skip the conda segment when conda is already showing the environment itself,
|
||||
# i.e. when changeps1 is left on. This mirrors how VIRTUAL_ENV_DISABLE_PROMPT
|
||||
# is handled below, and stops the name appearing twice in the prompt.
|
||||
if [[ -n "$CONDA_DEFAULT_ENV" && -z "$CONDA_PROMPT_MODIFIER" ]]; then
|
||||
prompt_segment magenta $CURRENT_FG "🐍 ${CONDA_DEFAULT_ENV:t:gs/%/%%}"
|
||||
if [ -n "$CONDA_DEFAULT_ENV" ]; then
|
||||
prompt_segment magenta $CURRENT_FG "🐍 $CONDA_DEFAULT_ENV"
|
||||
fi
|
||||
if [[ -n "$VIRTUAL_ENV" && -n "$VIRTUAL_ENV_DISABLE_PROMPT" ]]; then
|
||||
prompt_segment "$AGNOSTER_VENV_BG" "$AGNOSTER_VENV_FG" "(${VIRTUAL_ENV:t:gs/%/%%})"
|
||||
|
||||
@@ -14,7 +14,7 @@ function theme_precmd {
|
||||
if (( promptsize + rubypromptsize + pwdsize + venvpromptsize + condapromptsize > TERMWIDTH )); then
|
||||
(( PR_PWDLEN = TERMWIDTH - promptsize ))
|
||||
elif [[ "${langinfo[CODESET]}" = UTF-8 ]]; then
|
||||
PR_FILLBAR="\${(l:$(( TERMWIDTH - (promptsize + rubypromptsize + pwdsize + venvpromptsize + condapromptsize ) ))::─:)}"
|
||||
PR_FILLBAR="\${(l:$(( TERMWIDTH - (promptsize + rubypromptsize + pwdsize + venvpromptsize + condapromptsize ) ))::${PR_HBAR}:)}"
|
||||
else
|
||||
PR_FILLBAR="${PR_SHIFT_IN}\${(l:$(( TERMWIDTH - (promptsize + rubypromptsize + pwdsize + venvpromptsize + condapromptsize ) ))::${altchar[q]:--}:)}${PR_SHIFT_OUT}"
|
||||
fi
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,170 @@
|
||||
# Loaded only by `omz theme browse`. The UI runs in a subshell; stdout is a
|
||||
# two-line data handoff, while all terminal I/O goes through a private tty fd.
|
||||
function _omz_theme_browser() (
|
||||
emulate -L zsh
|
||||
setopt extendedglob promptpercent
|
||||
# Displayed names, filter text, and worker output are data, not prompt code.
|
||||
unsetopt promptsubst
|
||||
zmodload zsh/terminfo && zmodload zsh/zpty && zmodload zsh/system && zmodload zsh/datetime || return 1
|
||||
local tty saved filter=$1 key suffix name previous='' preview='' chunk
|
||||
local mode=browse action='' pty=omz-browser-$sysparams[pid] record pgid
|
||||
local -a names matches lines size
|
||||
local -i selected=1 page=1 height=0 width=0 rows=0 cols=0 active=0 dirty=1 i row cancelled=0
|
||||
local -F render_after=0
|
||||
[[ -n $terminfo[smcup] && -n $terminfo[rmcup] && -n $terminfo[cup] ]] || {
|
||||
print -u2 -r -- 'Theme browser requires a terminal with alternate-screen support.'
|
||||
return 1
|
||||
}
|
||||
exec {tty}<>/dev/tty || return 1
|
||||
saved=$(command stty -g <&$tty) || return 1
|
||||
names=("${(@f)$(_omz_theme_names)}")
|
||||
names=("${(@)names:#}")
|
||||
trap 'cancelled=1' INT TERM HUP
|
||||
trap 'dirty=1' WINCH
|
||||
|
||||
function _omz_browser_stop {
|
||||
if (( active )); then
|
||||
[[ $pgid == <2-> ]] && kill -HUP -- -$pgid 2>/dev/null
|
||||
# Let the preview supervisor clean up its nested worker group first.
|
||||
local -i wait_count=0
|
||||
while zpty -t "$pty" && (( wait_count++ < 100 )); do
|
||||
zselect -t 1
|
||||
done
|
||||
zpty -d "$pty" 2>/dev/null
|
||||
active=0
|
||||
fi
|
||||
}
|
||||
|
||||
function _omz_browser_line {
|
||||
local text=${1//$'\t'/ } formatted='' MATCH MBEGIN MEND
|
||||
text=${text//\%/%%}
|
||||
# Tell prompt truncation that SGR has zero display width. All other terminal
|
||||
# controls have already been removed by the preview backend.
|
||||
while [[ $text =~ $'\e\\[[0-9;:]*m' ]]; do
|
||||
formatted+=${text[1,$(( MBEGIN - 1 ))]}"%{"$MATCH"%}"
|
||||
text=${text[$(( MEND + 1 )),-1]}
|
||||
done
|
||||
formatted+=$text
|
||||
echoti cup $row 0 >&$tty
|
||||
print -Prn -u $tty -- "%${width}<..<${formatted}%<<%f%k%b"
|
||||
(( row++ ))
|
||||
}
|
||||
|
||||
{
|
||||
zmodload zsh/zselect || return 1
|
||||
command stty -echo -icanon min 0 time 0 <&$tty || return 1
|
||||
print -rn -u $tty -- "$terminfo[smcup]$terminfo[civis]"
|
||||
while (( ! cancelled )); do
|
||||
size=(${=$(command stty size <&$tty)})
|
||||
rows=${size[1]:-24} cols=${size[2]:-80}
|
||||
if (( height != rows || width != cols - 1 )); then
|
||||
height=$rows width=$(( cols - 1 )) dirty=1
|
||||
fi
|
||||
matches=()
|
||||
for name in "${names[@]}"; do
|
||||
[[ ${(L)name} == *${(L)filter}* ]] && matches+=("$name")
|
||||
done
|
||||
(( selected > $#matches )) && selected=$#matches
|
||||
(( selected < 1 )) && selected=1
|
||||
name=${matches[$selected]}
|
||||
if [[ $name != "$previous" ]]; then
|
||||
_omz_browser_stop
|
||||
previous=$name preview='' dirty=1
|
||||
render_after=$(( EPOCHREALTIME + 0.12 ))
|
||||
fi
|
||||
# Coalesce typing and key repeats rather than starting a worker per key.
|
||||
if (( ! active && EPOCHREALTIME >= render_after )); then
|
||||
if [[ -n $name ]]; then
|
||||
# zpty joins arguments as shell text; quote the literal theme name.
|
||||
zpty -b "$pty" _omz_theme_preview "${(q)name}" || return 1
|
||||
active=1
|
||||
pgid=''
|
||||
for record in "${(@f)$(zpty)}"; do
|
||||
[[ $record == \(<->\)\ "$pty":* ]] && pgid=${${record#\(}%%\)*}
|
||||
done
|
||||
fi
|
||||
fi
|
||||
if (( active )); then
|
||||
while zpty -r -t "$pty" chunk; do
|
||||
preview+=${chunk//$'\r'/}
|
||||
dirty=1
|
||||
done
|
||||
fi
|
||||
page=$(( height > 24 ? 8 : 4 ))
|
||||
if (( dirty )); then
|
||||
print -rn -u $tty -- "$terminfo[clear]"
|
||||
row=0
|
||||
if (( width < 39 || height < 16 )); then
|
||||
_omz_browser_line 'Resize to at least 40 columns x 16 rows.'
|
||||
_omz_browser_line 'Esc / Ctrl-C: cancel'
|
||||
else
|
||||
_omz_browser_line 'Oh My Zsh theme browser | Esc / Ctrl-C: cancel'
|
||||
_omz_browser_line "Filter: ${(V)filter} ($#matches themes)"
|
||||
if [[ $mode == actions ]]; then
|
||||
_omz_browser_line '[u] Use in session [s] Save + reload [b] Back'
|
||||
_omz_browser_line 'Use keeps old theme hooks. Save edits .zshrc + reloads.'
|
||||
else
|
||||
_omz_browser_line 'Arrows / Ctrl-P,N: move | PgUp,Dn | Enter: actions'
|
||||
_omz_browser_line 'Type to filter | Backspace: delete | Ctrl-U: clear'
|
||||
fi
|
||||
for (( i = ((selected - 1) / page) * page + 1; i <= $#matches && row < page + 4; i++ )); do
|
||||
if (( i == selected )); then
|
||||
_omz_browser_line "> ${(V)matches[$i]}"
|
||||
else
|
||||
_omz_browser_line " ${(V)matches[$i]}"
|
||||
fi
|
||||
done
|
||||
_omz_browser_line '--- Static preview (right prompt shown separately) ---'
|
||||
lines=("${(@f)preview}")
|
||||
if [[ -z $preview ]]; then
|
||||
[[ -n $name ]] && lines=('Rendering...') || lines=('No matching themes.')
|
||||
fi
|
||||
for chunk in "${lines[@]}"; do
|
||||
(( row < height - 1 )) || break
|
||||
_omz_browser_line "$chunk"
|
||||
done
|
||||
fi
|
||||
dirty=0
|
||||
fi
|
||||
key=''
|
||||
read -r -k 1 -t 0.1 -u $tty key || continue
|
||||
if [[ $key == $'\e' ]]; then
|
||||
suffix=''
|
||||
while read -r -k 1 -t 0.03 -u $tty chunk; do
|
||||
suffix+=$chunk
|
||||
[[ $chunk == [A-Za-z~] || ${#suffix} -ge 8 ]] && break
|
||||
done
|
||||
[[ -n $suffix ]] || break
|
||||
key=$'\e'$suffix
|
||||
fi
|
||||
[[ $key == $'\x03' ]] && break
|
||||
if [[ $mode == actions ]]; then
|
||||
case $key in
|
||||
u) action=use; break ;;
|
||||
s) action=set; break ;;
|
||||
b|$'\r'|$'\n') mode=browse ;;
|
||||
esac
|
||||
else
|
||||
case $key in
|
||||
$'\e[A'|$'\eOA'|$'\x10') (( selected-- )) ;;
|
||||
$'\e[B'|$'\eOB'|$'\x0e') (( selected++ )) ;;
|
||||
$'\e[5~') (( selected -= page )) ;;
|
||||
$'\e[6~') (( selected += page )) ;;
|
||||
$'\x7f'|$'\b') filter=${filter[1,-2]}; selected=1 ;;
|
||||
$'\x15') filter=''; selected=1 ;;
|
||||
$'\r'|$'\n') [[ -n $name ]] && mode=actions ;;
|
||||
[[:print:]]) filter+=$key; selected=1 ;;
|
||||
esac
|
||||
fi
|
||||
dirty=1
|
||||
done
|
||||
} always {
|
||||
_omz_browser_stop
|
||||
print -rn -u $tty -- $'\e[0m'"$terminfo[cnorm]$terminfo[rmcup]"
|
||||
command stty "$saved" <&$tty
|
||||
exec {tty}>&-
|
||||
}
|
||||
(( cancelled )) && return 130
|
||||
[[ -n $action ]] && print -rl -- "$action" "$name"
|
||||
return 0
|
||||
)
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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
|
||||
# This process is the private PTY's session leader. Keep the actual worker's
|
||||
# standard descriptors non-TTY, and report its status outside theme state.
|
||||
typeset -r _omz_preview_tmp=$2
|
||||
shift 2
|
||||
unsetopt monitor
|
||||
command "$commands[zsh]" -df "${0:A}" "$@" \
|
||||
< /dev/null > "$_omz_preview_tmp/output" 2>&1
|
||||
print -r -- $? > "$_omz_preview_tmp/result"
|
||||
exit
|
||||
fi
|
||||
typeset -r _omz_preview_root=$1 _omz_preview_theme=$2 _omz_preview_status=$3
|
||||
export ZSH=${ZSH:-$_omz_preview_root}
|
||||
ZSH_THEME=$4
|
||||
ZSH_CUSTOM=$5
|
||||
fpath=("$_omz_preview_root/functions" $fpath)
|
||||
autoload -Uz colors add-zsh-hook vcs_info
|
||||
colors
|
||||
setopt prompt_subst prompt_percent
|
||||
zstyle ':omz:alpha:lib:git' async-prompt no
|
||||
ZSH_THEME_GIT_PROMPT_PREFIX='git:('
|
||||
ZSH_THEME_GIT_PROMPT_SUFFIX=')'
|
||||
ZSH_THEME_GIT_PROMPT_DIRTY='*'
|
||||
ZSH_THEME_GIT_PROMPT_CLEAN=''
|
||||
ZSH_THEME_RUBY_PROMPT_PREFIX='('
|
||||
ZSH_THEME_RUBY_PROMPT_SUFFIX=')'
|
||||
source "$_omz_preview_root/lib/git.zsh"
|
||||
source "$_omz_preview_root/lib/vcs_info.zsh"
|
||||
source "$_omz_preview_root/lib/bzr.zsh"
|
||||
source "$_omz_preview_root/lib/nvm.zsh"
|
||||
source "$_omz_preview_root/lib/prompt_info_functions.zsh"
|
||||
source "$_omz_preview_root/lib/spectrum.zsh"
|
||||
# Check syntax before accepting a final false conditional as a successful load.
|
||||
# This subprocess shares the worker's execution deadline and process group.
|
||||
command "$commands[zsh]" -dfn "$_omz_preview_theme" || {
|
||||
print -u2 -r -- 'theme preview: theme syntax check failed'
|
||||
exit 1
|
||||
}
|
||||
# Keep zsh's PROMPT/PS1 and RPROMPT/RPS1 aliases intact (dieter sets RPS1).
|
||||
PROMPT=''
|
||||
RPROMPT=''
|
||||
source "$_omz_preview_theme"
|
||||
typeset -i _omz_preview_load_status=$?
|
||||
# Status 1 may just be a final conditional; require an initialized prompt below.
|
||||
# Higher statuses include syntax/runtime errors and missing commands.
|
||||
if (( _omz_preview_load_status > 1 )); then
|
||||
print -u2 -r -- "theme preview: theme could not be loaded (exit $_omz_preview_load_status)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function _omz_preview_set_status { return "$_omz_preview_status" }
|
||||
typeset -i _omz_preview_hook_status=0
|
||||
if (( $+functions[precmd] )); then
|
||||
_omz_preview_set_status
|
||||
precmd
|
||||
_omz_preview_hook_status=$?
|
||||
if (( _omz_preview_hook_status )); then
|
||||
print -u2 -r -- "theme preview: precmd failed (exit $_omz_preview_hook_status); remaining hooks skipped"
|
||||
fi
|
||||
fi
|
||||
for _omz_preview_hook in "${precmd_functions[@]}"; do
|
||||
(( _omz_preview_hook_status )) && break
|
||||
if (( $+functions[$_omz_preview_hook] )); then
|
||||
_omz_preview_set_status
|
||||
"$_omz_preview_hook"
|
||||
_omz_preview_hook_status=$?
|
||||
if (( _omz_preview_hook_status )); then
|
||||
print -u2 -r -- "theme preview: precmd hook $_omz_preview_hook failed (exit $_omz_preview_hook_status); remaining hooks skipped"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if (( _omz_preview_load_status )) && [[ -z $PROMPT && -z $RPROMPT ]]; then
|
||||
print -u2 -r -- "theme preview: theme did not initialize a prompt (load exit $_omz_preview_load_status)"
|
||||
exit 1
|
||||
fi
|
||||
# Native prompt expansion handles PROMPT_SUBST once, followed by percent escapes.
|
||||
# An explicit (e) pass here would re-execute literal substitutions from helpers.
|
||||
print -r -- "Left prompt (status $_omz_preview_status):"
|
||||
_omz_preview_set_status
|
||||
print -Pr -- "${PROMPT-}"
|
||||
print -r -- 'Right prompt (shown separately):'
|
||||
_omz_preview_set_status
|
||||
print -Pr -- "${RPROMPT-}"
|
||||
(( _omz_preview_hook_status )) && exit 1
|
||||
exit 42
|
||||
@@ -0,0 +1,217 @@
|
||||
# Static previews execute theme code as the current user, not in a sandbox.
|
||||
# A private PTY supplies a process group without requiring shell job control.
|
||||
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:h}}
|
||||
local custom=${ZSH_CUSTOM:-$root/custom} dir file name
|
||||
local -a names files
|
||||
for dir in "$custom" "$root/themes"; do
|
||||
if [[ $dir == "$custom" ]]; then
|
||||
files=("$dir"/**/*.zsh-theme(N-.))
|
||||
else
|
||||
files=("$dir"/*.zsh-theme(N-.))
|
||||
fi
|
||||
for file in "${files[@]}"; do
|
||||
name=${${file#"$dir/"}%.zsh-theme}
|
||||
[[ $dir == "$custom" ]] && name=${name#themes/}
|
||||
[[ -n $name && $name != random && /$name/ != */(.|..)/* && $name != /* && $name != *[[:cntrl:]\\]* ]] || continue
|
||||
names+=("$name")
|
||||
done
|
||||
done
|
||||
(( $#names )) && print -rl -- ${(ou)names}
|
||||
return 0
|
||||
)
|
||||
|
||||
function _omz_theme_resolve() (
|
||||
emulate -L zsh
|
||||
setopt extendedglob
|
||||
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'
|
||||
return 2
|
||||
fi
|
||||
for dir in "$custom" "$custom/themes" "$root/themes"; do
|
||||
if [[ -f "$dir/$name.zsh-theme" ]]; then
|
||||
print -r -- "${dir:A}/$name.zsh-theme"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
print -u2 -r -- 'theme preview: theme not found'
|
||||
return 1
|
||||
)
|
||||
|
||||
# One sample, with the requested exit status (0 by default). All execution and
|
||||
# state changes are confined to this subshell and the fresh -df worker.
|
||||
# For asynchronous callers, cancel the job group, not just zsh's waiting wrapper.
|
||||
function _omz_theme_preview() (
|
||||
emulate -L zsh
|
||||
local theme sample=${2:-0} backend=$_OMZ_THEME_PREVIEW_DIR
|
||||
if (( $# < 1 || $# > 2 )) || [[ $sample != <0-255> ]]; then
|
||||
print -u2 -r -- 'theme preview: usage: _omz_theme_preview name [status 0..255]'
|
||||
return 2
|
||||
fi
|
||||
theme=$(_omz_theme_resolve "$1") || return $?
|
||||
if ! zmodload zsh/zpty || ! zmodload zsh/system || ! zmodload zsh/datetime || ! zmodload zsh/zselect; then
|
||||
print -u2 -r -- 'theme preview: required zsh modules unavailable (zpty, system, datetime, zselect)'
|
||||
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:h}}/custom}
|
||||
local -i active=0 cancelled=0 bytes=0 count remaining result=1
|
||||
local -F deadline
|
||||
trap 'cancelled=130' INT
|
||||
trap 'cancelled=143' TERM
|
||||
trap 'cancelled=129' HUP
|
||||
{
|
||||
tmp=$(command mktemp -d "${TMPDIR:-/tmp}/omz-preview.XXXXXXXX") || return 1
|
||||
command mkfifo "$tmp/output" || return 1
|
||||
sysopen -r -o nonblock,cloexec -u fifo_fd "$tmp/output" || return 1
|
||||
|
||||
# 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/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'
|
||||
return 1
|
||||
fi
|
||||
active=1
|
||||
# The builtin listing supplies the session leader even before the child
|
||||
# starts; this avoids a PID-handshake race during immediate cancellation.
|
||||
for record in "${(@f)$(zpty)}"; do
|
||||
if [[ $record == \(<->\)\ "$pty":* ]]; then
|
||||
pgid=${${record#\(}%%\)*}
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $pgid != <2-> ]]; then
|
||||
print -u2 -r -- 'theme preview: could not identify worker process group'
|
||||
return 1
|
||||
fi
|
||||
while true; do
|
||||
if (( cancelled )); then
|
||||
error='cancelled'
|
||||
break
|
||||
fi
|
||||
if (( EPOCHREALTIME >= deadline )); then
|
||||
error='timed out after 3 seconds'
|
||||
break
|
||||
fi
|
||||
remaining=$(( 32001 - bytes ))
|
||||
if sysread -i "$fifo_fd" -s $(( remaining < 4096 ? remaining : 4096 )) -t 0.02 -c count chunk; then
|
||||
raw+=$chunk
|
||||
(( bytes += count ))
|
||||
if (( bytes > 32000 )); then
|
||||
error='output exceeded 32 KiB'
|
||||
break
|
||||
fi
|
||||
elif [[ -s "$tmp/result" ]]; then
|
||||
result=$(<"$tmp/result")
|
||||
# The worker's dedicated completion status distinguishes a theme's
|
||||
# early exit (including exit 0) from rendering both prompts.
|
||||
[[ $result == 42 ]] || error="worker did not complete preview (exit $result)"
|
||||
break
|
||||
elif ! zpty -t "$pty"; then
|
||||
error='worker supervisor exited before completing preview'
|
||||
break
|
||||
else
|
||||
zselect -t 1
|
||||
fi
|
||||
done
|
||||
} always {
|
||||
# Clean up the whole group even on success. Deliberately detached processes
|
||||
# are outside this boundary. Delete only our PTY, not any inherited ones.
|
||||
if [[ $pgid == <2-> ]]; then
|
||||
kill -TERM -- -$pgid 2>/dev/null
|
||||
zselect -t 5
|
||||
kill -KILL -- -$pgid 2>/dev/null
|
||||
fi
|
||||
(( active )) && zpty -d "$pty" 2>/dev/null
|
||||
[[ -n $fifo_fd ]] && exec {fifo_fd}<&-
|
||||
[[ -n $tmp ]] && command rm -rf -- "$tmp"
|
||||
}
|
||||
|
||||
# Bound by bytes before splitting into locale-aware characters. No escape is
|
||||
# emitted until it is a complete numeric SGR; all string controls are dropped,
|
||||
# including their payloads and incomplete sequences at the output boundary.
|
||||
unsetopt multibyte
|
||||
raw=${raw[1,32000]}
|
||||
setopt multibyte
|
||||
local char safe='' state=text sgr=''
|
||||
local -i code valid=1
|
||||
for char in "${(@s::)raw}"; do
|
||||
if (( cancelled )); then
|
||||
error='cancelled'
|
||||
break
|
||||
fi
|
||||
printf -v code '%d' "'$char" 2>/dev/null
|
||||
case $state in
|
||||
string|string-escape)
|
||||
if [[ $char == $'\a' || $char == $'\xc2\x9c' || $char == $'\x9c' || ( $state == string-escape && $char == \\ ) ]]; then
|
||||
state=text
|
||||
elif [[ $char == $'\e' ]]; then
|
||||
state=string-escape
|
||||
else
|
||||
state=string
|
||||
fi
|
||||
;;
|
||||
escape)
|
||||
case $char in
|
||||
'[') state=csi; sgr=''; valid=1 ;;
|
||||
']'|P|X|'^'|_) state=string ;;
|
||||
$'\e') ;;
|
||||
*)
|
||||
if (( code >= 32 && code <= 47 )); then
|
||||
state=intermediate
|
||||
else
|
||||
state=text
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
intermediate)
|
||||
(( code >= 32 && code <= 47 )) || state=text
|
||||
;;
|
||||
csi)
|
||||
if [[ $char == [0-9\;:] ]]; then
|
||||
sgr+=$char
|
||||
elif (( code >= 64 && code <= 126 )); then
|
||||
[[ $char == m && $valid == 1 ]] && safe+=$'\e['${sgr}m
|
||||
state=text
|
||||
elif [[ $char == $'\e' ]]; then
|
||||
state=escape
|
||||
else
|
||||
valid=0
|
||||
fi
|
||||
;;
|
||||
text)
|
||||
case $char in
|
||||
$'\e') state=escape ;;
|
||||
$'\xc2\x9b') state=csi; sgr=''; valid=1 ;;
|
||||
# Drop raw 8-bit CSI instead of expanding it to two bytes. Filtering
|
||||
# can then never enlarge the byte-bounded input buffer.
|
||||
$'\x9b') state=csi; sgr=''; valid=0 ;;
|
||||
$'\xc2\x90'|$'\xc2\x98'|$'\xc2\x9d'|$'\xc2\x9e'|$'\xc2\x9f'|$'\x90'|$'\x98'|$'\x9d'|$'\x9e'|$'\x9f') state=string ;;
|
||||
$'\n'|$'\t') safe+=$char ;;
|
||||
*)
|
||||
# Reject format/directional controls even on libc implementations
|
||||
# that classify them as printable. Keep private-use font glyphs.
|
||||
if [[ $char == [[:print:]] ]] && ! (( code >= 0x200b && code <= 0x200f || code >= 0x2028 && code <= 0x202e || code >= 0x2060 && code <= 0x206f || code == 0xfeff )); then
|
||||
safe+=$char
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
done
|
||||
(( cancelled )) && error='cancelled'
|
||||
print -rn -- "$safe"$'\e[0m\n'
|
||||
[[ -n $error ]] && print -u2 -r -- "theme preview: $error"
|
||||
(( cancelled )) && return $cancelled
|
||||
[[ -z $error ]]
|
||||
)
|
||||
@@ -0,0 +1,406 @@
|
||||
#!/usr/bin/env 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
|
||||
|
||||
if [[ $1 == --child ]]; then
|
||||
if [[ $2 == completion ]]; then
|
||||
function compdef { print -r -- "REGISTERED:${(j.:.)@}"; }
|
||||
fi
|
||||
source "$ZSH/lib/cli.zsh"
|
||||
# Match normal OMZ sessions without sourcing any user startup/configuration.
|
||||
setopt promptsubst
|
||||
[[ -z $BROWSER_TEST_NO_PROMPT_PERCENT ]] || unsetopt promptpercent
|
||||
print -r -- "LAZY:$+functions[_omz_theme_browser]:$+functions[_omz_theme_preview]"
|
||||
if [[ $2 == completion ]]; then
|
||||
# Capture the real completion function's candidates without loading the
|
||||
# user's completion setup or replacing any CLI/backend functions.
|
||||
function _describe { print -rl -- "DESCRIBE:$1" "${(@P)2}"; }
|
||||
CURRENT=3 words=(omz theme '')
|
||||
print -r -- SUBCOMMANDS
|
||||
_omz
|
||||
print -r -- END_SUBCOMMANDS
|
||||
for action in browse preview set use; do
|
||||
CURRENT=4 words=(omz theme "$action" '')
|
||||
print -r -- "CANDIDATES:$action"
|
||||
_omz
|
||||
print -r -- "END:$action"
|
||||
done
|
||||
print -r -- "STILL_LAZY:$+functions[_omz_theme_browser]:$+functions[_omz_theme_preview]"
|
||||
print -r -- COMPLETIONS_FINISHED
|
||||
exit
|
||||
fi
|
||||
if [[ $2 == reject* ]]; then
|
||||
case $2 in
|
||||
reject-stdin) omz theme browse </dev/null ;;
|
||||
reject-stdout) omz theme browse >/dev/null ;;
|
||||
*) omz theme browse ;;
|
||||
esac
|
||||
result=$?
|
||||
print -r -- "REJECT:$result:$+functions[_omz_theme_browser]:$+functions[_omz_theme_preview]"
|
||||
exit
|
||||
fi
|
||||
function _omz::theme::use { calls+=("use:$1"); ZSH_THEME=$1; }
|
||||
function _omz::theme::set { calls+=("set:$1"); ZSH_THEME=$1; }
|
||||
function browser_test_hook { : parent-hook; }
|
||||
function precmd { : parent-precmd; }
|
||||
typeset -a calls=() precmd_functions=(browser_test_hook) chpwd_functions=(browser_test_hook)
|
||||
ZSH_THEME=parent-theme PROMPT=parent-prompt RPROMPT=parent-rprompt
|
||||
typeset original_hook=$functions[browser_test_hook] original_precmd=$functions[precmd]
|
||||
typeset original_options=$(setopt) original_pwd=$PWD
|
||||
stty rows 24 cols 100
|
||||
# PENDIN is a transient kernel input-reprocessing flag, not a user mode.
|
||||
typeset original_tty=$(stty -a)
|
||||
original_tty=${${original_tty//-pendin/}//pendin/}
|
||||
tty > "$TEST_SCRATCH/tty"
|
||||
print -r -- READY
|
||||
# An interactive script otherwise aborts on SIGINT instead of returning to
|
||||
# the next prompt as an interactive command loop would.
|
||||
trap ':' INT
|
||||
case $BROWSER_TEST_EXIT_TRAP in
|
||||
function) function TRAPEXIT { print -r -- EXIT_HOOK; } ;;
|
||||
string) trap 'print -r -- EXIT_HOOK' EXIT ;;
|
||||
esac
|
||||
typeset original_exit_trap=${functions[TRAPEXIT]-}
|
||||
omz theme browse "$2"
|
||||
result=$?
|
||||
if [[ -n $BROWSER_TEST_EXIT_TRAP ]]; then
|
||||
[[ ${functions[TRAPEXIT]-} == "$original_exit_trap" ]] && print -r -- EXIT_FUNCTION_UNCHANGED
|
||||
trap
|
||||
trap - EXIT
|
||||
fi
|
||||
typeset restored_tty=$(stty -a)
|
||||
restored_tty=${${restored_tty//-pendin/}//pendin/}
|
||||
if [[ $restored_tty == "$original_tty" ]]; then
|
||||
print -r -- TTY_RESTORED
|
||||
else
|
||||
print -r -- "TTY_MISMATCH:before=$original_tty after=$restored_tty"
|
||||
fi
|
||||
if [[ $PROMPT == parent-prompt && $RPROMPT == parent-rprompt &&
|
||||
$functions[browser_test_hook] == "$original_hook" &&
|
||||
$functions[precmd] == "$original_precmd" &&
|
||||
${(j:,:)precmd_functions} == browser_test_hook &&
|
||||
${(j:,:)chpwd_functions} == browser_test_hook &&
|
||||
-o promptsubst && $(setopt) == "$original_options" && $PWD == "$original_pwd" &&
|
||||
${BROWSER_TEST_LEAK-unset} == unset ]]; then
|
||||
print -r -- PARENT_UNCHANGED
|
||||
fi
|
||||
print -r -- "RESULT:$result:${(j:,:)calls}:$ZSH_THEME"
|
||||
print -r -- FINISHED
|
||||
exit 0
|
||||
fi
|
||||
|
||||
setopt err_exit pipe_fail
|
||||
[[ -n $BROWSER_TEST_TRACE ]] && setopt xtrace
|
||||
zmodload zsh/zpty
|
||||
zmodload zsh/datetime
|
||||
zmodload zsh/zselect
|
||||
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
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
mkdir -p "$scratch/home" "$scratch/custom/themes/nested" "$scratch/tmp"
|
||||
export ZSH=$repo ZSH_CUSTOM=$scratch/custom HOME=$scratch/home ZDOTDIR=$scratch/home
|
||||
export TMPDIR=$scratch/tmp TEST_SCRATCH=$scratch TERM=xterm-256color
|
||||
unset ZSH_THEME
|
||||
# Even a mistakenly enabled startup-file load must never reach real dotfiles.
|
||||
print -r -- 'print STARTUP_RAN; exit 99' > "$HOME/.zshenv"
|
||||
print -r -- 'PROMPT="nested fixture"' > "$ZSH_CUSTOM/themes/nested/browser-fixture.zsh-theme"
|
||||
for number in {01..10}; do
|
||||
print -r -- 'typeset -g BROWSER_TEST_LEAK=bad
|
||||
ZSH_THEME=fixture-theme
|
||||
precmd_functions=() chpwd_functions=()
|
||||
function precmd { PROMPT="%F{red}FIXTURE_PREVIEW%f" }
|
||||
RPROMPT="fixture-right"' > "$ZSH_CUSTOM/zz-omz-test-$number.zsh-theme"
|
||||
done
|
||||
print -r -- 'sleep 30 &
|
||||
print -r -- $! > "$TEST_SCRATCH/slow-pid"
|
||||
wait
|
||||
PROMPT="SLOW_FINISHED"' > "$ZSH_CUSTOM/zz-omz-test-slow.zsh-theme"
|
||||
typeset filter_probe='$(print HIT > $TEST_SCRATCH/filter-executed)'
|
||||
typeset preview_probe='$(print HIT > $TEST_SCRATCH/preview-executed)'
|
||||
# Emit literal preview output, rather than asking the worker to evaluate a
|
||||
# prompt expression. Only a second evaluation by the browser can execute it.
|
||||
print -rl -- "print -r -- ${(q)preview_probe}" 'PROMPT="literal-preview-ready"' > "$ZSH_CUSTOM/literal-browser.zsh-theme"
|
||||
typeset backslash_probe='literal:\e[2J\n\c:end'
|
||||
print -rl -- "print -r -- ${(q)backslash_probe}" 'PROMPT="backslash-preview-ready"' > "$ZSH_CUSTOM/backslash-browser.zsh-theme"
|
||||
|
||||
typeset buffer='' transcript='' chunk='' label=''
|
||||
typeset -F browser_finished_at=0
|
||||
function cleanup_pty {
|
||||
local record group
|
||||
for record in "${(@f)$(zpty)}"; do
|
||||
[[ $record == \(<->\)\ browser:* ]] || continue
|
||||
group=${${record#\(}%%\)*}
|
||||
kill -HUP -- -$group 2>/dev/null || true
|
||||
zselect -t 20 || true
|
||||
# A renderer regression can loop without checking its cancellation flag.
|
||||
kill -KILL -- -$group 2>/dev/null || true
|
||||
done
|
||||
zpty -d 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
function fail {
|
||||
print -u2 -r -- "FAIL: $label: $1"
|
||||
print -u2 -r -- "PTY transcript, last 4000 characters (escaped): ${(V)transcript[-4000,-1]}"
|
||||
exit 1
|
||||
}
|
||||
function check {
|
||||
"$@" || fail "$1 assertion failed"
|
||||
}
|
||||
function await {
|
||||
local wanted=$1
|
||||
local -F deadline=$(( EPOCHREALTIME + ${2:-6} ))
|
||||
[[ $buffer == *"$wanted"* ]] && return 0
|
||||
while (( EPOCHREALTIME < deadline )); do
|
||||
if zpty -r browser chunk; then
|
||||
buffer+=$chunk transcript+=$chunk
|
||||
[[ $buffer == *"$wanted"* ]] && return 0
|
||||
else
|
||||
zselect -t 1 || true
|
||||
fi
|
||||
done
|
||||
fail "timed out waiting for ${(V)wanted}"
|
||||
}
|
||||
function send {
|
||||
buffer=''
|
||||
zpty -w -n browser "$1"
|
||||
}
|
||||
function start {
|
||||
label=$1 buffer='' transcript=''
|
||||
local -a launch=("$commands[zsh]" -dfi "$self" --child "$2")
|
||||
zpty -b browser exec "${(@q)launch}"
|
||||
await 'LAZY:0:0'
|
||||
await "$3"
|
||||
}
|
||||
function finish {
|
||||
await FINISHED
|
||||
# zpty -d can spend a second reaping an already-completed child. It is
|
||||
# harness teardown, not browser cancellation/terminal-restoration latency.
|
||||
browser_finished_at=$EPOCHREALTIME
|
||||
check test "${transcript#*TTY_RESTORED}" != "$transcript"
|
||||
check test "${transcript#*PARENT_UNCHANGED}" != "$transcript"
|
||||
check test "${transcript#*RESULT:$1}" != "$transcript"
|
||||
check test "${transcript#*$'\e[?1049l'}" != "$transcript"
|
||||
check test "${transcript#*$'\e[?25h'}" != "$transcript"
|
||||
check test "${transcript#*STARTUP_RAN}" = "$transcript"
|
||||
zpty -d browser
|
||||
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: PTY deletion $(( EPOCHREALTIME - browser_finished_at ))s"
|
||||
}
|
||||
|
||||
function test_completion {
|
||||
start 'completion registration, commands, theme candidates, lazy loading' completion COMPLETIONS_FINISHED
|
||||
check test "${transcript#*REGISTERED:_omz:omz}" != "$transcript"
|
||||
local section=${${transcript#*SUBCOMMANDS}%END_SUBCOMMANDS*} action
|
||||
check test "${section#*browse:Browse theme previews}" != "$section"
|
||||
check test "${section#*preview:Preview a theme without applying it}" != "$section"
|
||||
for action in browse preview set use; do
|
||||
section=${${transcript#*CANDIDATES:$action}%END:$action*}
|
||||
check test "${section#*DESCRIBE:theme}" != "$section"
|
||||
for name in robbyrussell zz-omz-test-01 nested/browser-fixture; do
|
||||
check test "${section#*$name}" != "$section"
|
||||
done
|
||||
done
|
||||
check test "${transcript#*STILL_LAZY:0:0}" != "$transcript"
|
||||
check test "${transcript#*STARTUP_RAN}" = "$transcript"
|
||||
zpty -d browser
|
||||
}
|
||||
|
||||
function test_rejection {
|
||||
# Noninteractive rejection even with both descriptors attached to a PTY.
|
||||
label=noninteractive
|
||||
typeset -a launch=("$commands[zsh]" -df "$self" --child reject)
|
||||
zpty -b browser exec "${(@q)launch}"
|
||||
await 'REJECT:1:0:0'
|
||||
check test "${transcript#*requires an interactive terminal}" != "$transcript"
|
||||
zpty -d browser
|
||||
for mode in stdin stdout; do
|
||||
start "redirected $mode" "reject-$mode" 'REJECT:1:0:0'
|
||||
check test "${transcript#*requires an interactive terminal}" != "$transcript"
|
||||
zpty -d browser
|
||||
done
|
||||
label='noninteractive and redirected-I/O rejection'
|
||||
}
|
||||
|
||||
function test_navigation {
|
||||
start 'filter, navigation, actions/back, Esc isolation' zz-omz-test- '> zz-omz-test-01'
|
||||
await FIXTURE_PREVIEW
|
||||
send $'\e[B'; await '> zz-omz-test-02'
|
||||
send $'\e[A'; await '> zz-omz-test-01'
|
||||
send $'\x0e'; await '> zz-omz-test-02'
|
||||
send $'\x10'; await '> zz-omz-test-01'
|
||||
send $'\e[6~'; await '> zz-omz-test-05'
|
||||
send $'\e[5~'; await '> zz-omz-test-01'
|
||||
send '09'; await '> zz-omz-test-09'
|
||||
send $'\x7f'; await '(9 themes)'
|
||||
send $'\x15'; await 'Filter: ('
|
||||
send 'ZZ-OMZ-TEST-03'; await '> zz-omz-test-03'
|
||||
send $'\r'; await '[u] Use in session'
|
||||
send b; await 'Enter: actions'
|
||||
send $'\x15no-such-omz-fixture'; await 'No matching themes.'
|
||||
send $'\r'; await 'No matching themes.'
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
}
|
||||
|
||||
function test_use {
|
||||
start 'explicit use dispatch' zz-omz-test-01 '> zz-omz-test-01'
|
||||
send $'\r'; await '[u] Use in session'
|
||||
send u
|
||||
finish '0:use:zz-omz-test-01:zz-omz-test-01'
|
||||
}
|
||||
|
||||
function test_promptsubst {
|
||||
start 'literal filter/preview substitutions with parent promptsubst enabled' literal-browser '> literal-browser'
|
||||
await "$preview_probe"
|
||||
await literal-preview-ready
|
||||
check test ! -e "$scratch/preview-executed"
|
||||
send $'\x15'"$filter_probe"
|
||||
await "Filter: $filter_probe"
|
||||
await 'No matching themes.'
|
||||
check test ! -e "$scratch/filter-executed"
|
||||
send $'\x15literal-browser'
|
||||
await "$preview_probe"
|
||||
await literal-preview-ready
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
check test ! -e "$scratch/filter-executed"
|
||||
check test ! -e "$scratch/preview-executed"
|
||||
}
|
||||
|
||||
function test_save {
|
||||
start 'explicit save dispatch (stubbed)' zz-omz-test-02 '> zz-omz-test-02'
|
||||
send $'\r'; await '[u] Use in session'
|
||||
send s
|
||||
finish '0:set:zz-omz-test-02:zz-omz-test-02'
|
||||
}
|
||||
|
||||
function test_backslashes {
|
||||
start 'literal backslashes in preview and filter' backslash-browser '> backslash-browser'
|
||||
await "$backslash_probe"
|
||||
await backslash-preview-ready
|
||||
send $'\x15'"$backslash_probe"
|
||||
await "Filter: $backslash_probe"
|
||||
await 'No matching themes.'
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
}
|
||||
|
||||
function test_promptpercent {
|
||||
local -x BROWSER_TEST_NO_PROMPT_PERCENT=1
|
||||
start 'renderer enables promptpercent without changing parent options' zz-omz-test-01 '> zz-omz-test-01'
|
||||
await FIXTURE_PREVIEW
|
||||
check test "${transcript#*'%<<'}" = "$transcript"
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
}
|
||||
|
||||
function test_exittrap {
|
||||
local -x BROWSER_TEST_EXIT_TRAP
|
||||
local action
|
||||
for BROWSER_TEST_EXIT_TRAP in function string; do
|
||||
for action in use set cancel; do
|
||||
start "inherited $BROWSER_TEST_EXIT_TRAP EXIT trap: $action" zz-omz-test-01 '> zz-omz-test-01'
|
||||
await FIXTURE_PREVIEW
|
||||
if [[ $action == cancel ]]; then
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
else
|
||||
send $'\r'; await '[u] Use in session'
|
||||
send "${action[1]}"
|
||||
finish "0:$action:zz-omz-test-01:zz-omz-test-01"
|
||||
fi
|
||||
check test "${transcript#*EXIT_FUNCTION_UNCHANGED}" != "$transcript"
|
||||
if [[ $BROWSER_TEST_EXIT_TRAP == string ]]; then
|
||||
check test "${transcript#*"trap -- 'print -r -- EXIT_HOOK' EXIT"}" != "$transcript"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
function test_interrupt {
|
||||
start 'Ctrl-C restores terminal' zz-omz-test-01 '> zz-omz-test-01'
|
||||
send $'\x03'
|
||||
finish '130::parent-theme'
|
||||
}
|
||||
|
||||
function test_resize {
|
||||
start 'resize small and back' zz-omz-test-01 '> zz-omz-test-01'
|
||||
typeset terminal=$(<"$scratch/tty")
|
||||
terminal=${terminal//$'\r'/}
|
||||
buffer=''
|
||||
stty rows 10 cols 30 < "$terminal"
|
||||
await '40 columns x 16 rows.'
|
||||
buffer=''
|
||||
stty rows 24 cols 100 < "$terminal"
|
||||
await '> zz-omz-test-01'
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
}
|
||||
|
||||
function test_slow {
|
||||
local cancellation=$1
|
||||
command rm -f "$scratch/slow-pid"
|
||||
start "slow preview cancellation: $cancellation" zz-omz-test-slow '> zz-omz-test-slow'
|
||||
for attempt in {1..150}; do
|
||||
[[ -s "$scratch/slow-pid" ]] && break
|
||||
zselect -t 1 || true
|
||||
done
|
||||
check test -s "$scratch/slow-pid"
|
||||
typeset -F started=$EPOCHREALTIME
|
||||
case $cancellation in
|
||||
navigation)
|
||||
send $'\x15zz-omz-test-01'
|
||||
await FIXTURE_PREVIEW 2
|
||||
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: navigation preview $(( EPOCHREALTIME - started ))s"
|
||||
(( EPOCHREALTIME - started < 2 )) || fail 'replacement preview exceeded 2 seconds'
|
||||
send $'\e'
|
||||
finish '0::parent-theme'
|
||||
;;
|
||||
escape) send $'\e'; finish '0::parent-theme' ;;
|
||||
interrupt) send $'\x03'; finish '130::parent-theme' ;;
|
||||
esac
|
||||
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: browser completed $(( browser_finished_at - started ))s; including teardown $(( EPOCHREALTIME - started ))s"
|
||||
(( browser_finished_at - started < 2 )) || fail "browser completion exceeded 2 seconds: $(( browser_finished_at - started ))s"
|
||||
typeset child=$(<"$scratch/slow-pid") child_state
|
||||
for attempt in {1..100}; do
|
||||
child_state=$(command ps -o stat= -p "$child" 2>/dev/null || true)
|
||||
[[ -z "${child_state//[ ZN+]/}" ]] && break
|
||||
zselect -t 1 || true
|
||||
done
|
||||
[[ -z $BROWSER_TEST_TIMING ]] || print -r -- "TIMING: descendant $child state=${child_state:-absent}, cleanup polls=$attempt"
|
||||
[[ -z "${child_state//[ ZN+]/}" ]] || fail "preview descendant $child still running: $child_state"
|
||||
}
|
||||
|
||||
# Keep independent regressions running after a failed UI session.
|
||||
unsetopt err_exit
|
||||
typeset -i failures=0
|
||||
for scenario in rejection completion navigation promptsubst backslashes promptpercent exittrap use save interrupt resize slow-navigation slow-escape slow-interrupt; do
|
||||
[[ -z $1 || $scenario == "$1" ]] || continue
|
||||
(
|
||||
setopt err_exit
|
||||
trap 'cleanup_pty' EXIT
|
||||
if [[ $scenario == slow-* ]]; then
|
||||
test_slow "${scenario#slow-}"
|
||||
else
|
||||
test_$scenario
|
||||
fi
|
||||
print -r -- "PASS: $label"
|
||||
)
|
||||
(( $? == 0 )) || (( failures++ ))
|
||||
done
|
||||
typeset -a leftovers
|
||||
for attempt in {1..100}; do
|
||||
leftovers=("$TMPDIR"/omz-preview.*(N))
|
||||
(( $#leftovers == 0 )) && break
|
||||
zselect -t 1 || true
|
||||
done
|
||||
if (( $#leftovers )); then
|
||||
print -u2 -r -- "FAIL: $#leftovers preview temporary directories remain"
|
||||
(( failures++ ))
|
||||
fi
|
||||
print -r -- "Theme browser regression groups failed: $failures"
|
||||
(( failures == 0 ))
|
||||
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env 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: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
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
mkdir "$scratch/tmp"
|
||||
export TMPDIR=$scratch/tmp
|
||||
|
||||
function check {
|
||||
if ! "$@"; then
|
||||
print -u2 -r -- "FAIL: ${(j: :)@}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
export ZSH=$scratch/omz ZSH_CUSTOM=$scratch/custom
|
||||
mkdir -p "$ZSH/themes" "$ZSH_CUSTOM/themes/nested" "$scratch/home" "$scratch/work"
|
||||
print -r -- "PROMPT='bundled'" > "$ZSH/themes/same.zsh-theme"
|
||||
print -r -- "PROMPT='custom themes'" > "$ZSH_CUSTOM/themes/same.zsh-theme"
|
||||
print -r -- "PROMPT='custom root'" > "$ZSH_CUSTOM/same.zsh-theme"
|
||||
print -r -- "PROMPT='z'" > "$ZSH/themes/zebra.zsh-theme"
|
||||
print -r -- "PROMPT='a'" > "$ZSH/themes/alpha.zsh-theme"
|
||||
print -r -- "PROMPT='bad'" > "$ZSH/themes/"$'bad\nname.zsh-theme'
|
||||
print -r -- "PROMPT='random'" > "$ZSH/themes/random.zsh-theme"
|
||||
print -r -- "PROMPT='nested'" > "$ZSH_CUSTOM/themes/nested/example.zsh-theme"
|
||||
check test "$(_omz_theme_names)" = $'alpha\nnested/example\nsame\nzebra'
|
||||
check test "$(_omz_theme_resolve nested/example)" = "$ZSH_CUSTOM/themes/nested/example.zsh-theme"
|
||||
check test "$(_omz_theme_resolve same)" = "$ZSH_CUSTOM/same.zsh-theme"
|
||||
rm "$ZSH_CUSTOM/same.zsh-theme"
|
||||
check test "$(_omz_theme_resolve same)" = "$ZSH_CUSTOM/themes/same.zsh-theme"
|
||||
rm "$ZSH_CUSTOM/themes/same.zsh-theme"
|
||||
check test "$(_omz_theme_resolve same)" = "$ZSH/themes/same.zsh-theme"
|
||||
for name in ../same /same . .. nested/../same nested/./example nested//example random $'bad\nname' 'bad\name' missing ''; do
|
||||
if _omz_theme_resolve "$name" > /dev/null 2>&1; then
|
||||
print -u2 -r -- 'FAIL: accepted invalid/missing name'
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The fixtures intentionally write only inside this private, always-cleaned tree.
|
||||
print -r -- 'print STARTUP_RAN; exit 99' > "$scratch/home/.zshenv"
|
||||
export ZDOTDIR=$scratch/home PREVIEW_ENV=preserved
|
||||
print -r -- '
|
||||
typeset -g PREVIEW_LEAK=bad
|
||||
export PREVIEW_ENV=changed
|
||||
alias preview_leak=true
|
||||
setopt noclobber
|
||||
function precmd { PROMPT="hook:$PWD:$PREVIEW_ENV:%?" }
|
||||
function second_hook { RPROMPT="second-hook:%?" }
|
||||
precmd_functions=(second_hook)
|
||||
' > "$ZSH_CUSTOM/isolation.zsh-theme"
|
||||
typeset before_pwd=$PWD before_options=$(setopt) before_env=$PREVIEW_ENV
|
||||
typeset output=$(_omz_theme_preview isolation 7)
|
||||
check test "$PWD" = "$before_pwd"
|
||||
check test "$(setopt)" = "$before_options"
|
||||
check test "$PREVIEW_ENV" = "$before_env"
|
||||
check test "${PREVIEW_LEAK-unset}" = unset
|
||||
check test "${aliases[preview_leak]-unset}" = unset
|
||||
check test "${functions[second_hook]-unset}" = unset
|
||||
check test "${output#*hook:${PWD}:changed:7}" != "$output"
|
||||
check test "${output#*second-hook:7}" != "$output"
|
||||
check test "${output#*STARTUP_RAN}" = "$output"
|
||||
|
||||
print -r -- 'PROMPT="env:$PREVIEW_ENV cwd:$PWD %(?.success.failure)"' > "$ZSH_CUSTOM/status.zsh-theme"
|
||||
output=$(_omz_theme_preview status)
|
||||
check test "${output#*env:preserved cwd:${PWD} success}" != "$output"
|
||||
output=$(_omz_theme_preview status 1)
|
||||
check test "${output#*failure}" != "$output"
|
||||
if _omz_theme_preview status nope >/dev/null 2>&1; then exit 1; fi
|
||||
|
||||
# Values produced by parameters or helpers are prompt text, not shell code for
|
||||
# a second substitution pass. Check both sides and the helper's incoming status.
|
||||
export PREVIEW_REEXECUTED=$scratch/reexecuted
|
||||
export PREVIEW_LITERAL='$(print reexecuted > "$PREVIEW_REEXECUTED")'
|
||||
print -r -- '
|
||||
function literal_helper { print -r -- "helper-status:$? $PREVIEW_LITERAL" }
|
||||
PROMPT='"'"'$PREVIEW_LITERAL $(literal_helper) %?'"'"'
|
||||
RPROMPT='"'"'$(literal_helper) $PREVIEW_LITERAL %?'"'"'
|
||||
' > "$ZSH_CUSTOM/literal.zsh-theme"
|
||||
output=$(_omz_theme_preview literal 7)
|
||||
check test ! -e "$PREVIEW_REEXECUTED"
|
||||
check test "${output#*"$PREVIEW_LITERAL"}" != "$output"
|
||||
check test "${output#*helper-status:7}" != "$output"
|
||||
check test "${output#*"$PREVIEW_LITERAL 7"}" != "$output"
|
||||
print -r -- 'unsetopt promptsubst; PROMPT='"'"'$PREVIEW_LITERAL %?'"'" > "$ZSH_CUSTOM/no-subst.zsh-theme"
|
||||
output=$(_omz_theme_preview no-subst 7)
|
||||
check test "${output#*'$PREVIEW_LITERAL 7'}" != "$output"
|
||||
|
||||
print -r -- 'PROMPT="conditional prompt"; [[ -n ${OMZ_PREVIEW_UNSET_SSH-} ]] && RPROMPT="remote"' > "$ZSH_CUSTOM/final-false.zsh-theme"
|
||||
output=$(_omz_theme_preview final-false)
|
||||
check test "${output#*conditional prompt}" != "$output"
|
||||
print -r -- 'PROMPT=""' > "$ZSH_CUSTOM/empty.zsh-theme"
|
||||
output=$(_omz_theme_preview empty)
|
||||
print -r -- 'RPROMPT="right-only"; false' > "$ZSH_CUSTOM/right-only.zsh-theme"
|
||||
output=$(_omz_theme_preview right-only)
|
||||
check test "${output#*right-only}" != "$output"
|
||||
|
||||
# A helper can fail inside command substitution without making print -P fail.
|
||||
# Its runtime diagnostic must remain visible in the sanitized partial preview.
|
||||
print -r -- 'PROMPT='"'"'$( _omz_preview_missing_helper )'"'" > "$ZSH_CUSTOM/helper-failure.zsh-theme"
|
||||
output=$(_omz_theme_preview helper-failure 2>&1)
|
||||
check test "${output#*command not found: _omz_preview_missing_helper}" != "$output"
|
||||
|
||||
# A failing precmd (or array hook) stops later hooks, as in interactive zsh.
|
||||
# Preserve any partial preview, but report failure rather than silently claiming
|
||||
# that an incomplete initialization is a faithful preview.
|
||||
for hook_setup in 'precmd() { return 3 }; precmd_functions=(must_not_run)' \
|
||||
'failed_hook() { return 4 }; precmd_functions=(failed_hook must_not_run)' \
|
||||
'failed_hook() { _omz_preview_missing_command }; precmd_functions=(failed_hook must_not_run)'; do
|
||||
print -rl -- 'PROMPT="partial prompt"; must_not_run() { print UNEXPECTED_HOOK; }' "$hook_setup" > "$ZSH_CUSTOM/hook-failure.zsh-theme"
|
||||
if _omz_theme_preview hook-failure >"$scratch/hook-error" 2>&1; then
|
||||
print -u2 -r -- 'FAIL: accepted failing precmd hook'
|
||||
exit 1
|
||||
fi
|
||||
output=$(<"$scratch/hook-error")
|
||||
check test "${output#*UNEXPECTED_HOOK}" = "$output"
|
||||
check test "${output#*remaining hooks skipped}" != "$output"
|
||||
check test "${output#*partial prompt}" != "$output"
|
||||
done
|
||||
|
||||
# No language runtime, timeout utility, or process-inspection utility is needed
|
||||
# by the backend. The worker still has zsh's usual autoload/module search paths.
|
||||
mkdir "$scratch/bin"
|
||||
for name in zsh mktemp mkfifo rm; do
|
||||
ln -s "$commands[$name]" "$scratch/bin/$name"
|
||||
done
|
||||
output=$(PATH="$scratch/bin" _omz_theme_preview status)
|
||||
check test "${output#*success}" != "$output"
|
||||
print -r -- '[[ ! -t 0 && ! -t 1 && ! -t 2 && ! -o interactive ]] || exit 9; PROMPT="non-tty"' > "$ZSH_CUSTOM/non-tty.zsh-theme"
|
||||
output=$(_omz_theme_preview non-tty)
|
||||
check test "${output#*non-tty}" != "$output"
|
||||
|
||||
print -r -- 'print -n -- "\e]52;c;SECRET\a\e[2J\r\b\ePSECRET\e\\"; PROMPT="%F{red}safe%f"' > "$ZSH_CUSTOM/escapes.zsh-theme"
|
||||
output=$(_omz_theme_preview escapes)
|
||||
check test "${output#*SECRET}" = "$output"
|
||||
check test "${output#*$'\e[2J'}" = "$output"
|
||||
check test "${output#*$'\r'}" = "$output"
|
||||
check test "${output#*$'\e[31m'safe}" != "$output"
|
||||
|
||||
for code in 'return 1' 'exit 0' 'exit 7' 'if then' 'PROMPT="partial"; if then' \
|
||||
'PROMPT="partial"; return 2' 'PROMPT="partial"; _omz_preview_missing_command'; do
|
||||
print -r -- "$code" > "$ZSH_CUSTOM/broken.zsh-theme"
|
||||
if _omz_theme_preview broken >"$scratch/error" 2>&1; then
|
||||
print -u2 -r -- "FAIL: accepted broken theme: $code"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
print -r -- 'while true; do print -r -- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; done' > "$ZSH_CUSTOM/flood.zsh-theme"
|
||||
if _omz_theme_preview flood >"$scratch/flood" 2>&1; then exit 1; fi
|
||||
check test "$(wc -c < "$scratch/flood")" -le 32768
|
||||
print -r -- 'printf "\377%.0s" {1..11000}; PROMPT="safe"' > "$ZSH_CUSTOM/bytes.zsh-theme"
|
||||
_omz_theme_preview bytes >"$scratch/bytes" 2>&1
|
||||
check test "$(wc -c < "$scratch/bytes")" -le 32768
|
||||
check test "${$(<"$scratch/bytes")#*$'\xff'}" = "$(<"$scratch/bytes")"
|
||||
print -r -- 'printf "\233m%.0s" {1..12000}; PROMPT="safe"' > "$ZSH_CUSTOM/csi-bytes.zsh-theme"
|
||||
_omz_theme_preview csi-bytes >"$scratch/csi-bytes" 2>&1
|
||||
check test "$(wc -c < "$scratch/csi-bytes")" -le 32768
|
||||
|
||||
print -r -- 'sleep 30 & print -r -- $! > "$PREVIEW_PID_FILE"; wait' > "$ZSH_CUSTOM/slow.zsh-theme"
|
||||
export PREVIEW_PID_FILE=$scratch/child
|
||||
zmodload zsh/datetime
|
||||
typeset -F started=$EPOCHREALTIME
|
||||
if _omz_theme_preview slow >"$scratch/timeout" 2>&1; then exit 1; fi
|
||||
check test $(( EPOCHREALTIME - started < 5 )) = 1
|
||||
check test "${$(<"$scratch/timeout")#*timed out}" != "$(<"$scratch/timeout")"
|
||||
typeset child=$(<"$scratch/child")
|
||||
# A killed orphan may briefly remain a zombie until the system reaps it.
|
||||
typeset child_state=$(command ps -o stat= -p "$child" 2>/dev/null || true)
|
||||
check test -z "${child_state//[ ZN+]/}"
|
||||
|
||||
for signal in INT TERM HUP; do
|
||||
rm -f "$PREVIEW_PID_FILE"
|
||||
_omz_theme_preview slow >"$scratch/cancel" 2>&1 &
|
||||
typeset preview_job=$!
|
||||
for attempt in {1..100}; do
|
||||
[[ -s "$PREVIEW_PID_FILE" ]] && break
|
||||
sleep 0.01
|
||||
done
|
||||
check test -s "$PREVIEW_PID_FILE"
|
||||
# zsh adds a waiting wrapper around a backgrounded subshell function.
|
||||
# Signal the supervisor itself; terminal interrupts reach it via the job group.
|
||||
typeset supervisor='' candidate='' parent=''
|
||||
for candidate parent in ${(z)$(command ps -A -o pid= -o ppid=)}; do
|
||||
if [[ $parent == $preview_job ]]; then
|
||||
supervisor=$candidate
|
||||
break
|
||||
fi
|
||||
done
|
||||
check test -n "$supervisor"
|
||||
kill -$signal "$supervisor"
|
||||
typeset result=0
|
||||
wait "$preview_job" || result=$?
|
||||
if (( result != 130 && result != 143 && result != 129 )); then
|
||||
print -u2 -r -- "Unexpected cancellation result: $result: $(<"$scratch/cancel")"
|
||||
fi
|
||||
if [[ $signal == INT ]]; then
|
||||
check test "$result" = 130
|
||||
elif [[ $signal == TERM ]]; then
|
||||
check test "$result" = 143
|
||||
else
|
||||
check test "$result" = 129
|
||||
fi
|
||||
child=$(<"$PREVIEW_PID_FILE")
|
||||
child_state=$(command ps -o stat= -p "$child" 2>/dev/null || true)
|
||||
check test -z "${child_state//[ ZN+]/}"
|
||||
done
|
||||
|
||||
print -r -- 'sleep 30 & print -r -- $! > "$PREVIEW_PID_FILE"; PROMPT="background"' > "$ZSH_CUSTOM/background.zsh-theme"
|
||||
output=$(_omz_theme_preview background)
|
||||
child=$(<"$PREVIEW_PID_FILE")
|
||||
child_state=$(command ps -o stat= -p "$child" 2>/dev/null || true)
|
||||
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-browser/preview.zsh")
|
||||
zpty -b browser exec "${(@q)nested_command}"
|
||||
output=''
|
||||
for attempt in {1..300}; do
|
||||
if zpty -r browser chunk; then
|
||||
output+=$chunk
|
||||
elif ! zpty -t browser; then
|
||||
break
|
||||
else
|
||||
sleep 0.01
|
||||
fi
|
||||
done
|
||||
check test "${output#*success}" != "$output"
|
||||
check zpty -t unrelated
|
||||
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-browser/preview.zsh")
|
||||
zpty -b browser exec "${(@q)nested_command}"
|
||||
typeset browser_group record
|
||||
for record in "${(@f)$(zpty)}"; do
|
||||
[[ $record == \(<->\)\ browser:* ]] && browser_group=${${record#\(}%%\)*}
|
||||
done
|
||||
check test -n "$browser_group"
|
||||
for attempt in {1..100}; do
|
||||
[[ -s "$PREVIEW_PID_FILE" ]] && break
|
||||
sleep 0.01
|
||||
done
|
||||
check test -s "$PREVIEW_PID_FILE"
|
||||
child=$(<"$PREVIEW_PID_FILE")
|
||||
kill -TERM -- -$browser_group
|
||||
for attempt in {1..100}; do
|
||||
child_state=$(command ps -o stat= -p "$child" 2>/dev/null || true)
|
||||
[[ -z "${child_state//[ ZN+]/}" ]] && break
|
||||
sleep 0.01
|
||||
done
|
||||
check test -z "${child_state//[ ZN+]/}"
|
||||
zpty -d browser
|
||||
|
||||
# Real themes exercise synchronous git, top-level locals, command substitution,
|
||||
# and hook-driven prompt construction against an isolated repository.
|
||||
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-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)
|
||||
check test "${output#*Left prompt}" != "$output"
|
||||
check test "${output#*Right prompt}" != "$output"
|
||||
check test "${output#*preview-branch}" != "$output"
|
||||
if [[ $name == dieter || $name == bira ]]; then
|
||||
check test "${output#*7 }" != "$output"
|
||||
fi
|
||||
done
|
||||
typeset -a leftovers=("$TMPDIR"/omz-preview.*(N))
|
||||
check test "$#leftovers" = 0
|
||||
print -r -- 'PASS: theme preview resolution, isolation, single-pass expansion, conditional loads, hook failures, status, sanitization, limits, cancellation, descendant cleanup, and six bundled themes'
|
||||
Reference in New Issue
Block a user