1#!/usr/bin/env zsh
2
3function omz {
4  [[ $# -gt 0 ]] || {
5    _omz::help
6    return 1
7  }
8
9  local command="$1"
10  shift
11
12  # Subcommand functions start with _ so that they don't
13  # appear as completion entries when looking for `omz`
14  (( $+functions[_omz::$command] )) || {
15    _omz::help
16    return 1
17  }
18
19  _omz::$command "$@"
20}
21
22function _omz {
23  local -a cmds subcmds
24  cmds=(
25    'changelog:Print the changelog'
26    'help:Usage information'
27    'plugin:Manage plugins'
28    'pr:Manage Oh My Zsh Pull Requests'
29    'reload:Reload the current zsh session'
30    'theme:Manage themes'
31    'update:Update Oh My Zsh'
32    'version:Show the version'
33  )
34
35  if (( CURRENT == 2 )); then
36    _describe 'command' cmds
37  elif (( CURRENT == 3 )); then
38    case "$words[2]" in
39      changelog) local -a refs
40        refs=("${(@f)$(cd "$ZSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}")
41        _describe 'command' refs ;;
42      plugin) subcmds=(
43        'disable:Disable plugin(s)'
44        'enable:Enable plugin(s)'
45        'info:Get plugin information'
46        'list:List plugins'
47        'load:Load plugin(s)'
48      )
49        _describe 'command' subcmds ;;
50      pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request')
51        _describe 'command' subcmds ;;
52      theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
53        _describe 'command' subcmds ;;
54    esac
55  elif (( CURRENT == 4 )); then
56    case "${words[2]}::${words[3]}" in
57      plugin::(disable|enable|load))
58        local -aU valid_plugins
59
60        if [[ "${words[3]}" = disable ]]; then
61          # if command is "disable", only offer already enabled plugins
62          valid_plugins=($plugins)
63        else
64          valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(.N:h:t))
65          # if command is "enable", remove already enabled plugins
66          [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
67        fi
68
69        _describe 'plugin' valid_plugins ;;
70      plugin::info)
71        local -aU plugins
72        plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(.N:h:t))
73        _describe 'plugin' plugins ;;
74      theme::(set|use))
75        local -aU themes
76        themes=("$ZSH"/themes/*.zsh-theme(.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
77        _describe 'theme' themes ;;
78    esac
79  elif (( CURRENT > 4 )); then
80    case "${words[2]}::${words[3]}" in
81      plugin::(enable|disable|load))
82        local -aU valid_plugins
83
84        if [[ "${words[3]}" = disable ]]; then
85          # if command is "disable", only offer already enabled plugins
86          valid_plugins=($plugins)
87        else
88          valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(.N:h:t))
89          # if command is "enable", remove already enabled plugins
90          [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
91        fi
92
93        # Remove plugins already passed as arguments
94        # NOTE: $(( CURRENT - 1 )) is the last plugin argument completely passed, i.e. that which
95        # has a space after them. This is to avoid removing plugins partially passed, which makes
96        # the completion not add a space after the completed plugin.
97        local -a args
98        args=(${words[4,$(( CURRENT - 1))]})
99        valid_plugins=(${valid_plugins:|args})
100
101        _describe 'plugin' valid_plugins ;;
102    esac
103  fi
104
105  return 0
106}
107
108compdef _omz omz
109
110## Utility functions
111
112function _omz::confirm {
113  # If question supplied, ask it before reading the answer
114  # NOTE: uses the logname of the caller function
115  if [[ -n "$1" ]]; then
116    _omz::log prompt "$1" "${${functrace[1]#_}%:*}"
117  fi
118
119  # Read one character
120  read -r -k 1
121
122  # If no newline entered, add a newline
123  if [[ "$REPLY" != $'\n' ]]; then
124    echo
125  fi
126}
127
128function _omz::log {
129  # if promptsubst is set, a message with `` or $()
130  # will be run even if quoted due to `print -P`
131  setopt localoptions nopromptsubst
132
133  # $1 = info|warn|error|debug
134  # $2 = text
135  # $3 = (optional) name of the logger
136
137  local logtype=$1
138  local logname=${3:-${${functrace[1]#_}%:*}}
139
140  # Don't print anything if debug is not active
141  if [[ $logtype = debug && -z $_OMZ_DEBUG ]]; then
142    return
143  fi
144
145  # Choose coloring based on log type
146  case "$logtype" in
147    prompt) print -Pn "%S%F{blue}$logname%f%s: $2" ;;
148    debug) print -P "%F{white}$logname%f: $2" ;;
149    info) print -P "%F{green}$logname%f: $2" ;;
150    warn) print -P "%S%F{yellow}$logname%f%s: $2" ;;
151    error) print -P "%S%F{red}$logname%f%s: $2" ;;
152  esac >&2
153}
154
155## User-facing commands
156
157function _omz::help {
158  cat >&2 <<EOF
159Usage: omz <command> [options]
160
161Available commands:
162
163  help                Print this help message
164  changelog           Print the changelog
165  plugin <command>    Manage plugins
166  pr     <command>    Manage Oh My Zsh Pull Requests
167  reload              Reload the current zsh session
168  theme  <command>    Manage themes
169  update              Update Oh My Zsh
170  version             Show the version
171
172EOF
173}
174
175function _omz::changelog {
176  local version=${1:-HEAD} format=${3:-"--text"}
177
178  if (
179    cd "$ZSH"
180    ! command git show-ref --verify refs/heads/$version && \
181    ! command git show-ref --verify refs/tags/$version && \
182    ! command git rev-parse --verify "${version}^{commit}"
183  ) &>/dev/null; then
184    cat >&2 <<EOF
185Usage: omz changelog [version]
186
187NOTE: <version> must be a valid branch, tag or commit.
188EOF
189    return 1
190  fi
191
192  "$ZSH/tools/changelog.sh" "$version" "${2:-}" "$format"
193}
194
195function _omz::plugin {
196  (( $# > 0 && $+functions[_omz::plugin::$1] )) || {
197    cat >&2 <<EOF
198Usage: omz plugin <command> [options]
199
200Available commands:
201
202  disable <plugin> Disable plugin(s)
203  enable <plugin>  Enable plugin(s)
204  info <plugin>    Get information of a plugin
205  list             List all available Oh My Zsh plugins
206  load <plugin>    Load plugin(s)
207
208EOF
209    return 1
210  }
211
212  local command="$1"
213  shift
214
215  _omz::plugin::$command "$@"
216}
217
218function _omz::plugin::disable {
219  if [[ -z "$1" ]]; then
220    echo >&2 "Usage: omz plugin disable <plugin> [...]"
221    return 1
222  fi
223
224  # Check that plugin is in $plugins
225  local -a dis_plugins
226  for plugin in "$@"; do
227    if [[ ${plugins[(Ie)$plugin]} -eq 0 ]]; then
228      _omz::log warn "plugin '$plugin' is not enabled."
229      continue
230    fi
231    dis_plugins+=("$plugin")
232  done
233
234  # Exit if there are no enabled plugins to disable
235  if [[ ${#dis_plugins} -eq 0 ]]; then
236    return 1
237  fi
238
239  # Remove plugins substitution awk script
240  local awk_subst_plugins="\
241  gsub(/\s+(${(j:|:)dis_plugins})/, \"\") # with spaces before
242  gsub(/(${(j:|:)dis_plugins})\s+/, \"\") # with spaces after
243  gsub(/\((${(j:|:)dis_plugins})\)/, \"\") # without spaces (only plugin)
244"
245  # Disable plugins awk script
246  local awk_script="
247# if plugins=() is in oneline form, substitute disabled plugins and go to next line
248/^\s*plugins=\([^#]+\).*\$/ {
249  $awk_subst_plugins
250  print \$0
251  next
252}
253
254# if plugins=() is in multiline form, enable multi flag and disable plugins if they're there
255/^\s*plugins=\(/ {
256  multi=1
257  $awk_subst_plugins
258  print \$0
259  next
260}
261
262# if multi flag is enabled and we find a valid closing parenthesis, remove plugins and disable multi flag
263multi == 1 && /^[^#]*\)/ {
264  multi=0
265  $awk_subst_plugins
266  print \$0
267  next
268}
269
270multi == 1 && length(\$0) > 0 {
271  $awk_subst_plugins
272  if (length(\$0) > 0) print \$0
273  next
274}
275
276{ print \$0 }
277"
278
279  local zdot="${ZDOTDIR:-$HOME}"
280  awk "$awk_script" "$zdot/.zshrc" > "$zdot/.zshrc.new" \
281  && command mv -f "$zdot/.zshrc" "$zdot/.zshrc.bck" \
282  && command mv -f "$zdot/.zshrc.new" "$zdot/.zshrc"
283
284  # Exit if the new .zshrc file wasn't created correctly
285  [[ $? -eq 0 ]] || {
286    local ret=$?
287    _omz::log error "error disabling plugins."
288    return $ret
289  }
290
291  # Exit if the new .zshrc file has syntax errors
292  if ! zsh -n "$zdot/.zshrc"; then
293    _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
294    command mv -f "$zdot/.zshrc" "$zdot/.zshrc.new"
295    command mv -f "$zdot/.zshrc.bck" "$zdot/.zshrc"
296    return 1
297  fi
298
299  # Restart the zsh session if there were no errors
300  _omz::log info "plugins disabled: ${(j:, :)dis_plugins}."
301
302  # Old zsh versions don't have ZSH_ARGZERO
303  local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
304  # Check whether to run a login shell
305  [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
306}
307
308function _omz::plugin::enable {
309  if [[ -z "$1" ]]; then
310    echo >&2 "Usage: omz plugin enable <plugin> [...]"
311    return 1
312  fi
313
314  # Check that plugin is not in $plugins
315  local -a add_plugins
316  for plugin in "$@"; do
317    if [[ ${plugins[(Ie)$plugin]} -ne 0 ]]; then
318      _omz::log warn "plugin '$plugin' is already enabled."
319      continue
320    fi
321    add_plugins+=("$plugin")
322  done
323
324  # Exit if there are no plugins to enable
325  if [[ ${#add_plugins} -eq 0 ]]; then
326    return 1
327  fi
328
329  # Enable plugins awk script
330  local awk_script="
331# if plugins=() is in oneline form, substitute ) with new plugins and go to the next line
332/^\s*plugins=\([^#]+\).*\$/ {
333  sub(/\)/, \" $add_plugins&\")
334  print \$0
335  next
336}
337
338# if plugins=() is in multiline form, enable multi flag
339/^\s*plugins=\(/ {
340  multi=1
341}
342
343# if multi flag is enabled and we find a valid closing parenthesis,
344# add new plugins and disable multi flag
345multi == 1 && /^[^#]*\)/ {
346  multi=0
347  sub(/\)/, \" $add_plugins&\")
348  print \$0
349  next
350}
351
352{ print \$0 }
353"
354
355  local zdot="${ZDOTDIR:-$HOME}"
356  awk "$awk_script" "$zdot/.zshrc" > "$zdot/.zshrc.new" \
357  && command mv -f "$zdot/.zshrc" "$zdot/.zshrc.bck" \
358  && command mv -f "$zdot/.zshrc.new" "$zdot/.zshrc"
359
360  # Exit if the new .zshrc file wasn't created correctly
361  [[ $? -eq 0 ]] || {
362    local ret=$?
363    _omz::log error "error enabling plugins."
364    return $ret
365  }
366
367  # Exit if the new .zshrc file has syntax errors
368  if ! zsh -n "$zdot/.zshrc"; then
369    _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
370    command mv -f "$zdot/.zshrc" "$zdot/.zshrc.new"
371    command mv -f "$zdot/.zshrc.bck" "$zdot/.zshrc"
372    return 1
373  fi
374
375  # Restart the zsh session if there were no errors
376  _omz::log info "plugins enabled: ${(j:, :)add_plugins}."
377
378  # Old zsh versions don't have ZSH_ARGZERO
379  local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
380  # Check whether to run a login shell
381  [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
382}
383
384function _omz::plugin::info {
385  if [[ -z "$1" ]]; then
386    echo >&2 "Usage: omz plugin info <plugin>"
387    return 1
388  fi
389
390  local readme
391  for readme in "$ZSH_CUSTOM/plugins/$1/README.md" "$ZSH/plugins/$1/README.md"; do
392    if [[ -f "$readme" ]]; then
393      (( ${+commands[less]} )) && less "$readme" || cat "$readme"
394      return 0
395    fi
396  done
397
398  if [[ -d "$ZSH_CUSTOM/plugins/$1" || -d "$ZSH/plugins/$1" ]]; then
399    _omz::log error "the '$1' plugin doesn't have a README file"
400  else
401    _omz::log error "'$1' plugin not found"
402  fi
403
404  return 1
405}
406
407function _omz::plugin::list {
408  local -a custom_plugins builtin_plugins
409  custom_plugins=("$ZSH_CUSTOM"/plugins/*(-/N:t))
410  builtin_plugins=("$ZSH"/plugins/*(-/N:t))
411
412  # If the command is being piped, print all found line by line
413  if [[ ! -t 1 ]]; then
414    print -l ${(q-)custom_plugins} ${(q-)builtin_plugins}
415    return
416  fi
417
418  if (( ${#custom_plugins} )); then
419    print -P "%U%BCustom plugins%b%u:"
420    print -l ${(q-)custom_plugins} | column -x
421  fi
422
423  if (( ${#builtin_plugins} )); then
424    (( ${#custom_plugins} )) && echo # add a line of separation
425
426    print -P "%U%BBuilt-in plugins%b%u:"
427    print -l ${(q-)builtin_plugins} | column -x
428  fi
429}
430
431function _omz::plugin::load {
432  if [[ -z "$1" ]]; then
433    echo >&2 "Usage: omz plugin load <plugin> [...]"
434    return 1
435  fi
436
437  local plugin base has_completion=0
438  for plugin in "$@"; do
439    if [[ -d "$ZSH_CUSTOM/plugins/$plugin" ]]; then
440      base="$ZSH_CUSTOM/plugins/$plugin"
441    elif [[ -d "$ZSH/plugins/$plugin" ]]; then
442      base="$ZSH/plugins/$plugin"
443    else
444      _omz::log warn "plugin '$plugin' not found"
445      continue
446    fi
447
448    # Check if its a valid plugin
449    if [[ ! -f "$base/_$plugin" && ! -f "$base/$plugin.plugin.zsh" ]]; then
450      _omz::log warn "'$plugin' is not a valid plugin"
451      continue
452    # It it is a valid plugin, add its directory to $fpath unless it is already there
453    elif (( ! ${fpath[(Ie)$base]} )); then
454      fpath=("$base" $fpath)
455    fi
456
457    # Check if it has completion to reload compinit
458    local -a comp_files
459    comp_files=($base/_*(N))
460    has_completion=$(( $#comp_files > 0 ))
461
462    # Load the plugin
463    if [[ -f "$base/$plugin.plugin.zsh" ]]; then
464      source "$base/$plugin.plugin.zsh"
465    fi
466  done
467
468  # If we have completion, we need to reload the completion
469  # We pass -D to avoid generating a new dump file, which would overwrite our
470  # current one for the next session (and we don't want that because we're not
471  # actually enabling the plugins for the next session).
472  # Note that we still have to pass -d "$_comp_dumpfile", so that compinit
473  # doesn't use the default zcompdump location (${ZDOTDIR:-$HOME}/.zcompdump).
474  if (( has_completion )); then
475    compinit -D -d "$_comp_dumpfile"
476  fi
477}
478
479function _omz::pr {
480  (( $# > 0 && $+functions[_omz::pr::$1] )) || {
481    cat >&2 <<EOF
482Usage: omz pr <command> [options]
483
484Available commands:
485
486  clean                       Delete all PR branches (ohmyzsh/pull-*)
487  test <PR_number_or_URL>     Fetch PR #NUMBER and rebase against master
488
489EOF
490    return 1
491  }
492
493  local command="$1"
494  shift
495
496  _omz::pr::$command "$@"
497}
498
499function _omz::pr::clean {
500  (
501    set -e
502    builtin cd -q "$ZSH"
503
504    # Check if there are PR branches
505    local fmt branches
506    fmt="%(color:bold blue)%(align:18,right)%(refname:short)%(end)%(color:reset) %(color:dim bold red)%(objectname:short)%(color:reset) %(color:yellow)%(contents:subject)"
507    branches="$(command git for-each-ref --sort=-committerdate --color --format="$fmt" "refs/heads/ohmyzsh/pull-*")"
508
509    # Exit if there are no PR branches
510    if [[ -z "$branches" ]]; then
511      _omz::log info "there are no Pull Request branches to remove."
512      return
513    fi
514
515    # Print found PR branches
516    echo "$branches\n"
517    # Confirm before removing the branches
518    _omz::confirm "do you want remove these Pull Request branches? [Y/n] "
519    # Only proceed if the answer is a valid yes option
520    [[ "$REPLY" != [yY$'\n'] ]] && return
521
522    _omz::log info "removing all Oh My Zsh Pull Request branches..."
523    command git branch --list 'ohmyzsh/pull-*' | while read branch; do
524      command git branch -D "$branch"
525    done
526  )
527}
528
529function _omz::pr::test {
530  # Allow $1 to be a URL to the pull request
531  if [[ "$1" = https://* ]]; then
532    1="${1:t}"
533  fi
534
535  # Check the input
536  if ! [[ -n "$1" && "$1" =~ ^[[:digit:]]+$ ]]; then
537    echo >&2 "Usage: omz pr test <PR_NUMBER_or_URL>"
538    return 1
539  fi
540
541  # Save current git HEAD
542  local branch
543  branch=$(builtin cd -q "$ZSH"; git symbolic-ref --short HEAD) || {
544    _omz::log error "error when getting the current git branch. Aborting..."
545    return 1
546  }
547
548
549  # Fetch PR onto ohmyzsh/pull-<PR_NUMBER> branch and rebase against master
550  # If any of these operations fail, undo the changes made
551  (
552    set -e
553    builtin cd -q "$ZSH"
554
555    # Get the ohmyzsh git remote
556    command git remote -v | while read remote url _; do
557      case "$url" in
558      https://github.com/ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
559      git@github.com:ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
560      esac
561    done
562
563    (( $found )) || {
564      _omz::log error "could not found the ohmyzsh git remote. Aborting..."
565      return 1
566    }
567
568    # Fetch pull request head
569    _omz::log info "fetching PR #$1 to ohmyzsh/pull-$1..."
570    command git fetch -f "$remote" refs/pull/$1/head:ohmyzsh/pull-$1 || {
571      _omz::log error "error when trying to fetch PR #$1."
572      return 1
573    }
574
575    # Rebase pull request branch against the current master
576    _omz::log info "rebasing PR #$1..."
577    command git rebase master ohmyzsh/pull-$1 || {
578      command git rebase --abort &>/dev/null
579      _omz::log warn "could not rebase PR #$1 on top of master."
580      _omz::log warn "you might not see the latest stable changes."
581      _omz::log info "run \`zsh\` to test the changes."
582      return 1
583    }
584
585    _omz::log info "fetch of PR #${1} successful."
586  )
587
588  # If there was an error, abort running zsh to test the PR
589  [[ $? -eq 0 ]] || return 1
590
591  # Run zsh to test the changes
592  _omz::log info "running \`zsh\` to test the changes. Run \`exit\` to go back."
593  command zsh -l
594
595  # After testing, go back to the previous HEAD if the user wants
596  _omz::confirm "do you want to go back to the previous branch? [Y/n] "
597  # Only proceed if the answer is a valid yes option
598  [[ "$REPLY" != [yY$'\n'] ]] && return
599
600  (
601    set -e
602    builtin cd -q "$ZSH"
603
604    command git checkout "$branch" -- || {
605      _omz::log error "could not go back to the previous branch ('$branch')."
606      return 1
607    }
608  )
609}
610
611function _omz::reload {
612  # Delete current completion cache
613  command rm -f $_comp_dumpfile $ZSH_COMPDUMP
614
615  # Old zsh versions don't have ZSH_ARGZERO
616  local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
617  # Check whether to run a login shell
618  [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
619}
620
621function _omz::theme {
622  (( $# > 0 && $+functions[_omz::theme::$1] )) || {
623    cat >&2 <<EOF
624Usage: omz theme <command> [options]
625
626Available commands:
627
628  list            List all available Oh My Zsh themes
629  set <theme>     Set a theme in your .zshrc file
630  use <theme>     Load a theme
631
632EOF
633    return 1
634  }
635
636  local command="$1"
637  shift
638
639  _omz::theme::$command "$@"
640}
641
642function _omz::theme::list {
643  local -a custom_themes builtin_themes
644  custom_themes=("$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
645  builtin_themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r))
646
647  # If the command is being piped, print all found line by line
648  if [[ ! -t 1 ]]; then
649    print -l ${(q-)custom_themes} ${(q-)builtin_themes}
650    return
651  fi
652
653  # Print theme in use
654  if [[ -n "$ZSH_THEME" ]]; then
655    print -Pn "%U%BCurrent theme%b%u: "
656    [[ $ZSH_THEME = random ]] && echo "$RANDOM_THEME (via random)" || echo "$ZSH_THEME"
657    echo
658  fi
659
660  # Print custom themes if there are any
661  if (( ${#custom_themes} )); then
662    print -P "%U%BCustom themes%b%u:"
663    print -l ${(q-)custom_themes} | column -x
664    echo
665  fi
666
667  # Print built-in themes
668  print -P "%U%BBuilt-in themes%b%u:"
669  print -l ${(q-)builtin_themes} | column -x
670}
671
672function _omz::theme::set {
673  if [[ -z "$1" ]]; then
674    echo >&2 "Usage: omz theme set <theme>"
675    return 1
676  fi
677
678  # Check that theme exists
679  if [[ ! -f "$ZSH_CUSTOM/$1.zsh-theme" ]] \
680    && [[ ! -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]] \
681    && [[ ! -f "$ZSH/themes/$1.zsh-theme" ]]; then
682    _omz::log error "%B$1%b theme not found"
683    return 1
684  fi
685
686  # Enable theme in .zshrc
687  local awk_script='
688!set && /^\s*ZSH_THEME=[^#]+.*$/ {
689  set=1
690  sub(/^\s*ZSH_THEME=[^#]+.*$/, "ZSH_THEME=\"'$1'\" # set by `omz`")
691  print $0
692  next
693}
694
695{ print $0 }
696
697END {
698  # If no ZSH_THEME= line was found, return an error
699  if (!set) exit 1
700}
701'
702
703  local zdot="${ZDOTDIR:-$HOME}"
704  awk "$awk_script" "$zdot/.zshrc" > "$zdot/.zshrc.new" \
705  || {
706    # Prepend ZSH_THEME= line to .zshrc if it doesn't exist
707    cat <<EOF
708ZSH_THEME="$1" # set by \`omz\`
709
710EOF
711    cat "$zdot/.zshrc"
712  } > "$zdot/.zshrc.new" \
713  && command mv -f "$zdot/.zshrc" "$zdot/.zshrc.bck" \
714  && command mv -f "$zdot/.zshrc.new" "$zdot/.zshrc"
715
716  # Exit if the new .zshrc file wasn't created correctly
717  [[ $? -eq 0 ]] || {
718    local ret=$?
719    _omz::log error "error setting theme."
720    return $ret
721  }
722
723  # Exit if the new .zshrc file has syntax errors
724  if ! zsh -n "$zdot/.zshrc"; then
725    _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
726    command mv -f "$zdot/.zshrc" "$zdot/.zshrc.new"
727    command mv -f "$zdot/.zshrc.bck" "$zdot/.zshrc"
728    return 1
729  fi
730
731  # Restart the zsh session if there were no errors
732  _omz::log info "'$1' theme set correctly."
733
734  # Old zsh versions don't have ZSH_ARGZERO
735  local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
736  # Check whether to run a login shell
737  [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
738}
739
740function _omz::theme::use {
741  if [[ -z "$1" ]]; then
742    echo >&2 "Usage: omz theme use <theme>"
743    return 1
744  fi
745
746  # Respect compatibility with old lookup order
747  if [[ -f "$ZSH_CUSTOM/$1.zsh-theme" ]]; then
748    source "$ZSH_CUSTOM/$1.zsh-theme"
749  elif [[ -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]]; then
750    source "$ZSH_CUSTOM/themes/$1.zsh-theme"
751  elif [[ -f "$ZSH/themes/$1.zsh-theme" ]]; then
752    source "$ZSH/themes/$1.zsh-theme"
753  else
754    _omz::log error "%B$1%b theme not found"
755    return 1
756  fi
757
758  # Update theme settings
759  ZSH_THEME="$1"
760  [[ $1 = random ]] || unset RANDOM_THEME
761}
762
763function _omz::update {
764  local last_commit=$(cd "$ZSH"; git rev-parse HEAD)
765
766  # Run update script
767  if [[ "$1" != --unattended ]]; then
768    ZSH="$ZSH" zsh -f "$ZSH/tools/upgrade.sh" --interactive || return $?
769  else
770    ZSH="$ZSH" zsh -f "$ZSH/tools/upgrade.sh" || return $?
771  fi
772
773  # Update last updated file
774  zmodload zsh/datetime
775  echo "LAST_EPOCH=$(( EPOCHSECONDS / 60 / 60 / 24 ))" >! "${ZSH_CACHE_DIR}/.zsh-update"
776  # Remove update lock if it exists
777  command rm -rf "$ZSH/log/update.lock"
778
779  # Restart the zsh session if there were changes
780  if [[ "$1" != --unattended && "$(cd "$ZSH"; git rev-parse HEAD)" != "$last_commit" ]]; then
781    # Old zsh versions don't have ZSH_ARGZERO
782    local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
783    # Check whether to run a login shell
784    [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
785  fi
786}
787
788function _omz::version {
789  (
790    cd "$ZSH"
791
792    # Get the version name:
793    # 1) try tag-like version
794    # 2) try name-rev
795    # 3) try branch name
796    local version
797    version=$(command git describe --tags HEAD 2>/dev/null) \
798    || version=$(command git name-rev --no-undefined --name-only --exclude="remotes/*" HEAD 2>/dev/null) \
799    || version=$(command git symbolic-ref --quiet --short HEAD 2>/dev/null)
800
801    # Get short hash for the current HEAD
802    local commit=$(command git rev-parse --short HEAD 2>/dev/null)
803
804    # Show version and commit hash
805    printf "%s (%s)\n" "$version" "$commit"
806  )
807}
808