From 46aee25f275cef4a5c4e8c40bad3a01ba8a48883 Mon Sep 17 00:00:00 2001 From: Robby Russell Date: Sat, 5 Sep 2026 14:28:32 -0700 Subject: [PATCH] feat(cli): add `omz generate plugin` to scaffold custom plugins Adds a rails-style generator that creates a custom plugin in $ZSH_CUSTOM/plugins/ from commented templates: the plugin file, a README in the usual shape and, for plugins that wrap a command-line tool, a completion skeleton (or the cached-completion block when the tool generates its own). Anything not passed as an option is asked for interactively; non-interactive shells use defaults. `omz generate` is a new top-level command so that other generators (e.g. `omz generate theme`) can follow the same shape. Templates live in templates/generators/plugin and are syntax-checked by CI. lib/tests/generate-plugin.test.zsh covers the non-interactive path, name validation and the prompt helper. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/main.yml | 1 + README.md | 4 + lib/cli.zsh | 377 +++++++++++++++++- lib/tests/generate-plugin.test.zsh | 173 ++++++++ .../plugin/README-cache.md-template | 7 + .../plugin/README-generic.md-template | 11 + .../generators/plugin/README.md-template | 20 + .../plugin/completion-cached.zsh-template | 22 + .../plugin/completion-note.zsh-template | 7 + .../generators/plugin/completion.zsh-template | 32 ++ .../plugin/plugin-generic.zsh-template | 48 +++ .../generators/plugin/plugin.zsh-template | 43 ++ 12 files changed, 736 insertions(+), 9 deletions(-) create mode 100644 lib/tests/generate-plugin.test.zsh create mode 100644 templates/generators/plugin/README-cache.md-template create mode 100644 templates/generators/plugin/README-generic.md-template create mode 100644 templates/generators/plugin/README.md-template create mode 100644 templates/generators/plugin/completion-cached.zsh-template create mode 100644 templates/generators/plugin/completion-note.zsh-template create mode 100644 templates/generators/plugin/completion.zsh-template create mode 100644 templates/generators/plugin/plugin-generic.zsh-template create mode 100644 templates/generators/plugin/plugin.zsh-template diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6354caaee..5f4dd699b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,6 +38,7 @@ jobs: ./lib/*.zsh \ ./plugins/*/*.plugin.zsh \ ./plugins/*/_* \ + ./templates/generators/*/*.zsh-template \ ./themes/*.zsh-theme; do zsh -n "$file" || return 1 done diff --git a/README.md b/README.md index d7f143397..1b7e5ab79 100644 --- a/README.md +++ b/README.md @@ -346,6 +346,10 @@ directory. If you have many functions that go well together, you can put them as a `XYZ.plugin.zsh` file in the `custom/plugins/` directory and then enable this plugin. +The quickest way to start one is `omz generate plugin `. It creates the plugin directory with a +commented `.plugin.zsh`, a `README.md`, and (for plugins that wrap a command-line tool) a completion +skeleton, then tells you how to try it out. + If you would like to override the functionality of a plugin distributed with Oh My Zsh, create a plugin of the same name in the `custom/plugins/` directory and it will be loaded instead of the one in `plugins/`. diff --git a/lib/cli.zsh b/lib/cli.zsh index a86434411..c195cd9e7 100644 --- a/lib/cli.zsh +++ b/lib/cli.zsh @@ -24,6 +24,7 @@ function _omz { local -a cmds subcmds cmds=( 'changelog:Print the changelog' + 'generate:Run a generator, e.g. to create a custom plugin' 'help:Usage information' 'plugin:Manage plugins' 'pr:Manage Oh My Zsh Pull Requests' @@ -41,6 +42,8 @@ function _omz { changelog) local -a refs refs=("${(@f)$(builtin cd -q "$ZSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}") _describe 'command' refs ;; + generate) subcmds=('plugin:Create a custom plugin') + _describe 'generator' subcmds ;; plugin) subcmds=( 'disable:Disable plugin(s)' 'enable:Enable plugin(s)' @@ -56,6 +59,8 @@ function _omz { esac elif (( CURRENT == 4 )); then case "${words[2]}::${words[3]}" in + generate::plugin) + _omz::generate::plugin::complete_options ;; plugin::(disable|enable|load)) local -aU valid_plugins @@ -84,6 +89,8 @@ function _omz { esac elif (( CURRENT > 4 )); then case "${words[2]}::${words[3]}" in + generate::plugin) + _omz::generate::plugin::complete_options ;; plugin::(enable|disable|load)) local -aU valid_plugins @@ -169,15 +176,16 @@ Usage: omz [options] Available commands: - help Print this help message - changelog Print the changelog - plugin Manage plugins - pr Manage Oh My Zsh Pull Requests - reload Reload the current zsh session - shop Open the Oh My Zsh shop - theme Manage themes - update Update Oh My Zsh - version Show the version + help Print this help message + changelog Print the changelog + generate Run a generator, e.g. to create a custom plugin + plugin Manage plugins + pr Manage Oh My Zsh Pull Requests + reload Reload the current zsh session + shop Open the Oh My Zsh shop + theme Manage themes + update Update Oh My Zsh + version Show the version EOF } @@ -202,6 +210,357 @@ EOF ZSH="$ZSH" command zsh -f "$ZSH/tools/changelog.sh" "$version" "${2:-}" "$format" } +function _omz::generate { + (( $# > 0 && $+functions[$0::$1] )) || { + cat >&2 < [options] + +Available generators: + + plugin [] Create a custom plugin in \$ZSH_CUSTOM/plugins + +EOF + return 1 + } + + local command="$1" + shift + + $0::$command "$@" +} + +function _omz::generate::plugin { + setopt localoptions extendedglob + + local -A opts + zparseopts -D -E -A opts -- d: -description: c: -command: -no-completion -enable y -yes h -help || return 1 + + if (( ${+opts[-h]} || ${+opts[--help]} )); then + _omz::generate::plugin::usage + return 0 + fi + + # zparseopts -E leaves anything it doesn't recognise in $@ + local arg + for arg in "$@"; do + if [[ "$arg" == -* ]]; then + _omz::log error "unknown option '$arg'." + _omz::generate::plugin::usage + return 1 + fi + done + if (( $# > 1 )); then + _omz::generate::plugin::usage + return 1 + fi + + # Only ask questions when there is someone there to answer them + local interactive=0 + if [[ -o interactive && -t 0 ]] && (( ! ${+opts[-y]} && ! ${+opts[--yes]} )); then + interactive=1 + fi + + local custom="${ZSH_CUSTOM:-${ZSH:+$ZSH/custom}}" + if [[ -z "$custom" ]]; then + _omz::log error "\$ZSH is not set. Is Oh My Zsh loaded?" + return 1 + fi + + local templates="$ZSH/templates/generators/plugin" + if [[ ! -d "$templates" ]]; then + _omz::log error "templates not found at '${templates/#$HOME/\~}'. Try running 'omz update'." + return 1 + fi + + # Check we can write to $ZSH_CUSTOM/plugins, or the closest directory that exists + local probe="$custom/plugins" + while [[ ! -e "$probe" && "$probe" != "${probe:h}" ]]; do + probe="${probe:h}" + done + if [[ ! -w "$probe" ]]; then + _omz::log error "cannot write to '${probe/#$HOME/\~}'. Set \$ZSH_CUSTOM to a directory you own." + return 1 + fi + + ## Gather answers. Nothing is written until all of them are in. + + local name="$1" attempts=0 + if [[ -n "$name" ]]; then + _omz::generate::plugin::validate_name "$name" || return 1 + elif (( interactive )); then + while true; do + _omz::generate::plugin::ask "Plugin name" || return 1 + name="$REPLY" + _omz::generate::plugin::validate_name "$name" && break + if (( ++attempts >= 3 )); then + _omz::log error "giving up." + return 1 + fi + done + else + _omz::generate::plugin::usage + return 1 + fi + + local dir="$custom/plugins/$name" + if [[ -e "$dir" ]]; then + _omz::log error "'$name' already exists at '${dir/#$HOME/\~}'. Remove it or pick another name." + return 1 + fi + if [[ -d "$ZSH/plugins/$name" ]]; then + _omz::log warn "'$name' is also a built-in plugin. Your custom plugin will override it." + if (( interactive )); then + _omz::confirm "Continue? [y/N] " + if [[ "$REPLY" != [yY] ]]; then + _omz::log info "aborted." + return 1 + fi + fi + fi + + local description="${opts[-d]:-${opts[--description]:-}}" + if [[ -z "$description" ]]; then + description="$name plugin for Oh My Zsh" + if (( interactive )); then + _omz::generate::plugin::ask "Short description" "$description" || return 1 + description="$REPLY" + fi + fi + # The description goes into a comment and a README line: keep it on one line + description="${description//[[:cntrl:]]/ }" + + local cmd="${opts[-c]:-${opts[--command]:-}}" + if [[ -n "$cmd" ]]; then + _omz::generate::plugin::validate_command "$cmd" || return 1 + elif (( interactive )); then + local default_cmd="" + (( $+commands[$name] )) && default_cmd="$name" + attempts=0 + while true; do + if [[ -n "$default_cmd" ]]; then + _omz::generate::plugin::ask "Command-line tool this plugin wraps (or 'none')" "$default_cmd" || return 1 + else + _omz::generate::plugin::ask "Command-line tool this plugin wraps (blank for none)" || return 1 + fi + cmd="$REPLY" + [[ "$cmd" != (none|-) ]] || cmd="" + [[ -n "$cmd" ]] || break + _omz::generate::plugin::validate_command "$cmd" && break + if (( ++attempts >= 3 )); then + _omz::log error "giving up." + return 1 + fi + done + fi + + # completion: "" (none), "file" (a _ skeleton) or "cached" (the tool generates it) + local completion="" compgen="" + if [[ -n "$cmd" ]] && (( ! ${+opts[--no-completion]} )); then + completion=file + if (( interactive )); then + _omz::confirm "Add a completion function for $cmd? [Y/n] " + if [[ "$REPLY" == [nN] ]]; then + completion="" + else + _omz::confirm "Does $cmd generate its own zsh completion (e.g. '$cmd completion zsh')? [y/N] " + if [[ "$REPLY" == [yY] ]]; then + completion=cached + _omz::generate::plugin::ask "Command that prints the completion script" "$cmd completion zsh" || return 1 + compgen="$REPLY" + fi + fi + fi + fi + + local enable=${+opts[--enable]} + if (( interactive && ! enable )); then + _omz::confirm "Add $name to plugins=() in your .zshrc now? [y/N] " + [[ "$REPLY" != [yY] ]] || enable=1 + fi + + ## Write the files. If anything fails, remove only what we created. + + local -a created + local block="" cache="" ok=0 + { + if [[ ! -d "$custom/plugins" ]]; then + command mkdir -p "$custom/plugins" || return 1 + _omz::generate::plugin::created "$custom/plugins" + fi + command mkdir "$dir" || return 1 + _omz::generate::plugin::created "$dir" + + local plugin_tpl=plugin.zsh-template readme_tpl=README.md-template + if [[ -z "$cmd" ]]; then + plugin_tpl=plugin-generic.zsh-template + readme_tpl=README-generic.md-template + elif [[ "$completion" == cached ]]; then + block="$(_omz::generate::plugin::template completion-cached.zsh-template)" || return 1 + cache="$(_omz::generate::plugin::template README-cache.md-template)" || return 1 + else + block="$(_omz::generate::plugin::template completion-note.zsh-template)" || return 1 + fi + + _omz::generate::plugin::render "$plugin_tpl" "$dir/$name.plugin.zsh" || return 1 + _omz::generate::plugin::render "$readme_tpl" "$dir/README.md" || return 1 + if [[ "$completion" == file ]]; then + _omz::generate::plugin::render completion.zsh-template "$dir/_$cmd" || return 1 + fi + ok=1 + } always { + if (( ! ok )); then + _omz::log error "could not generate the plugin. Cleaning up..." + (( ${#created} )) && command rm -f -- "${created[@]}" + [[ ! -d "$dir" ]] || command rmdir -- "$dir" 2>/dev/null + fi + } + + print + _omz::log info "plugin '$name' generated." + print + print -r -- "Next steps:" + print + print -r -- " 1. Edit it: ${EDITOR:-vim} ${dir/#$HOME/\~}/$name.plugin.zsh" + print -r -- " 2. Try it now: omz plugin load $name" + (( enable )) || print -r -- " 3. Keep it: omz plugin enable $name" + print + print -r -- "Docs: https://github.com/ohmyzsh/ohmyzsh/wiki/Customization#overriding-and-adding-plugins" + + # Last thing we do: in an interactive shell this restarts zsh + if (( enable )); then + print + _omz::plugin::enable "$name" + fi +} + +function _omz::generate::plugin::usage { + cat >&2 <] [options] + +Creates a custom plugin in \$ZSH_CUSTOM/plugins/. Anything not given as an +option is asked for interactively. In a non-interactive shell (or with --yes) +defaults are used instead. + +Options: + -d, --description One-line description for the README and file header + -c, --command Command-line tool this plugin wraps. Adds a check + that it is installed and scaffolds its completion + --no-completion Don't scaffold a completion function + --enable Add the plugin to plugins=() in .zshrc when done + (restarts your shell) + -y, --yes Never prompt; use defaults for anything not given + -h, --help Show this help + +EOF +} + +function _omz::generate::plugin::complete_options { + local -a opts + opts=( + '--description:One-line description for the README' + '--command:Command-line tool this plugin wraps' + '--no-completion:Skip the completion scaffold' + '--enable:Enable the plugin when done' + '--yes:Never prompt' + ) + _describe -o 'options' opts +} + +# The name ends up in a mkdir path and, via --enable, inside the awk script that +# rewrites .zshrc. This is the one place it gets checked. +function _omz::generate::plugin::validate_name { + setopt localoptions extendedglob + local name="$1" + + if [[ -z "$name" ]]; then + _omz::log error "plugin name cannot be empty." + elif (( ${#name} > 64 )); then + _omz::log error "plugin name is too long (64 characters max)." + elif [[ "$name" != "${name:l}" ]]; then + _omz::log error "plugin names must be lowercase. Did you mean '${name:l}'?" + elif [[ "$name" != [a-z0-9][a-z0-9_-]# ]]; then + _omz::log error "invalid plugin name: use only lowercase letters, digits, '-' and '_', starting with a letter or digit." + else + return 0 + fi + return 1 +} + +# The command name ends up in $+commands[...], _comps[...] and a filename +function _omz::generate::plugin::validate_command { + setopt localoptions extendedglob + + if (( ${#1} > 64 )) || [[ "$1" != [[:alnum:]_][[:alnum:]_.+-]# ]]; then + _omz::log error "invalid command name: use only letters, digits, '.', '_', '+' and '-'." + return 1 + fi +} + +# Ask a question and leave the answer in $REPLY. An empty answer takes the +# default. Returns 1 when there is no more input (Ctrl-D). +function _omz::generate::plugin::ask { + setopt localoptions extendedglob + local question="$1" default="$2" + [[ -z "$default" ]] || question+=" [$default]" + + _omz::log prompt "$question: " "omz::generate::plugin" + if ! builtin read -r; then + print >&2 + _omz::log info "aborted." "omz::generate::plugin" + return 1 + fi + + [[ -n "$REPLY" ]] || REPLY="$default" + # Answers are single-line values: flatten control characters and trim + REPLY="${REPLY//[[:cntrl:]]/ }" + REPLY="${${REPLY##[[:space:]]#}%%[[:space:]]#}" +} + +# Print a template file, or explain which one is missing +function _omz::generate::plugin::template { + if [[ ! -f "$templates/$1" ]]; then + _omz::log error "missing template '${templates/#$HOME/\~}/$1'." "omz::generate::plugin" + return 1 + fi + print -r -- "$(<"$templates/$1")" +} + +# Fill in the %placeholders% of a template and write it to a file. Values come +# from the caller: $name, $cmd, $description, $compgen, $block and $cache. +# Substitution is done with parameter expansion rather than sed so that the +# values are always taken literally. +function _omz::generate::plugin::render { + setopt localoptions extendedglob + local content + content="$(_omz::generate::plugin::template "$1")" || return 1 + + # Blocks go first: they contain placeholders of their own + content="${content//'%completion%'/$block}" + content="${content//'%cache%'/$cache}" + content="${content//'%name%'/$name}" + content="${content//'%command%'/$cmd}" + content="${content//'%description%'/$description}" + content="${content//'%compgen%'/$compgen}" + # Drop blank lines left behind by an empty placeholder at the end + content="${content%%$'\n'##}" + + created+=("$2") + print -r -- "$content" > "$2" || return 1 + _omz::generate::plugin::created "$2" +} + +function _omz::generate::plugin::created { + # Colour the verb with print -P, but print the path with print -r so that a + # '%' somewhere in the path isn't treated as a prompt escape. + # Keep the output plain when it is being piped. + if [[ -t 1 ]]; then + print -Pn " %F{green}create%f " + else + print -n " create " + fi + print -r -- "${1/#$HOME/\~}" +} + function _omz::plugin { (( $# > 0 && $+functions[$0::$1] )) || { cat >&2 < +assert_files() { + local actual="$(print -l "$ZSH_CUSTOM"/plugins/$1/*(N:t) "$ZSH_CUSTOM"/plugins/$1/_*(N:t) | sort -u | tr '\n' ' ')" + local expected="$(print -l ${=2} | sort -u | tr '\n' ' ')" + if [[ "$actual" == "$expected" ]]; then + pass "$1 has files: $expected" + else + fail "$1 has files '$actual', expected '$expected'" + fi +} + +# assert_syntax : every generated file must pass zsh -n, like CI does +assert_syntax() { + local file + for file in "$ZSH_CUSTOM"/plugins/$1/*.plugin.zsh(N) "$ZSH_CUSTOM"/plugins/$1/_*(N); do + if zsh -n "$file"; then + pass "${file:t} passes zsh -n" + else + fail "${file:t} fails zsh -n" + fi + done +} + +# assert_contains +assert_contains() { + if grep -qF -- "$2" "$1"; then + pass "${1:t} contains '$2'" + else + fail "${1:t} does not contain '$2'" + fi +} + +## Templates must be valid zsh even before substitution (CI checks this too) + +for file in "$ZSH"/templates/generators/plugin/*.zsh-template; do + if zsh -n "$file"; then + pass "template ${file:t} passes zsh -n" + else + fail "template ${file:t} fails zsh -n" + fi +done + +## A plugin that wraps a command + +description='Shortcuts & stuff $(id) `id` 100%' +if omz generate plugin foo -d "$description" -c foo --yes >/dev/null 2>&1; then + pass "generates a command-wrapping plugin" +else + fail "generating a command-wrapping plugin failed" +fi +assert_files foo "README.md _foo foo.plugin.zsh" +assert_syntax foo +assert_contains "$ZSH_CUSTOM/plugins/foo/README.md" 'plugins=(... foo)' +assert_contains "$ZSH_CUSTOM/plugins/foo/README.md" "$description" +assert_contains "$ZSH_CUSTOM/plugins/foo/foo.plugin.zsh" "$description" +assert_contains "$ZSH_CUSTOM/plugins/foo/foo.plugin.zsh" '$+commands[foo]' +assert_contains "$ZSH_CUSTOM/plugins/foo/_foo" '#compdef foo' +if grep -q '%[a-z]*%' "$ZSH_CUSTOM"/plugins/foo/*; then + fail "foo still has unfilled placeholders" +else + pass "foo has no unfilled placeholders" +fi + +## A plugin with no command + +if omz generate plugin bar --yes >/dev/null 2>&1; then + pass "generates a generic plugin" +else + fail "generating a generic plugin failed" +fi +assert_files bar "README.md bar.plugin.zsh" +assert_syntax bar +assert_contains "$ZSH_CUSTOM/plugins/bar/README.md" 'This plugin does not add any aliases.' +assert_contains "$ZSH_CUSTOM/plugins/bar/bar.plugin.zsh" 'bar plugin for Oh My Zsh' + +## --no-completion, and the rails-style output + +output="$(omz generate plugin baz -c baz --no-completion --yes 2>/dev/null)" +assert_files baz "README.md baz.plugin.zsh" +create_lines=$(print -r -- "$output" | grep -c '^ create ') +if (( create_lines == 3 )); then + pass "prints one create line per path" +else + fail "expected 3 create lines, got $create_lines" +fi +if [[ "$output" == *"omz plugin load baz"* ]]; then + pass "prints next steps" +else + fail "next steps missing from output" +fi + +## Bad names are rejected without touching the filesystem + +before="$(print -l "$ZSH_CUSTOM"/plugins/*(N:t))" +for bad in '' 'Foo' 'a b' '../evil' 'a/b' '.hidden' '.' '..' '-x' 'foo`id`' 'foo"$(id)"' $'a\nb' "$(printf 'x%.0s' {1..65})"; do + if omz generate plugin "$bad" --yes >/dev/null 2>&1; then + fail "accepted bad name ${(qq)bad}" + else + pass "rejected bad name ${(qq)bad}" + fi +done +after="$(print -l "$ZSH_CUSTOM"/plugins/*(N:t))" +if [[ "$before" == "$after" ]]; then + pass "bad names created nothing" +else + fail "bad names changed \$ZSH_CUSTOM/plugins" +fi + +## Bad command names are rejected + +for bad in 'a b' '../x' 'foo;id' 'foo$(id)'; do + if omz generate plugin qux -c "$bad" --yes >/dev/null 2>&1; then + fail "accepted bad command ${(qq)bad}" + else + pass "rejected bad command ${(qq)bad}" + fi +done + +## Existing plugin, unknown option, missing templates + +if omz generate plugin foo --yes >/dev/null 2>&1; then + fail "overwrote an existing plugin" +else + pass "refuses to overwrite an existing plugin" +fi + +if omz generate plugin quux --bogus --yes >/dev/null 2>&1; then + fail "accepted an unknown option" +else + pass "rejects an unknown option" +fi + +if ( ZSH=/nonexistent; omz generate plugin quux --yes >/dev/null 2>&1 ); then + fail "ran without templates" +else + pass "fails cleanly when templates are missing" +fi +[[ -e "$ZSH_CUSTOM/plugins/quux" ]] && fail "quux was created by a failed run" + +## The prompt helper + +_omz::generate::plugin::ask "Q" "dflt" 2>/dev/null < <(print '') +[[ "$REPLY" == "dflt" ]] && pass "ask: empty answer takes the default" || fail "ask: got '$REPLY', expected 'dflt'" + +_omz::generate::plugin::ask "Q" "dflt" 2>/dev/null < <(print ' hi there ') +[[ "$REPLY" == "hi there" ]] && pass "ask: trims whitespace" || fail "ask: got '$REPLY', expected 'hi there'" + +if _omz::generate::plugin::ask "Q" 2>/dev/null < /dev/null; then + fail "ask: did not fail on EOF" +else + pass "ask: fails on EOF" +fi + +print -u2 +if (( failures )); then + print -u2 "\e[31m$failures test(s) failed\e[0m" + exit 1 +fi +print -u2 "\e[32mAll tests passed\e[0m" diff --git a/templates/generators/plugin/README-cache.md-template b/templates/generators/plugin/README-cache.md-template new file mode 100644 index 000000000..d8a74560a --- /dev/null +++ b/templates/generators/plugin/README-cache.md-template @@ -0,0 +1,7 @@ + +## 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/_%command%`. diff --git a/templates/generators/plugin/README-generic.md-template b/templates/generators/plugin/README-generic.md-template new file mode 100644 index 000000000..0811a47cd --- /dev/null +++ b/templates/generators/plugin/README-generic.md-template @@ -0,0 +1,11 @@ +# %name% plugin + +%description% + +To use it, add `%name%` to the plugins array in your zshrc file: + +```zsh +plugins=(... %name%) +``` + +This plugin does not add any aliases. diff --git a/templates/generators/plugin/README.md-template b/templates/generators/plugin/README.md-template new file mode 100644 index 000000000..fbc980a7c --- /dev/null +++ b/templates/generators/plugin/README.md-template @@ -0,0 +1,20 @@ +# %name% plugin + +%description% + +To use it, add `%name%` to the plugins array in your zshrc file: + +```zsh +plugins=(... %name%) +``` + +## Aliases + +| Alias | Command | Description | +| :----------- | ------------------ | :-------------------- | +| `%command%s` | `%command% status` | Show %command% status | + +## Requirements + +This plugin requires [%command%](https://example.com) to be installed. +%cache% diff --git a/templates/generators/plugin/completion-cached.zsh-template b/templates/generators/plugin/completion-cached.zsh-template new file mode 100644 index 000000000..a96125914 --- /dev/null +++ b/templates/generators/plugin/completion-cached.zsh-template @@ -0,0 +1,22 @@ +# +# Completion +# +# %command% generates its own zsh completion, so we cache it instead of +# maintaining a `_%command%` file by hand. +# +# On the first shell after installing this plugin the cache file doesn't exist +# yet, so compinit hasn't bound it. Do that manually here. +if [[ ! -f "$ZSH_CACHE_DIR/completions/_%command%" ]]; then + typeset -g -A _comps + autoload -Uz _%command% + _comps[%command%]=_%command% +fi + +# Regenerate the cache in the background so it never blocks shell startup. +# TMPPREFIX puts the temp file next to the destination, which keeps the move +# atomic. +zmodload -F zsh/files b:zf_mv +() { + local TMPPREFIX="$ZSH_CACHE_DIR/completions/_%command%" + zf_mv -f -- =( %compgen% ) "$TMPPREFIX" +} &| diff --git a/templates/generators/plugin/completion-note.zsh-template b/templates/generators/plugin/completion-note.zsh-template new file mode 100644 index 000000000..ec4a8af7d --- /dev/null +++ b/templates/generators/plugin/completion-note.zsh-template @@ -0,0 +1,7 @@ +# +# Completion +# +# Oh My Zsh adds this directory to $fpath, so a file named `_%command%` next to +# this one is picked up automatically as the completion for %command%. +# There is nothing to do here. +# diff --git a/templates/generators/plugin/completion.zsh-template b/templates/generators/plugin/completion.zsh-template new file mode 100644 index 000000000..7ff2448f3 --- /dev/null +++ b/templates/generators/plugin/completion.zsh-template @@ -0,0 +1,32 @@ +#compdef %command% +# +# Completion for %command%. +# +# This is a starting point. Delete what you don't need. Two references: +# https://zsh.sourceforge.io/Doc/Release/Completion-System.html +# https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide + +local -a subcommands +subcommands=( + 'status:Show the current status' + 'run:Run something' + 'help:Show help for a command' +) + +_arguments -C \ + '(-h --help)'{-h,--help}'[show help]' \ + '(-v --version)'{-v,--version}'[show the version]' \ + '1: :->subcommand' \ + '*:: :->args' + +case $state in + subcommand) + _describe -t commands '%command% subcommand' subcommands + ;; + args) + case $words[1] in + status) _arguments '--short[one-line output]' ;; + run) _files ;; + esac + ;; +esac diff --git a/templates/generators/plugin/plugin-generic.zsh-template b/templates/generators/plugin/plugin-generic.zsh-template new file mode 100644 index 000000000..b48393a85 --- /dev/null +++ b/templates/generators/plugin/plugin-generic.zsh-template @@ -0,0 +1,48 @@ +# %name% plugin +# +# %description% +# +# This file is sourced by Oh My Zsh in every new shell, so keep it fast. +# Avoid running external commands at the top level unless you really need to. +# +# Wrapping a command-line tool? Guard everything so people who don't have it +# installed don't end up with broken aliases: +# +# if (( ! $+commands[sometool] )); then +# return +# fi + +# +# Aliases +# +# Document every alias in README.md. Short, obvious, and few beats many. +# + +# alias %name%='echo "hello from %name%"' + +# +# Functions +# +# Anything that needs arguments in the middle, or more than one command, +# belongs here rather than in an alias. +# + +# function %name%_hello() { +# echo "hello, ${1:-world}" +# } + +# +# Completion +# +# Oh My Zsh adds this directory to $fpath, so a file named `_sometool` next to +# this one is picked up automatically as the completion for `sometool`. +# + +# +# Need the path to this plugin's own directory (for data files, a lib/ dir...)? +# This is the standard way to get it, since $0 is not reliable on its own: +# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html +# +# 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" +# 0="${${(M)0:#/*}:-$PWD/$0}" +# source "${0:A:h}/lib/helpers.zsh" diff --git a/templates/generators/plugin/plugin.zsh-template b/templates/generators/plugin/plugin.zsh-template new file mode 100644 index 000000000..9b6b71615 --- /dev/null +++ b/templates/generators/plugin/plugin.zsh-template @@ -0,0 +1,43 @@ +# %name% plugin +# +# %description% +# +# This file is sourced by Oh My Zsh in every new shell, so keep it fast. +# Avoid running external commands at the top level unless you really need to. + +# Only define anything if the tool is actually installed. Without this guard, +# people who don't have %command% get aliases that fail with "command not found". +if (( ! $+commands[%command%] )); then + return +fi + +# +# Aliases +# +# Document every alias in README.md. Short, obvious, and few beats many. +# The one below is just an example: replace it with something %command% can do. +# + +alias %command%s='%command% status' + +# +# Functions +# +# Anything that needs arguments in the middle, or more than one command, +# belongs here rather than in an alias. +# + +# function %name%_current() { +# %command% status --short "$@" +# } + +%completion% + +# +# Need the path to this plugin's own directory (for data files, a lib/ dir...)? +# This is the standard way to get it, since $0 is not reliable on its own: +# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html +# +# 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}" +# 0="${${(M)0:#/*}:-$PWD/$0}" +# source "${0:A:h}/lib/helpers.zsh"