Compare commits

...
Author SHA1 Message Date
Robby Russell 76439de772 fix(cli): preserve literal theme browser output
Keep backslash sequences literal and enable prompt-percent rendering independently of the parent shell options. Add PTY coverage for both fixes and verify EXIT-hook preservation through the public CLI.
2026-09-09 06:13:21 -07:00
Robby Russell 3bf157dc77 refactor(cli): group theme browser files 2026-09-09 05:48:02 -07:00
Robby Russell 2e0c643117 feat(cli): add interactive theme browser 2026-09-08 19:53:23 -07:00
Lie Ryan bd77419b2a fix(virtualenvwrapper): stop the project-root walk at a .git file (#10030) 2026-09-08 15:56:43 -07:00
Berken Tekin d217d05374 feat(strug): distinguish the root shell in the prompt (#9723) 2026-09-08 14:36:55 -07:00
Robby Russell c891b750b0 core: Add merchandise link and image to README (#14081)
Added a link to official merchandise on commitgoods.com with responsive images.
2026-09-08 21:08:44 +02:00
Raihan Firdaus 6aa16696d5 feat(laravel): add pammig alias for php artisan make:migration (#14080) 2026-09-08 02:05:39 -07:00
Grant StephensandClaude Sonnet 5 8d10cc9488 feat(go): Create go_prompt_info to add Go version to theme prompts (#8924)
* Create go_prompt_info to add go version to prompts

* golang: move go_prompt_info to plugin, guard and quote output

Addresses review feedback: the prompt helper only belongs to Go
developers, so it moves out of lib/ (sourced for every shell) into
plugins/golang/golang.plugin.zsh. Adds a $+commands[go] guard so
users without Go installed don't spawn a process or see a stderr
error on every prompt render, quotes the version output the same
way the rvm/nvm helpers do so a % can't be read as a prompt escape,
and adds the dummy go_prompt_info stub to
lib/prompt_info_functions.zsh for themes that call it when the
plugin isn't loaded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193G8TyNqmNnv6v9HsVafGa

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 07:49:06 -07:00
dependabot[bot] bf77e350e0 chore(deps): bump step-security/harden-runner from 2.21.0 to 2.21.1 in the gha-minor group (#14060)
Signed-off-by: dependabot[bot] <support@github.com>
2026-09-07 10:19:48 +02:00
ohmyzsh[bot] 07351f99fc chore(gradle): update to version e7c881db (#14057) 2026-09-07 10:19:34 +02:00
Robby Russell 39334a08e9 perf(init): check zcompdump metadata without forking grep (#14061) 2026-09-07 10:18:49 +02:00
Robby Russell cba369e090 perf(update): check the update epoch before forking git (#14062) 2026-09-07 10:17:25 +02:00
Robby Russell 0da7919339 perf(init): skip the compdump lock and zrecompile when the .zwc is fresh (#14064) 2026-09-07 10:14:56 +02:00
Robby Russell 992edcc17d perf(init): only mkdir the cache completions dir when it's missing (#14065) 2026-09-07 10:12:56 +02:00
Yui(ゆい) 78cbb3fe5e fix(juju): add missing junex alias (#14076) 2026-09-07 10:09:00 +02:00
Robby Russell 76ac9fcddc chore(core): refresh links in install and upgrade (#14074) 2026-09-07 10:08:43 +02:00
Robby Russell 17bbf5fdac fix(poetry-env): scope venv_dir to the activation function (#14043) 2026-09-07 09:55:47 +02:00
Robby RussellandJan Červenka 704f7d59d8 fix(vagrant): complete VM names for suspend and rdp (#14039)
Co-authored-by: Jan Červenka <cervenka@raynet.cz>
2026-09-07 09:53:36 +02:00
Let Avocado 0b235c54de feat(web-search): add 8 new search engines (#13879)
* feat(web-search): add 8 new search engines

Add search engine aliases for:
- maven (Maven Central)
- mdn (MDN Web Docs)
- pd (pub.dev / Dart packages)
- pypi (Python packages)
- aur (Arch User Repository)
- archwiki (Arch Wiki)
- hf (Hugging Face)
- wikipedia (Wikipedia)

* fix(web-search): resolve alias conflicts
2026-09-06 13:09:02 -07:00
Jérôme Tamarelle 426650fbe8 fix(symfony6): don't run command substitutions on tab completion (#14058)
The plugin is a copy of the completion script shipped by symfony/console.
It has not been updated since it was added, and two fixes made upstream
since then are missing.

The completion request was assembled as a string from the raw words of
the command line, then passed to "eval". Any command substitution present
in a word was therefore executed by the completion, before the user
validated the line. The request is now run as an array of arguments,
without being read again by the shell, and "_describe" is called directly
instead of through "eval". The alias of the command is resolved
explicitly, since that was the only useful effect of the "eval".

The request also runs with SHELL_VERBOSITY=0, so that completion keeps
working for users who exported SHELL_VERBOSITY=-1.

Ported from symfony/symfony#65818 and symfony/symfony#63859.
2026-09-06 07:02:47 -07:00
Robby RussellandMoulick Aggarwal 8a5b393088 feat(kubectl): add kesec alias for editing secrets (#14048)
Secret was the only resource in the plugin with get, describe and delete
aliases but no edit alias, while thirteen other resource types have one.

Closes #8309

Co-authored-by: Moulick Aggarwal <Moulick@users.noreply.github.com>
2026-09-05 23:04:08 -03:00
Josh McKinney 87d4d40418 feat(jj): Add common jj aliases (#13962) 2026-09-05 14:36:14 -07:00
36 changed files with 1466 additions and 48 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies:
plugins/gradle:
repo: gradle/gradle-completion
branch: master
version: 63578c9bc56134b80766e88957ba0239e6be2343
version: e7c881dbef7462bc35ad05328397e1582fe25cb8
precopy: |
set -e
find . ! -name _gradle ! -name LICENSE -delete
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
contents: write # this is needed to push commits and branches
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
- macos-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
@@ -47,7 +47,7 @@ jobs:
- test
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
if: github.repository == 'ohmyzsh/ohmyzsh'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: github.repository == 'ohmyzsh/ohmyzsh'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: Authenticate as @ohmyzsh
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
+21
View File
@@ -23,6 +23,14 @@ Twitter), and join us on [Discord](https://discord.gg/ohmyzsh).
[![Mastodon Follow](https://img.shields.io/mastodon/follow/111169632522566717?label=%40ohmyzsh&domain=https%3A%2F%2Fmstdn.social&logo=mastodon&style=flat)](https://mstdn.social/@ohmyzsh)
[![Discord server](https://img.shields.io/discord/642496866407284746)](https://discord.gg/ohmyzsh)
<a href="https://commitgoods.com/collections/oh-my-zsh?utm_source=github">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cdn.shopify.com/s/files/1/0101/8752/files/commitgoods-badge-stacked-navy-dark.svg">
<img src="https://cdn.shopify.com/s/files/1/0101/8752/files/commitgoods-badge-stacked-navy.svg" height="28" alt="Official merch on commitgoods.com">
</picture>
</a>
<details>
<summary>Table of Contents</summary>
@@ -163,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
View File
@@ -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"/:::))
+1
View File
@@ -21,6 +21,7 @@ function chruby_prompt_info \
azure_prompt_info \
tf_prompt_info \
conda_prompt_info \
go_prompt_info \
{
return 1
}
+17 -7
View File
@@ -64,7 +64,7 @@ if [[ ! -w "$ZSH_CACHE_DIR" ]]; then
fi
# Create cache and completions dir and add to $fpath
mkdir -p "$ZSH_CACHE_DIR/completions"
[[ -d "$ZSH_CACHE_DIR/completions" ]] || mkdir -p "$ZSH_CACHE_DIR/completions"
(( ${fpath[(Ie)$ZSH_CACHE_DIR/completions]} )) || fpath=("$ZSH_CACHE_DIR/completions" $fpath)
# Check for updates on initial load...
@@ -114,9 +114,17 @@ fi
zcompdump_revision="#omz revision: $(builtin cd -q "$ZSH"; git rev-parse HEAD 2>/dev/null)"
zcompdump_fpath="#omz fpath: $fpath"
# Check whether the zcompdump file carries the current OMZ metadata lines
_omz_compdump_has_metadata() {
local -a lines
[[ -r "$ZSH_COMPDUMP" ]] || return 1
lines=("${(@f)$(<"$ZSH_COMPDUMP")}")
(( ${lines[(Ie)$zcompdump_revision]} && ${lines[(Ie)$zcompdump_fpath]} ))
}
# Delete the zcompdump file if OMZ zcompdump metadata changed
if ! command grep -q -Fx "$zcompdump_revision" "$ZSH_COMPDUMP" 2>/dev/null \
|| ! command grep -q -Fx "$zcompdump_fpath" "$ZSH_COMPDUMP" 2>/dev/null; then
zcompdump_refresh=0
if ! _omz_compdump_has_metadata; then
command rm -f "$ZSH_COMPDUMP"
zcompdump_refresh=1
fi
@@ -134,9 +142,8 @@ else
compinit -u -d "$ZSH_COMPDUMP"
fi
# Append zcompdump metadata if missing
if (( $zcompdump_refresh )) \
|| ! command grep -q -Fx "$zcompdump_revision" "$ZSH_COMPDUMP" 2>/dev/null; then
# Append zcompdump metadata if missing (compinit may have regenerated the file)
if (( zcompdump_refresh )) || ! _omz_compdump_has_metadata; then
# Use `tee` in case the $ZSH_COMPDUMP filename is invalid, to silence the error
# See https://github.com/ohmyzsh/ohmyzsh/commit/dd1a7269#commitcomment-39003489
tee -a "$ZSH_COMPDUMP" &>/dev/null <<EOF
@@ -146,9 +153,12 @@ $zcompdump_fpath
EOF
fi
unset zcompdump_revision zcompdump_fpath zcompdump_refresh
unset -f _omz_compdump_has_metadata
# zcompile the completion dump file if the .zwc is older or missing.
if command mkdir "${ZSH_COMPDUMP}.lock" 2>/dev/null; then
# Test that first so the lock directory and zrecompile are skipped when it's fresh.
if [[ ! "${ZSH_COMPDUMP}.zwc" -nt "$ZSH_COMPDUMP" ]] \
&& command mkdir "${ZSH_COMPDUMP}.lock" 2>/dev/null; then
zrecompile -q -p "$ZSH_COMPDUMP"
command rm -rf "$ZSH_COMPDUMP.zwc.old" "${ZSH_COMPDUMP}.lock"
fi
+15
View File
@@ -39,3 +39,18 @@ plugins=(... golang)
| gov | `go vet` | Vet examines Go source code and reports suspicious constructs |
| gove | `go version` | Prints Go version |
| gow | `go work` | Work provides access to operations on workspaces |
## Prompt
This plugin provides the `go_prompt_info` function to add the installed Go
version to your prompt.
For example:
```
PROMPT="%~$ "
RPROMPT='$(go_prompt_info)'
```
changes your prompt to:
```
~/go/project$ ▋ 1.22.3
```
+9
View File
@@ -36,3 +36,12 @@ alias gotofx='go tool fix'
alias gov='go vet'
alias gove='go version'
alias gow='go work'
## prompt
function go_prompt_info() {
(( $+commands[go] )) || return
local go_prompt
go_prompt=$(go version | awk '{ print substr($3, 3) }')
[[ -z "${go_prompt}" ]] && return
echo "${ZSH_THEME_GO_PROMPT_PREFIX}${go_prompt:gs/%/%%}${ZSH_THEME_GO_PROMPT_SUFFIX}"
}
+10 -4
View File
@@ -284,6 +284,9 @@ __gradle_subcommand() {
'-Dorg.gradle.debug.suspend=[]' \
'-Dorg.gradle.debug=[]:org.gradle.debug:(true false)' \
'-Dorg.gradle.dependency.verification=[Configures the dependency verification mode. Supported values are 'strict', 'lenient', or 'off'.]:org.gradle.dependency.verification:(strict lenient off)' \
'-Dorg.gradle.isolated-projects.dangerously-ignore-problems=[]' \
'-Dorg.gradle.isolated-projects.diagnostics=[]' \
'-Dorg.gradle.isolated-projects=[Enables Isolated Projects. Projects are configured in parallel. Implies `--configuration-cache`.]' \
'-Dorg.gradle.java.home=[]:org.gradle.java.home:_directories' \
'-Dorg.gradle.java.installations.auto-detect=[]' \
'-Dorg.gradle.java.installations.auto-download=[]' \
@@ -299,8 +302,6 @@ __gradle_subcommand() {
'-Dorg.gradle.problems.report=[Enables the HTML problems report.]' \
'-Dorg.gradle.projectcachedir=[Specifies the project-specific cache directory. Default is .gradle in the root project directory.]:org.gradle.projectcachedir:_directories' \
'-Dorg.gradle.tooling.parallel=[]' \
'-Dorg.gradle.unsafe.isolated-projects.diagnostics=[]' \
'-Dorg.gradle.unsafe.isolated-projects=[]' \
'-Dorg.gradle.vfs.verbose=[]' \
'-Dorg.gradle.vfs.watch=[Enables file system watching. Reuses file system data for subsequent builds.]:org.gradle.vfs.watch:(true false)' \
'-Dorg.gradle.warning.mode=[Specifies which mode of warnings to generate. Supported values are 'all', 'fail', 'summary' (default), or 'none'.]' \
@@ -328,12 +329,14 @@ __gradle_subcommand() {
\*'--include-build[Includes the specified build in the composite.]:include build:_directories' \
(--quiet,-q,--warn,-w,--debug,-d){-i,--info}'[Sets the log level to info.]' \
\*{-I,--init-script}'[Specifies an initialization script.]:init script:_files -g \*.gradle(|.kts)' \
(--no-isolated-projects)'--isolated-projects[Enables Isolated Projects. Projects are configured in parallel. Implies `--configuration-cache`. (incubating)]' \
'--max-workers[Configures the maximum number of concurrent workers Gradle is allowed to use.]' \
(--build-cache)'--no-build-cache[Disables the Gradle build cache.]' \
(--configuration-cache)'--no-configuration-cache[Disables the configuration cache.]' \
(--configure-on-demand)'--no-configure-on-demand[Disables the use of configuration on demand. (incubating)]' \
(--continue)'--no-continue[Stops task execution after a task failure.]' \
(--daemon)'--no-daemon[Runs the build without the Gradle daemon. Useful occasionally if you have configured Gradle to always run with the daemon by default.]' \
(--isolated-projects)'--no-isolated-projects[Disables Isolated Projects. (incubating)]' \
(--parallel)'--no-parallel[Disables parallel project execution.]' \
(--problems-report)'--no-problems-report[Disables the HTML problems report. (incubating)]' \
{-a,--no-rebuild}'[Disables rebuilding of project dependencies.]' \
@@ -421,6 +424,9 @@ _gradle() {
'-Dorg.gradle.debug.suspend=[]:->argument-expected' \
'-Dorg.gradle.debug=[]:org.gradle.debug:(true false):->argument-expected' \
'-Dorg.gradle.dependency.verification=[Configures the dependency verification mode. Supported values are 'strict', 'lenient', or 'off'.]:org.gradle.dependency.verification:(strict lenient off):->argument-expected' \
'-Dorg.gradle.isolated-projects.dangerously-ignore-problems=[]:->argument-expected' \
'-Dorg.gradle.isolated-projects.diagnostics=[]:->argument-expected' \
'-Dorg.gradle.isolated-projects=[Enables Isolated Projects. Projects are configured in parallel. Implies `--configuration-cache`.]:->argument-expected' \
'-Dorg.gradle.java.home=[]:org.gradle.java.home:_directories:->argument-expected' \
'-Dorg.gradle.java.installations.auto-detect=[]:->argument-expected' \
'-Dorg.gradle.java.installations.auto-download=[]:->argument-expected' \
@@ -436,8 +442,6 @@ _gradle() {
'-Dorg.gradle.problems.report=[Enables the HTML problems report.]:->argument-expected' \
'-Dorg.gradle.projectcachedir=[Specifies the project-specific cache directory. Default is .gradle in the root project directory.]:org.gradle.projectcachedir:_directories:->argument-expected' \
'-Dorg.gradle.tooling.parallel=[]:->argument-expected' \
'-Dorg.gradle.unsafe.isolated-projects.diagnostics=[]:->argument-expected' \
'-Dorg.gradle.unsafe.isolated-projects=[]:->argument-expected' \
'-Dorg.gradle.vfs.verbose=[]:->argument-expected' \
'-Dorg.gradle.vfs.watch=[Enables file system watching. Reuses file system data for subsequent builds.]:org.gradle.vfs.watch:(true false):->argument-expected' \
'-Dorg.gradle.warning.mode=[Specifies which mode of warnings to generate. Supported values are 'all', 'fail', 'summary' (default), or 'none'.]:->argument-expected' \
@@ -466,12 +470,14 @@ _gradle() {
\*'--include-build[Includes the specified build in the composite.]:include build:_directories:->argument-expected' \
(--quiet,-q,--warn,-w,--debug,-d){-i,--info}'[Sets the log level to info.]' \
\*{-I,--init-script}'[Specifies an initialization script.]:init script:_files -g \*.gradle(|.kts):->argument-expected' \
(--no-isolated-projects)'--isolated-projects[Enables Isolated Projects. Projects are configured in parallel. Implies `--configuration-cache`. (incubating)]' \
'--max-workers[Configures the maximum number of concurrent workers Gradle is allowed to use.]:->argument-expected' \
(--build-cache)'--no-build-cache[Disables the Gradle build cache.]' \
(--configuration-cache)'--no-configuration-cache[Disables the configuration cache.]' \
(--configure-on-demand)'--no-configure-on-demand[Disables the use of configuration on demand. (incubating)]' \
(--continue)'--no-continue[Stops task execution after a task failure.]' \
(--daemon)'--no-daemon[Runs the build without the Gradle daemon. Useful occasionally if you have configured Gradle to always run with the daemon by default.]' \
(--isolated-projects)'--no-isolated-projects[Disables Isolated Projects. (incubating)]' \
(--parallel)'--no-parallel[Disables parallel project execution.]' \
(--problems-report)'--no-problems-report[Disables the HTML problems report. (incubating)]' \
{-a,--no-rebuild}'[Disables rebuilding of project dependencies.]' \
+19
View File
@@ -19,35 +19,54 @@ plugins=(... jj)
| jjbd | `jj bookmark delete` |
| jjbf | `jj bookmark forget` |
| jjbl | `jj bookmark list` |
| jjblt | `jj bookmark list --tracked` |
| jjbm | `jj bookmark move` |
| jjbr | `jj bookmark rename` |
| jjbs | `jj bookmark set` |
| jjbt | `jj bookmark track` |
| jjbu | `jj bookmark untrack` |
| jjc | `jj commit` |
| jjcfg | `jj config` |
| jjcfgl | `jj config list` |
| jjcmsg | `jj commit --message` |
| jjd | `jj diff` |
| jjdmsg | `jj desc --message` |
| jjds | `jj desc` |
| jjdst | `jj diff --stat` |
| jje | `jj edit` |
| jjev | `jj evolog` |
| jjf | `jj file` |
| jjfl | `jj file list` |
| jjg | `jj git` |
| jjgcl | `jj git clone` |
| jjgf | `jj git fetch` |
| jjgfa | `jj git fetch --all-remotes` |
| jjgi | `jj git init` |
| jjgp | `jj git push` |
| jjgpa | `jj git push --all` |
| jjgpd | `jj git push --deleted` |
| jjgpt | `jj git push --tracked` |
| jjgrl | `jj git remote list` |
| jjl | `jj log` |
| jjla | `jj log -r "all()"` |
| jjn | `jj new` |
| jjnt | `jj new "trunk()"` |
| jjop | `jj op` |
| jjopl | `jj op log` |
| jjor | `jj op restore` |
| jjrb | `jj rebase` |
| jjrbm | `jj rebase -d "trunk()"` |
| jjrs | `jj restore` |
| jjrt | `cd "$(jj root \|\| echo .)"` |
| jjs | `jj show` |
| jjsp | `jj split` |
| jjsq | `jj squash` |
| jjst | `jj status` |
| jju | `jj undo` |
| jjw | `jj workspace` |
| jjwa | `jj workspace add` |
| jjwf | `jj workspace forget` |
| jjwl | `jj workspace list` |
## Prompt usage
+17
View File
@@ -45,34 +45,51 @@ alias jjbc='jj bookmark create'
alias jjbd='jj bookmark delete'
alias jjbf='jj bookmark forget'
alias jjbl='jj bookmark list'
alias jjblt='jj bookmark list --tracked'
alias jjbm='jj bookmark move'
alias jjbr='jj bookmark rename'
alias jjbs='jj bookmark set'
alias jjbt='jj bookmark track'
alias jjbu='jj bookmark untrack'
alias jjc='jj commit'
alias jjcfg='jj config'
alias jjcfgl='jj config list'
alias jjcmsg='jj commit --message'
alias jjd='jj diff'
alias jjdmsg='jj desc --message'
alias jjds='jj desc'
alias jjdst='jj diff --stat'
alias jje='jj edit'
alias jjev='jj evolog'
alias jjf='jj file'
alias jjfl='jj file list'
alias jjg='jj git'
alias jjgcl='jj git clone'
alias jjgf='jj git fetch'
alias jjgfa='jj git fetch --all-remotes'
alias jjgi='jj git init'
alias jjgp='jj git push'
alias jjgpa='jj git push --all'
alias jjgpd='jj git push --deleted'
alias jjgpt='jj git push --tracked'
alias jjgrl='jj git remote list'
alias jjl='jj log'
alias jjla='jj log -r "all()"'
alias jjn='jj new'
alias jjnt='jj new "trunk()"'
alias jjop='jj op'
alias jjopl='jj op log'
alias jjor='jj op restore'
alias jjrb='jj rebase'
alias jjrbm='jj rebase -d "trunk()"'
alias jjrs='jj restore'
alias jjrt='cd "$(jj root || echo .)"'
alias jjs='jj show'
alias jjsp='jj split'
alias jjsq='jj squash'
alias jjst='jj status'
alias jju='jj undo'
alias jjw='jj workspace'
alias jjwa='jj workspace add'
alias jjwf='jj workspace forget'
alias jjwl='jj workspace list'
+1
View File
@@ -35,6 +35,7 @@ alias jdlr='juju debug-log --ms --replay'
alias jcon='juju consume'
alias jeb='juju export-bundle'
alias jex='juju expose'
alias junex='juju unexpose'
alias jh='juju help'
alias jkc='juju kill-controller -y -t 0'
alias jm='juju models'
+1
View File
@@ -71,6 +71,7 @@ plugins=(... kubectl)
| | | **Secret management** |
| kgsec | `kubectl get secret` | Get secret for decoding |
| kgseca | `kubectl get secret --all-namespaces` | List secrets across all namespaces |
| kesec | `kubectl edit secret` | Edit secret resource |
| kdsec | `kubectl describe secret` | Describe secret resource in detail |
| kdelsec | `kubectl delete secret` | Delete the secret |
| | | **Deployment management** |
+1
View File
@@ -93,6 +93,7 @@ alias kdelcm='kubectl delete configmap'
# Secret management
alias kgsec='kubectl get secret'
alias kgseca='kubectl get secret --all-namespaces'
alias kesec='kubectl edit secret'
alias kdsec='kubectl describe secret'
alias kdelsec='kubectl delete secret'
+1
View File
@@ -43,6 +43,7 @@ plugins=(... laravel)
| `pami` | `php artisan make:interface` |
| `pamtr` | `php artisan make:trait` |
| `pamv` | `php artisan make:view` |
| `pammig` | `php artisan make:migration` |
## Clears
+1
View File
@@ -32,6 +32,7 @@ alias pamen='php artisan make:enum'
alias pami='php artisan make:interface'
alias pamtr='php artisan make:trait'
alias pamv='php artisan make:view'
alias pammig='php artisan make:migration'
# Clears
+1 -1
View File
@@ -23,7 +23,7 @@ _togglePoetryShell() {
# Activate the environment if in a Poetry directory and no environment is currently active
if [[ $in_poetry_dir -eq 1 ]] && [[ $poetry_active -ne 1 ]]; then
venv_dir=$(poetry env info --path 2>/dev/null)
local venv_dir=$(poetry env info --path 2>/dev/null)
# Handle case where poetry returns "." for in-project virtual environments
if [[ "$venv_dir" == "." ]]; then
venv_dir="$PWD/.venv"
+21 -13
View File
@@ -15,8 +15,8 @@
# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
#
_sf_console() {
local lastParam flagPrefix requestComp out comp
local -a completions
local lastParam out comp sf_cmd
local -a completions flagPrefix requestComp inputs
# The user could have moved the cursor backwards on the command-line.
# We need to trigger completion from the $CURRENT location, so we need
@@ -29,11 +29,20 @@ _sf_console() {
setopt local_options BASH_REMATCH
if [[ "${lastParam}" =~ '-.*=' ]]; then
# We are dealing with a flag with an =
flagPrefix="-P ${BASH_REMATCH}"
flagPrefix=(-P "${BASH_REMATCH}")
fi
# Prepare the command to obtain completions
requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a1 -c$((CURRENT-1))" i=""
# Prepare the command to obtain completions. An alias is resolved here,
# because the request is no longer read again by the shell.
sf_cmd="${words[1]}"
if [[ -n "${aliases[$sf_cmd]}" ]]; then
requestComp=(${(z)aliases[$sf_cmd]})
else
requestComp=(${~sf_cmd})
fi
requestComp+=(_complete --no-interaction -szsh -a1 "-c$((CURRENT-1))")
for w in ${words[@]}; do
w=$(printf -- '%b' "$w")
# remove quotes from typed values
@@ -47,19 +56,18 @@ _sf_console() {
fi
# empty values are ignored
if [ ! -z "$w" ]; then
i="${i}-i${w} "
inputs+=("-i$w")
fi
done
# Ensure at least 1 input
if [ "${i}" = "" ]; then
requestComp="${requestComp} -i\" \""
else
requestComp="${requestComp} ${i}"
if (( ! $#inputs )); then
inputs=(-i' ')
fi
# Use eval to handle any environment variables and such
out=$(eval ${requestComp} 2>/dev/null)
# The request is run without being read again by the shell, so that a
# "$(...)" or a backtick typed on the command line is not executed
out=$(SHELL_VERBOSITY=0 "${requestComp[@]}" "${inputs[@]}" 2>/dev/null)
while IFS='\n' read -r comp; do
if [ -n "$comp" ]; then
@@ -75,7 +83,7 @@ _sf_console() {
done < <(printf "%s\n" "${out[@]}")
# Let inbuilt _describe handle completions
eval _describe "completions" completions $flagPrefix
_describe "completions" completions "${flagPrefix[@]}"
return $?
}
+1 -1
View File
@@ -127,7 +127,7 @@ case $state in
(box)
__vagrant-box
;;
(up|provision|port|package|destroy|reload|ssh|ssh-config|halt|resume|status)
(up|provision|port|package|destroy|reload|ssh|ssh-config|halt|resume|status|suspend|rdp)
_arguments ':feature:__vm_list'
esac
;;
@@ -39,7 +39,7 @@ if [[ ! $DISABLE_VENV_CD -eq 1 ]]; then
# Get absolute path, resolving symlinks
local PROJECT_ROOT="${PWD:A}"
while [[ "$PROJECT_ROOT" != "/" && ! -e "$PROJECT_ROOT/.venv" \
&& ! -d "$PROJECT_ROOT/.git" ]]; do
&& ! -e "$PROJECT_ROOT/.git" ]]; do
PROJECT_ROOT="${PROJECT_ROOT:h}"
done
+5
View File
@@ -51,6 +51,11 @@ Available search contexts are:
| `npmpkg` | `https://www.npmjs.com/search?q=` |
| `packagist` | `https://packagist.org/?query=` |
| `gopkg` | `https://pkg.go.dev/search?m=package&q=` |
| `maven` | `https://central.sonatype.com/search?q=` |
| `pypi` | `https://pypi.org/search/?q=` |
| `aur` | `https://aur.archlinux.org/packages?O=0&K=` |
| `archwiki` | `https://wiki.archlinux.org/index.php?search=` |
| `wikipedia` | `https://en.wikipedia.org/w/index.php?search=` |
| `chatgpt` | `https://chatgpt.com/?q=` |
| `claudeai` | `https://claude.ai/new?q=` |
| `grokcom` | `https://grok.com/?q=` |
+10 -1
View File
@@ -32,6 +32,11 @@ function web_search() {
npmpkg "https://www.npmjs.com/search?q="
packagist "https://packagist.org/?query="
gopkg "https://pkg.go.dev/search?m=package&q="
maven "https://central.sonatype.com/search?q="
pypi "https://pypi.org/search/?q="
aur "https://aur.archlinux.org/packages?O=0&K="
archwiki "https://wiki.archlinux.org/index.php?search="
wikipedia "https://en.wikipedia.org/w/index.php?search="
chatgpt "https://chatgpt.com/?q="
grok "https://grok.com/?q="
claudeai "https://claude.ai/new?q="
@@ -92,6 +97,11 @@ alias gems='web_search gems'
alias npmpkg='web_search npmpkg'
alias packagist='web_search packagist'
alias gopkg='web_search gopkg'
alias maven='web_search maven'
alias pypi='web_search pypi'
alias aur='web_search aur'
alias archwiki='web_search archwiki'
alias wikipedia='web_search wikipedia'
alias chatgpt='web_search chatgpt'
alias grokcom='web_search grok'
alias claudeai='web_search claudeai'
@@ -116,4 +126,3 @@ if [[ ${#ZSH_WEB_SEARCH_ENGINES} -gt 0 ]]; then
done
unset engines key
fi
+2 -2
View File
@@ -4,8 +4,8 @@ export LSCOLORS=dxFxCxDxBxegedabagacad
local git_branch='$(git_prompt_info)%{$reset_color%}$(git_remote_status)'
PROMPT="%{$fg[green]%}╭─%n@%m %{$reset_color%}%{$fg[yellow]%}in %~ %{$reset_color%}${git_branch}
%{$fg[green]%}╰\$ %{$reset_color%}"
PROMPT="%{%(#~$fg[red]~$fg[green])%}╭─%n@%m %{$reset_color%}%{$fg[yellow]%}in %~ %{$reset_color%}${git_branch}
%{%(#~$fg[red]~$fg[green])%}╰%(#~#~\$) %{$reset_color%}"
ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[yellow]%}on "
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
+24 -4
View File
@@ -23,12 +23,32 @@ zstyle -s ':omz:update' mode update_mode || {
# - the automatic update is disabled
# - the current user doesn't have write permissions nor owns the $ZSH directory
# - is not run from a tty
# - git is unavailable on the system
# - $ZSH is not a git repository
if [[ "$update_mode" = disabled ]] \
|| [[ ! -w "$ZSH" || ! -O "$ZSH" ]] \
|| [[ ! -t 1 && ${POWERLEVEL9K_INSTANT_PROMPT:-off} == off ]] \
|| ! command git --version >/dev/null 2>&1 \
|| [[ ! -t 1 && ${POWERLEVEL9K_INSTANT_PROMPT:-off} == off ]]; then
unset update_mode
return
fi
# Cancel update if it's not time to check yet. This runs before the git checks
# below because those fork on every startup. Skipped when a background update
# left a result that still needs to be reported (EXIT_STATUS is set).
() {
local LAST_EPOCH EXIT_STATUS ERROR epoch_target
zmodload zsh/datetime
source "${ZSH_CACHE_DIR}/.zsh-update" 2>/dev/null || return 1
[[ -n "$LAST_EPOCH" && -z "$EXIT_STATUS" ]] || return 1
zstyle -s ':omz:update' frequency epoch_target || epoch_target=${UPDATE_ZSH_DAYS:-13}
(( ( EPOCHSECONDS / 60 / 60 / 24 - LAST_EPOCH ) < epoch_target ))
} && {
unset update_mode
return
}
# Cancel update if:
# - git is unavailable on the system
# - $ZSH is not a git repository
if ! command git --version >/dev/null 2>&1 \
|| (builtin cd -q "$ZSH"; ! command git rev-parse --is-inside-work-tree &>/dev/null); then
unset update_mode
return
+4 -3
View File
@@ -528,9 +528,10 @@ print_success() {
"$(fmt_code "$(fmt_link ".zshrc" "file://$zdot/.zshrc" --text)")" \
"file to select plugins, themes, and options."
printf '\n'
printf '%s\n' "• Follow us on X: $(fmt_link @ohmyzsh https://x.com/ohmyzsh)"
printf '%s\n' "Join our Discord community: $(fmt_link "Discord server" https://discord.gg/ohmyzsh)"
printf '%s\n' "Get stickers, t-shirts, coffee mugs and more: $(fmt_link "CommitGoods Shop" https://commitgoods.com/collections/oh-my-zsh)"
printf '%s\n' "• Follow along on X ($(fmt_link @ohmyzsh https://x.com/ohmyzsh)) or Bluesky ($(fmt_link @ohmyz.sh https://bsky.app/profile/ohmyz.sh))"
printf '%s\n' "Questions or ideas? Join the community on $(fmt_link Discord https://discord.gg/ohmyzsh)"
printf '%s\n' "Help support the project: sponsor us on $(fmt_link "Open Collective" https://opencollective.com/ohmyzsh),"
printf '%s\n' " or grab stickers, shirts, and swag from $(fmt_link CommitGoods https://commitgoods.com/ohmyzsh)"
printf '%s\n' $FMT_RESET
}
+55
View File
@@ -0,0 +1,55 @@
# Theme Browser
Preview installed themes without changing your current prompt:
```zsh
omz theme browse
omz theme browse agn
omz theme preview agnoster
```
The browser filters theme names as you type and renders the highlighted theme using
the current directory, including synchronous Git information.
## Controls
| Key | Action |
| --- | --- |
| Up / Down, Ctrl-P / Ctrl-N | Previous / next theme |
| Page Up / Page Down | Move one page |
| Printable text | Filter names |
| Backspace | Delete the last filter character |
| Ctrl-U | Clear the filter |
| Enter | Open selection actions |
| Esc / Ctrl-C | Cancel without applying or saving |
| `u` in selection actions | Use the theme for this session |
| `s` in selection actions | Save as default and reload the shell |
| `b` or Enter in selection actions | Return to browsing |
Using a theme changes the current session. It does not remove hooks or widgets installed
by the previous theme. Saving updates `ZSH_THEME` in `.zshrc` and reloads the shell.
## Limitations
- Previews run in a fresh shell, so plugin-dependent and interactive themes may be incomplete.
- Right prompts are labeled and shown separately rather than positioned by ZLE.
- The browser requires an interactive terminal with alternate-screen support.
- Theme code is isolated from your active shell, but it is not sandboxed. Preview only trusted themes.
## Local Testing
From this checkout, launch a disposable interactive shell:
```zsh
env ZSH="$PWD" zsh -dfi
source "$ZSH/lib/cli.zsh"
omz theme preview robbyrussell
omz theme browse
```
Run the regression suites with:
```zsh
zsh -df tools/theme-browser/tests/preview_test.zsh
zsh -df tools/theme-browser/tests/browser_test.zsh
```
+170
View File
@@ -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
)
+88
View File
@@ -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
+217
View File
@@ -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 ]]
)
+406
View File
@@ -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 ))
+282
View File
@@ -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'
+12 -3
View File
@@ -271,9 +271,18 @@ if LANG= git pull --quiet --rebase $remote $branch; then
printf '%s %s %s %s /____/ %s %s %s %s\n' $RAINBOW $RESET
printf '\n'
printf "${BLUE}%s${RESET}\n\n" "$message"
printf "${BLUE}${BOLD}%s %s${RESET}\n" "To keep up with the latest news and updates, follow us on X:" "$(fmt_link @ohmyzsh https://x.com/ohmyzsh)"
printf "${BLUE}${BOLD}%s %s${RESET}\n" "Want to get involved in the community? Join our Discord:" "$(fmt_link "Discord server" https://discord.gg/ohmyzsh)"
printf "${BLUE}${BOLD}%s %s${RESET}\n" "Get your Oh My Zsh swag at:" "$(fmt_link "CommitGoods Shop" https://commitgoods.com/collections/oh-my-zsh)"
printf "${BLUE}${BOLD}%s %s %s %s${RESET}\n" \
"Follow along on X" "($(fmt_link @ohmyzsh https://x.com/ohmyzsh))" \
"or Bluesky" "($(fmt_link @ohmyz.sh https://bsky.app/profile/ohmyz.sh))"
printf "${BLUE}${BOLD}%s %s${RESET}\n" \
"Questions or ideas? Join the community on" \
"$(fmt_link Discord https://discord.gg/ohmyzsh)"
printf "${BLUE}${BOLD}%s %s,${RESET}\n" \
"Help support the project: sponsor us on" \
"$(fmt_link "Open Collective" https://opencollective.com/ohmyzsh)"
printf "${BLUE}${BOLD}%s %s${RESET}\n" \
"or grab stickers, shirts, and swag from" \
"$(fmt_link CommitGoods https://commitgoods.com/ohmyzsh)"
elif [[ $verbose_mode == minimal ]]; then
printf "${BLUE}%s${RESET}\n" "$message"
fi