]> arthur.barton.de Git - ax-zsh.git/blob - bin/axzshctl
P10k: Read config after enabling instant prompt
[ax-zsh.git] / bin / axzshctl
1 #!/usr/bin/env zsh
2 #
3 # AX-ZSH: Alex' Modular ZSH Configuration
4 # Copyright (c) 2015-2023 Alexander Barton <alex@barton.de>
5 #
6
7 # Embedded ax-common compatibility functions ...
8 function ax_msg {
9         case "$1" in
10                 "0")    c="32"; ;;
11                 "1")    c="33"; ;;
12                 "2")    c="31"; ;;
13                 "-")    c="1";  ;;
14                 *)      c="0";  ;;
15         esac
16         shift
17         printf "\e[${c}m%s\e[0m\n" "$@"
18 }
19 function ax_error {
20         ax_msg 2 "$@" >&2
21 }
22
23 function Version {
24         echo "ax-zsh -- Modular configuration system for the Z shell (ZSH)"
25         echo "Copyright (c) 2015-2023 Alexander Barton <alex@barton.de>."
26         echo "Licensed under the terms of the MIT license, see LICENSE.md for details."
27         echo "Homepage: <https://github.com/alexbarton/ax-zsh>"
28         echo
29         echo "Installation prefix: $AXZSH"
30         echo -n "Version: "
31         if [[ -d "$AXZSH/.git" && -n "$commands[git]" ]]; then
32                 echo -n "Git Commit-ID "
33                 ( cd "$AXZSH" && git describe --always )
34         else
35                 echo "unknown"
36         fi
37         echo -n "Active theme: "
38         if [[ -n "$AXZSH_THEME" ]]; then
39                 echo "${AXZSH_THEME:A:t:r}"
40         else
41                 echo "unknown"
42         fi
43         echo
44         exit 0
45 }
46
47 function Usage {
48         echo "Usage: $NAME <command> [...]"
49         echo
50         echo "  enable"
51         echo "    Enable AX-ZSH altogether."
52         echo "  disable"
53         echo "    Disable AX-ZSH altogether."
54         echo
55         echo "  enable-plugin <name|directory> [<name|directory> [...]]"
56         echo "    Enable plugin(s)."
57         echo "  disable-plugin <name> [<name> [...]]"
58         echo "    Disable plugin(s)."
59         echo "  list-enabled"
60         echo "    List enabled plugins."
61         echo "  plugin-help"
62         echo "    Show help text for a plugin (when provided by the plugin)."
63         echo
64         echo "  reset-plugins"
65         echo "    Reset active plugins to the default set."
66         echo "  enable-default-plugins"
67         echo "    Enable all default plugins."
68         echo "  check-plugins"
69         echo "    Detect plugins which are \"useful\" on this system."
70         echo
71         echo "  set-theme {<name>|-}"
72         echo "    Set active theme to <name>, or to the default."
73         echo
74         echo "  upgrade"
75         echo "    Upgrade AX-ZSH installation (requires Git)."
76         echo "  update-caches"
77         echo "    Force rebuild of all cache files."
78         echo
79         echo "  help"
80         echo "    Show this help text."
81         echo "  version"
82         echo "    Show version and setup information."
83         echo
84         exit 0
85 }
86
87 function UpdatePluginCache {
88         [[ -r "$AXZSH/cache" ]] || return 0
89
90         [[ "$1" = "-v" ]] && ax_msg - "Invalidating & updating caches ..."
91
92         if [[ -d "$ZSH_CACHE_DIR" ]]; then
93                 [[ "$1" = "-v" ]] && echo "Removing ZSH cache folder ..."
94                 rm -fr "$ZSH_CACHE_DIR"
95         fi
96
97         [[ "$1" = "-v" ]] && echo "Removing AX-ZSH cache files ..."
98         rm -rf \
99                 $AXZSH/cache/ax-io.cache \
100                 $AXZSH/cache/zlogin.cache \
101                 $AXZSH/cache/zlogout.cache \
102                 $AXZSH/cache/zprofile.cache \
103                 $AXZSH/cache/zshrc.cache \
104                 || return 1
105
106         echo "Regenerating AX-ZSH cache ..."
107         [[ -z "$AXZSH_DEBUG" ]] \
108                 && AXZSH_PLUGIN_CHECK=1 zsh -ilc '' >/dev/null \
109                 || AXZSH_PLUGIN_CHECK=1 zsh -ilc ''
110 }
111
112 function NormalizedPluginName {
113         if [[ "$1" =~ "^@?[[:alnum:]-]+/[[:alnum:]_.-]+$" ]]; then
114                 echo "${1:gs/\//#}"
115         elif [[ "$1" =~ "/" ]]; then
116                 echo "${1:t}"
117         else
118                 echo "$1"
119         fi
120 }
121
122 function EnableAXZSH {
123         for f (~/.zlogin ~/.zlogout ~/.zprofile ~/.zshrc); do
124                 ln -s "$AXZSH/ax.zsh" "$f" \
125                         || ax_error "Failed to create symbolic link for \"$f\"!"
126         done
127         if [[ -z "$AXZSH_FPATH" ]]; then
128                 echo "AX-ZSH was enabled. Now you should restart your shell, for example like this:"
129                 echo "$ exec -l \"\$SHELL\""
130         fi
131 }
132
133 function DisableAXZSH {
134         for f (~/.zlogin ~/.zlogout ~/.zprofile ~/.zshrc); do
135                 if [[ -h "$f" ]]; then
136                         rm "$f" || ax_msg 2 "Failed to remove \"$f\"!"
137                 elif [[ -e "$f" ]]; then
138                         ax_error "Error: Not removing \"$f\", it is not a symbolic link!"
139                 else
140                         ax_msg 1 "Warning: \"$f\" already does not exist. Ok."
141                 fi
142         done
143 }
144
145 function EnablePlugin {
146         local plugin=$(NormalizedPluginName "$1")
147         local dir="$AXZSH/active_plugins"
148         local name="$plugin"
149
150         if [[ -h "$dir/$plugin" ]]; then
151                 ax_msg 1 "Plugin \"$1\" already active!"
152                 return 1
153         fi
154
155         if [[ "$1" =~ "^@[[:alnum:]-]+/[[:alnum:]_.+-]+$" ]]; then
156                 # GitHub plugin repository (like OhMyZsh)
157                 local repo="${1##@}"
158                 repo="${repo%/*}"
159                 mkdir -p "$AXZSH/repos/@$repo"
160                 if [[ ! -d "$AXZSH/repos/@$repo/plugins" ]]; then
161                         ax_msg - "Cloning \"$repo\" from GitHub ..."
162                         git clone --depth=1 "https://github.com/$repo/$repo.git" \
163                          "$AXZSH/repos/@$repo" \
164                                 || ax_error "Failed to clone \"$repo\" repository!"
165                 fi
166                 plugin="@$repo/plugins/${1#*/}"
167                 echo "Trying to enable \"$1\" ..."
168         elif [[ "$1" =~ "^[[:alnum:]-]+/[[:alnum:]_.-]+$" ]]; then
169                 # GitHub plugin
170                 mkdir -p "$AXZSH/repos"
171                 if [[ ! -e "$AXZSH/repos/$plugin" ]]; then
172                         ax_msg - "Cloning module from GitHub ..."
173                         git clone --depth=1 "https://github.com/$1.git" \
174                          "$AXZSH/repos/$plugin" \
175                                 || ax_error "Failed to clone repository!"
176                 fi
177                 # Try to enable a theme in this "foreign module", but ignore
178                 # errors: we don't know if this module provides a theme or is
179                 # a "regular" plugin ...
180                 if SetTheme "$plugin" 2>/dev/null; then
181                         ax_msg 0 "Module \"$1\" was enabled as theme \"${plugin#*#}\"."
182                         # A theme was enabled: So assume that this is a theme
183                         # and don't enable it as plugin.
184                         return 0
185                 fi
186                 echo "Trying to enable \"$1\" as plugin ..."
187         elif ! [[ "$1" =~ "^[[:alnum:]_.-]+$" ]]; then
188                 ax_error "Invalid plugin name!"
189                 return 1
190         fi
191
192         for dname (
193                 "$plugin:A"
194                 "$AXZSH_PLUGIN_D/$plugin"
195                 "$ZSH_CUSTOM/$plugin"
196                 "$AXZSH/custom_plugins/$plugin"
197                 "$AXZSH/repos/$plugin"
198                 "$AXZSH/plugins/$plugin"
199                 "$AXZSH/default_plugins/$plugin"
200                 "$AXZSH/core/$plugin"
201         ); do
202                 [[ ! -d "$dname" ]] && continue
203                 mkdir -p "$dir"
204                 if ! (
205                         cd "$dir" || exit 9
206                         ln -s "$dname" "$PWD/$name"
207                 ); then
208                         ax_error "Failed to create link!"
209                         return 1
210                 fi
211                 ax_msg 0 "Plugin \"$1\" enabled."
212                 return 0
213         done
214
215         ax_error "Plugin \"$1\" not found!"
216         return 1
217 }
218
219 function DisablePlugin {
220         local plugin=$(NormalizedPluginName "$1")
221         local dir="$AXZSH/active_plugins"
222         local r=-1
223
224         # Active theme?
225         if [[ $(readlink "$AXZSH/active_theme") = "$AXZSH/repos/$plugin/"* ]]; then
226                 rm "$AXZSH/active_theme"; r=$?
227         fi
228
229         # Active plugin?
230         if [[ -h "$dir/$plugin" ]]; then
231                 rm "$dir/$plugin"; r=$?
232         fi
233
234         if [[ $r -eq -1 ]]; then
235                 ax_msg 1 "Plugin \"$1\" not active, nothing to do?"
236                 r=1
237         fi
238
239         if [[ "$plugin" = *"#"* ]]; then
240                 # Name matches a cloned repository, try to clean up!
241                 echo "Cleaning up cloned repository ..."
242                 rm -fr "$AXZSH/repos/$plugin"
243         fi
244
245         return $r
246 }
247
248 function ListEnabledPlugins {
249         for plugin ($AXZSH/active_plugins/*(N)); do
250                 print ${plugin:t:s/#/\//}
251         done
252         return 0
253 }
254
255 function PluginHelp {
256         local plugin=$(NormalizedPluginName "$1")
257         local repo_plugin=$(echo "$plugin" | sed -e 's|#|/plugins/|')
258         local plugin_found=
259
260         for plugin_d (
261                 "$plugin:A"
262                 "$AXZSH/active_plugins/$plugin"
263                 "$AXZSH/active_plugins/$repo_plugin"
264                 "$AXZSH_PLUGIN_D/$plugin"
265                 "$ZSH_CUSTOM/$plugin"
266                 "$AXZSH/custom_plugins/$plugin"
267                 "$AXZSH/repos/$plugin"
268                 "$AXZSH/repos/$repo_plugin"
269                 "$AXZSH/plugins/$plugin"
270                 "$AXZSH/default_plugins/$plugin"
271                 "$AXZSH/core/$plugin"
272         ); do
273                 [[ -e "$plugin_d" ]] && plugin_found=1
274                 [[ -r "$plugin_d/README.md" ]] || continue
275                 less "$plugin_d/README.md"
276                 return 0
277         done
278         [[ -n "$plugin_found" ]] \
279                 && echo "Plugin \"$1\" found, but no help available!" >&2 \
280                 || echo "Plugin \"$1\" not found!" >&2
281         return 1
282 }
283
284 function ResetPlugins {
285         local dir="$AXZSH/active_plugins"
286         local r1=0, r2=0
287
288         if [[ -e "$dir" ]]; then
289                 ax_msg - "Removing all symbolic links in $dir ..."
290                 find "$dir" -type l -print -delete; r1=$?
291         fi
292
293         ax_msg - "Removing all external repositories in \"$AXZSH/repos\" ..."
294         rm -fr "$AXZSH/repos"; r2=$?
295
296         [[ $r1 == 0 && $r2 == 0 ]] && return 0 || return 1
297 }
298
299 function EnableDefaultPlugins {
300         local dir="$AXZSH/active_plugins"
301
302         ax_msg - "Activating default plugins ..."
303         mkdir -p "$dir"
304         (
305                 cd "$dir" || exit 9
306                 ln -sf "$AXZSH/default_plugins/"* "$PWD"
307         )
308         return $?
309 }
310
311 function SetTheme {
312         local link_name="$AXZSH/active_theme"
313
314         # --- Powerlevel10k ---
315         # Remove "instant prompt" configuration, if any ...
316         rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
317
318         if [[ "$1" = "-" ]]; then
319                 rm -f "$link_name" || return 1
320                 ax_msg 0 "Theme settings have been reset."
321                 return 0
322         fi
323
324         if [[ -r "$1" ]]; then
325                 theme="$1"
326         elif [[ -r "$AXZSH/custom_themes/$1.axzshtheme" ]]; then
327                 theme="$AXZSH/custom_themes/$1.axzshtheme"
328         elif [[ -r "$AXZSH/themes/$1.axzshtheme" ]]; then
329                 theme="$AXZSH/themes/$1.axzshtheme"
330         else
331                 # Look for theme in specific remote module:
332                 for f (
333                         "$AXZSH/repos/$1/"*.axzshtheme(N[1])
334                         "$AXZSH/repos/$1/"*.zsh-theme(N[1])
335                 ); do
336                         if [[ -r "$f" ]]; then
337                                 theme="$f"
338                                 break
339                         fi
340                 done
341
342                 # Look for theme inside of installed plugins:
343                 for dname (
344                         "$AXZSH/custom_themes"
345                         "$AXZSH/custom_plugins/"*(N)
346                         "$AXZSH/repos/"*(N)
347                 ); do
348                         if [[ -r "$dname/$1.axzshtheme" ]]; then
349                                 theme="$dname/$1.axzshtheme"
350                                 break
351                         elif [[ -r "$dname/$1.zsh-theme" ]]; then
352                                 theme="$dname/$1.zsh-theme"
353                                 break
354                         fi
355                 done
356
357                 if [[ -z "$theme" ]]; then
358                         ax_error "Theme \"$1\" not found!"
359                         return 1
360                 fi
361         fi
362         ln -fs "$theme" "$link_name" || return 1
363         return $?
364 }
365
366 function UpgradeAXZSH {
367         if [[ $+commands[git] -eq 0 ]]; then
368                 ax_error "The git(1) command is not available!"
369                 return 1
370         fi
371         if [[ ! -d "$AXZSH/.git" ]]; then
372                 ax_error "AX-ZSH seems not to be installed using Git. Can't upgrade!"
373                 return 1
374         fi
375
376         ax_msg - "Upgrading AX-ZSH in \"$AXZSH\" using git(1) ..."
377         (
378                 set -e
379                 cd "$AXZSH"
380                 git pull --ff-only || ax_error "Git pull failed!"
381                 git log --pretty=format:"%C(yellow)%h %C(blue)%ar %C(green)%an %Creset%s" ORIG_HEAD..
382         )
383 }
384
385 function UpgradeForeignPlugins {
386         if [[ $+commands[git] -eq 0 ]]; then
387                 ax_error "The git(1) command is not available!"
388                 return 1
389         fi
390
391         for dir ($AXZSH/repos/*(N)); do
392                 name=${dir:t:s/#/\//}
393                 if [[ -d "$dir/.git" ]]; then
394                         ax_msg - "Upgrading \"$name\" [git] ..."
395                         (
396                                 set -e
397                                 cd "$dir"
398                                 git pull --ff-only || ax_error "Git pull failed!"
399                                 git log --pretty=format:"%C(yellow)%h %C(blue)%ar %C(green)%an %Creset%s" ORIG_HEAD..
400                         )
401                 else
402                         ax_error "Unknown repository type!"
403                 fi
404         done
405 }
406
407 function CheckPlugins {
408         missing_plugins=()
409         invalid_plugins=()
410
411         # Building cache file for all zshrc core files:
412         if ! T=$(mktemp); then
413                 ax_error "Failed to create temporary file!"
414                 return 1
415         fi
416         for p in $AXZSH/core/*/*.zshrc; do
417                 [[ "$(basename "$p")" == "01_zprofile.zshrc" ]] && continue
418                 printf "# BEGIN: %s\naxzsh_plugin_init()\n{\n" "$p" >>"$T"
419                 cat "$p" >>"$T"
420                 printf "}\naxzsh_plugin_init\n# END: %s\n\n" "$p" >>"$T"
421         done
422
423         ax_msg - "Checking plugins ..."
424         for dir ($AXZSH/plugins/[a-z0-9]*(N)); do
425                 plugin=${dir:t}
426
427                 # Test if plugin is already enabled
428                 if [[ -e "$AXZSH/active_plugins/$plugin" ]]; then
429                         printf ' \e[1;32m+\e[m "\e[1m%s\e[m" ... ' "${plugin}"
430                         enabled=1
431                 else
432                         printf ' \e[1;31m-\e[m "%s" ... ' "${plugin}"
433                         unset enabled
434                 fi
435
436                 # Test plugin ...
437                 new_plugin=""
438                 for script ($AXZSH/plugins/$plugin/$plugin.{zshrc,zprofile,ax-io}); do
439                         [[ -r "$script" ]] || continue
440                         (
441                                 AXZSH_PLUGIN_CHECK=1
442                                 source "$T"
443                                 axzsh_plugin_fnc() { source "$script" }
444                                 axzsh_plugin_fnc
445                         ); r=$?
446                         [[ $r -eq 0 ]] && new_plugin=$plugin
447                         break
448                 done
449                 if [[ -n "$new_plugin" ]]; then
450                         detected_plugins+=($new_plugin)
451                         [[ -n "$enabled" ]] || missing_plugins+=($new_plugin)
452                         ax_msg 0 "OK."
453                 elif [[ $r -eq 91 ]]; then
454                         ax_msg 1 "ignored."
455                 elif [[ $r -eq 92 ]]; then
456                         ax_msg 1 "optional."
457                 else
458                         [[ -n "$enabled" ]] && invalid_plugins+=($plugin)
459                         ax_msg 2 "failed ($r)."
460                 fi
461         done
462         rm -f "$T"
463         echo
464
465         result=0
466         if [[ -n "$missing_plugins" ]]; then
467                 ax_msg 1 "Run the following command to enable all missing plugins:"
468                 echo "$AXZSH/bin/axzshctl enable-plugin" $missing_plugins
469                 echo
470                 result=1
471         else
472                 ax_msg 0 "All detected plugins are already enabled."
473         fi
474
475         if [[ -n "$invalid_plugins" ]]; then
476                 ax_msg 1 "Run the following command to disable all failed plugins:"
477                 echo "$AXZSH/bin/axzshctl disable-plugin" $invalid_plugins
478                 result=1
479         else
480                 ax_msg 0 "No failed plugins are enabled."
481         fi
482
483         echo
484         return $result
485 }
486
487 NAME="$0:t"
488
489 [[ $# -gt 0 ]] || Usage
490
491 if [[ -z "$AXZSH" || ! -r "$AXZSH/ax.zsh" ]]; then
492         [[ -r "$HOME/.axzsh/ax.zsh" ]] && AXZSH="$HOME/.axzsh"
493         if [[ ! -r "$AXZSH/ax.zsh" ]]; then
494                 ax_error "Oops, \"AXZSH\" is not set or invalid and can't be autodetected!"
495                 exit 3
496         fi
497 fi
498
499 cmd="$1"
500 shift
501
502 case "$cmd" in
503         "enable")
504                 [[ $# -eq 0 ]] || Usage
505                 EnableAXZSH
506                 ;;
507         "disable")
508                 [[ $# -eq 0 ]] || Usage
509                 DisableAXZSH
510                 ;;
511         "enable-plugin")
512                 [[ $# -gt 0 ]] || Usage
513                 for plugin in "$@"; do
514                         EnablePlugin "$plugin"
515                 done
516                 UpdatePluginCache
517                 ;;
518         "disable-plugin")
519                 [[ $# -gt 0 ]] || Usage
520                 for plugin in "$@"; do
521                         DisablePlugin "$plugin"
522                 done
523                 UpdatePluginCache
524                 ;;
525         "list-enabled")
526                 [[ $# -eq 0 ]] || Usage
527                 ListEnabledPlugins
528                 ;;
529         "plugin-help")
530                 [[ $# -eq 1 ]] || Usage
531                 PluginHelp "$1"
532                 ;;
533         "reset-plugins")
534                 [[ $# -eq 0 ]] || Usage
535                 ResetPlugins
536                 EnableDefaultPlugins
537                 UpdatePluginCache
538                 ;;
539         "enable-default-plugins")
540                 [[ $# -eq 0 ]] || Usage
541                 EnableDefaultPlugins && UpdatePluginCache
542                 ;;
543         "check-plugins")
544                 [[ $# -eq 0 ]] || Usage
545                 CheckPlugins
546                 ;;
547         "set-theme")
548                 [[ $# -eq 1 ]] || Usage
549                 SetTheme "$1"
550                 ;;
551         "upgrade")
552                 [[ $# -eq 0 ]] || Usage
553                 UpgradeAXZSH
554                 UpgradeForeignPlugins
555                 UpdatePluginCache
556                 ;;
557         "update-caches")
558                 [[ $# -eq 0 ]] || Usage
559                 UpdatePluginCache -v
560                 ;;
561         "--version"|"version")
562                 Version >&2
563                 ;;
564         "--help"|"help")
565                 Usage >&2
566                 ;;
567         *)
568                 ax_error "Invalid command \"$cmd\"!"
569                 ax_error "Try \"$0 --help\" for more information."
570                 exit 2
571 esac