summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rwxr-xr-xtools/changelog.sh441
-rw-r--r--tools/check_for_upgrade.sh183
-rwxr-xr-xtools/install.sh569
-rwxr-xr-xtools/theme_chooser.sh1
-rw-r--r--tools/uninstall.sh16
-rwxr-xr-x[-rw-r--r--]tools/upgrade.sh222
6 files changed, 1116 insertions, 316 deletions
diff --git a/tools/changelog.sh b/tools/changelog.sh
new file mode 100755
index 000000000..7329a9526
--- /dev/null
+++ b/tools/changelog.sh
@@ -0,0 +1,441 @@
+#!/usr/bin/env zsh
+
+##############################
+# CHANGELOG SCRIPT CONSTANTS #
+##############################
+
+#* Holds the list of valid types recognized in a commit subject
+#* and the display string of such type
+local -A TYPES
+TYPES=(
+ build "Build system"
+ chore "Chore"
+ ci "CI"
+ docs "Documentation"
+ feat "Features"
+ fix "Bug fixes"
+ perf "Performance"
+ refactor "Refactor"
+ style "Style"
+ test "Testing"
+)
+
+#* Types that will be displayed in their own section,
+#* in the order specified here.
+local -a MAIN_TYPES
+MAIN_TYPES=(feat fix perf docs)
+
+#* Types that will be displayed under the category of other changes
+local -a OTHER_TYPES
+OTHER_TYPES=(refactor style other)
+
+#* Commit types that don't appear in $MAIN_TYPES nor $OTHER_TYPES
+#* will not be displayed and will simply be ignored.
+
+
+############################
+# COMMIT PARSING UTILITIES #
+############################
+
+function parse-commit {
+
+ # This function uses the following globals as output: commits (A),
+ # subjects (A), scopes (A) and breaking (A). All associative arrays (A)
+ # have $hash as the key.
+ # - commits holds the commit type
+ # - subjects holds the commit subject
+ # - scopes holds the scope of a commit
+ # - breaking holds the breaking change warning if a commit does
+ # make a breaking change
+
+ function commit:type {
+ local type="$(sed -E 's/^([a-zA-Z_\-]+)(\(.+\))?!?: .+$/\1/' <<< "$1")"
+
+ # If $type doesn't appear in $TYPES array mark it as 'other'
+ if [[ -n "${(k)TYPES[(i)$type]}" ]]; then
+ echo $type
+ else
+ echo other
+ fi
+ }
+
+ function commit:scope {
+ local scope
+
+ # Try to find scope in "type(<scope>):" format
+ scope=$(sed -nE 's/^[a-zA-Z_\-]+\((.+)\)!?: .+$/\1/p' <<< "$1")
+ if [[ -n "$scope" ]]; then
+ echo "$scope"
+ return
+ fi
+
+ # If no scope found, try to find it in "<scope>:" format
+ # Make sure it's not a type before printing it
+ scope=$(sed -nE 's/^([a-zA-Z_\-]+): .+$/\1/p' <<< "$1")
+ if [[ -z "${(k)TYPES[(i)$scope]}" ]]; then
+ echo "$scope"
+ fi
+ }
+
+ function commit:subject {
+ # Only display the relevant part of the commit, i.e. if it has the format
+ # type[(scope)!]: subject, where the part between [] is optional, only
+ # displays subject. If it doesn't match the format, returns the whole string.
+ sed -E 's/^[a-zA-Z_\-]+(\(.+\))?!?: (.+)$/\2/' <<< "$1"
+ }
+
+ # Return subject if the body or subject match the breaking change format
+ function commit:is-breaking {
+ local subject="$1" body="$2" message
+
+ if [[ "$body" =~ "BREAKING CHANGE: (.*)" || \
+ "$subject" =~ '^[^ :\)]+\)?!: (.*)$' ]]; then
+ message="${match[1]}"
+ # remove CR characters (might be inserted in GitHub UI commit description form)
+ message="${message//$'\r'/}"
+ # skip next paragraphs (separated by two newlines or more)
+ message="${message%%$'\n\n'*}"
+ # ... and replace newlines with spaces
+ echo "${message//$'\n'/ }"
+ else
+ return 1
+ fi
+ }
+
+ # Return truncated hash of the reverted commit
+ function commit:is-revert {
+ local subject="$1" body="$2"
+
+ if [[ "$subject" = Revert* && \
+ "$body" =~ "This reverts commit ([^.]+)\." ]]; then
+ echo "${match[1]:0:7}"
+ else
+ return 1
+ fi
+ }
+
+ # Ignore commit if it is a merge commit
+ if [[ $(command git show -s --format=%p $1 | wc -w) -gt 1 ]]; then
+ return
+ fi
+
+ # Parse commit with hash $1
+ local hash="$1" subject body warning rhash
+ subject="$(command git show -s --format=%s $hash)"
+ body="$(command git show -s --format=%b $hash)"
+
+ # Commits following Conventional Commits (https://www.conventionalcommits.org/)
+ # have the following format, where parts between [] are optional:
+ #
+ # type[(scope)][!]: subject
+ #
+ # commit body
+ # [BREAKING CHANGE: warning]
+
+ # commits holds the commit type
+ commits[$hash]="$(commit:type "$subject")"
+ # scopes holds the commit scope
+ scopes[$hash]="$(commit:scope "$subject")"
+ # subjects holds the commit subject
+ subjects[$hash]="$(commit:subject "$subject")"
+
+ # breaking holds whether a commit has breaking changes
+ # and its warning message if it does
+ if warning=$(commit:is-breaking "$subject" "$body"); then
+ breaking[$hash]="$warning"
+ fi
+
+ # reverts holds commits reverted in the same release
+ if rhash=$(commit:is-revert "$subject" "$body"); then
+ reverts[$hash]=$rhash
+ fi
+}
+
+#############################
+# RELEASE CHANGELOG DISPLAY #
+#############################
+
+function display-release {
+
+ # This function uses the following globals: output, version,
+ # commits (A), subjects (A), scopes (A), breaking (A) and reverts (A).
+ #
+ # - output is the output format to use when formatting (raw|text|md)
+ # - version is the version in which the commits are made
+ # - commits, subjects, scopes, breaking, and reverts are associative arrays
+ # with commit hashes as keys
+
+ # Remove commits that were reverted
+ local hash rhash
+ for hash rhash in ${(kv)reverts}; do
+ if (( ${+commits[$rhash]} )); then
+ # Remove revert commit
+ unset "commits[$hash]" "subjects[$hash]" "scopes[$hash]" "breaking[$hash]"
+ # Remove reverted commit
+ unset "commits[$rhash]" "subjects[$rhash]" "scopes[$rhash]" "breaking[$rhash]"
+ fi
+ done
+
+ # If no commits left skip displaying the release
+ if (( $#commits == 0 )); then
+ return
+ fi
+
+ # Get length of longest scope for padding
+ local max_scope=0
+ for hash in ${(k)scopes}; do
+ max_scope=$(( max_scope < ${#scopes[$hash]} ? ${#scopes[$hash]} : max_scope ))
+ done
+
+ ##* Formatting functions
+
+ # Format the hash according to output format
+ # If no parameter is passed, assume it comes from `$hash`
+ function fmt:hash {
+ #* Uses $hash from outer scope
+ local hash="${1:-$hash}"
+ case "$output" in
+ raw) printf "$hash" ;;
+ text) printf "\e[33m$hash\e[0m" ;; # red
+ md) printf "[\`$hash\`](https://github.com/ohmyzsh/ohmyzsh/commit/$hash)" ;;
+ esac
+ }
+
+ # Format headers according to output format
+ # Levels 1 to 2 are considered special, the rest are formatted
+ # the same, except in md output format.
+ function fmt:header {
+ local header="$1" level="$2"
+ case "$output" in
+ raw)
+ case "$level" in
+ 1) printf "$header\n$(printf '%.0s=' {1..${#header}})\n\n" ;;
+ 2) printf "$header\n$(printf '%.0s-' {1..${#header}})\n\n" ;;
+ *) printf "$header:\n\n" ;;
+ esac ;;
+ text)
+ case "$level" in
+ 1|2) printf "\e[1;4m$header\e[0m\n\n" ;; # bold, underlined
+ *) printf "\e[1m$header:\e[0m\n\n" ;; # bold
+ esac ;;
+ md) printf "$(printf '%.0s#' {1..${level}}) $header\n\n" ;;
+ esac
+ }
+
+ function fmt:scope {
+ #* Uses $scopes (A) and $hash from outer scope
+ local scope="${1:-${scopes[$hash]}}"
+
+ # If no scopes, exit the function
+ if [[ $max_scope -eq 0 ]]; then
+ return
+ fi
+
+ # Get how much padding is required for this scope
+ local padding=0
+ padding=$(( max_scope < ${#scope} ? 0 : max_scope - ${#scope} ))
+ padding="${(r:$padding:: :):-}"
+
+ # If no scope, print padding and 3 spaces (equivalent to "[] ")
+ if [[ -z "$scope" ]]; then
+ printf "${padding} "
+ return
+ fi
+
+ # Print [scope]
+ case "$output" in
+ raw|md) printf "[$scope]${padding} " ;;
+ text) printf "[\e[38;5;9m$scope\e[0m]${padding} " ;; # red 9
+ esac
+ }
+
+ # If no parameter is passed, assume it comes from `$subjects[$hash]`
+ function fmt:subject {
+ #* Uses $subjects (A) and $hash from outer scope
+ local subject="${1:-${subjects[$hash]}}"
+
+ # Capitalize first letter of the subject
+ subject="${(U)subject:0:1}${subject:1}"
+
+ case "$output" in
+ raw) printf "$subject" ;;
+ # In text mode, highlight (#<issue>) and dim text between `backticks`
+ text) sed -E $'s|#([0-9]+)|\e[32m#\\1\e[0m|g;s|`([^`]+)`|`\e[2m\\1\e[0m`|g' <<< "$subject" ;;
+ # In markdown mode, link to (#<issue>) issues
+ md) sed -E 's|#([0-9]+)|[#\1](https://github.com/ohmyzsh/ohmyzsh/issues/\1)|g' <<< "$subject" ;;
+ esac
+ }
+
+ function fmt:type {
+ #* Uses $type from outer scope
+ local type="${1:-${TYPES[$type]:-${(C)type}}}"
+ [[ -z "$type" ]] && return 0
+ case "$output" in
+ raw|md) printf "$type: " ;;
+ text) printf "\e[4m$type\e[24m: " ;; # underlined
+ esac
+ }
+
+ ##* Section functions
+
+ function display:version {
+ fmt:header "$version" 2
+ }
+
+ function display:breaking {
+ (( $#breaking != 0 )) || return 0
+
+ case "$output" in
+ text) fmt:header "\e[31mBREAKING CHANGES" 3 ;;
+ raw) fmt:header "BREAKING CHANGES" 3 ;;
+ md) fmt:header "BREAKING CHANGES ⚠" 3 ;;
+ esac
+
+ local hash message
+ local wrap_width=$(( (COLUMNS < 100 ? COLUMNS : 100) - 3 ))
+ for hash message in ${(kv)breaking}; do
+ # Format the BREAKING CHANGE message by word-wrapping it at maximum 100
+ # characters (use $COLUMNS if smaller than 100)
+ message="$(fmt -w $wrap_width <<< "$message")"
+ # Display hash and scope in their own line, and then the full message with
+ # blank lines as separators and a 3-space left padding
+ echo " - $(fmt:hash) $(fmt:scope)\n\n$(fmt:subject "$message" | sed 's/^/ /')\n"
+ done
+ }
+
+ function display:type {
+ local hash type="$1"
+
+ local -a hashes
+ hashes=(${(k)commits[(R)$type]})
+
+ # If no commits found of type $type, go to next type
+ (( $#hashes != 0 )) || return 0
+
+ fmt:header "${TYPES[$type]}" 3
+ for hash in $hashes; do
+ echo " - $(fmt:hash) $(fmt:scope)$(fmt:subject)"
+ done | sort -k3 # sort by scope
+ echo
+ }
+
+ function display:others {
+ local hash type
+
+ # Commits made under types considered other changes
+ local -A changes
+ changes=(${(kv)commits[(R)${(j:|:)OTHER_TYPES}]})
+
+ # If no commits found under "other" types, don't display anything
+ (( $#changes != 0 )) || return 0
+
+ fmt:header "Other changes" 3
+ for hash type in ${(kv)changes}; do
+ case "$type" in
+ other) echo " - $(fmt:hash) $(fmt:scope)$(fmt:subject)" ;;
+ *) echo " - $(fmt:hash) $(fmt:scope)$(fmt:type)$(fmt:subject)" ;;
+ esac
+ done | sort -k3 # sort by scope
+ echo
+ }
+
+ ##* Release sections order
+
+ # Display version header
+ display:version
+
+ # Display breaking changes first
+ display:breaking
+
+ # Display changes for commit types in the order specified
+ for type in $MAIN_TYPES; do
+ display:type "$type"
+ done
+
+ # Display other changes
+ display:others
+}
+
+function main {
+ # $1 = until commit, $2 = since commit
+ local until="$1" since="$2"
+
+ # $3 = output format (--text|--raw|--md)
+ # --md: uses markdown formatting
+ # --raw: outputs without style
+ # --text: uses ANSI escape codes to style the output
+ local output=${${3:-"--text"}#--*}
+
+ if [[ -z "$until" ]]; then
+ until=HEAD
+ fi
+
+ if [[ -z "$since" ]]; then
+ # If $since is not specified:
+ # 1) try to find the version used before updating
+ # 2) try to find the first version tag before $until
+ since=$(command git config --get oh-my-zsh.lastVersion 2>/dev/null) || \
+ since=$(command git describe --abbrev=0 --tags "$until^" 2>/dev/null) || \
+ unset since
+ elif [[ "$since" = --all ]]; then
+ unset since
+ fi
+
+ # Commit classification arrays
+ local -A commits subjects scopes breaking reverts
+ local truncate=0 read_commits=0
+ local hash version tag
+
+ # Get the first version name:
+ # 1) try tag-like version, or
+ # 2) try name-rev, or
+ # 3) try branch name, or
+ # 4) try short hash
+ version=$(command git describe --tags $until 2>/dev/null) \
+ || version=$(command git name-rev --no-undefined --name-only --exclude="remotes/*" $until 2>/dev/null) \
+ || version=$(command git symbolic-ref --quiet --short $until 2>/dev/null) \
+ || version=$(command git rev-parse --short $until 2>/dev/null)
+
+ # Get commit list from $until commit until $since commit, or until root
+ # commit if $since is unset, in short hash form.
+ command git rev-list --abbrev-commit --abbrev=7 ${since:+$since..}$until | while read hash; do
+ # Truncate list on versions with a lot of commits
+ if [[ -z "$since" ]] && (( ++read_commits > 35 )); then
+ truncate=1
+ break
+ fi
+
+ # If we find a new release (exact tag)
+ if tag=$(command git describe --exact-match --tags $hash 2>/dev/null); then
+ # Output previous release
+ display-release
+ # Reinitialize commit storage
+ commits=()
+ subjects=()
+ scopes=()
+ breaking=()
+ reverts=()
+ # Start work on next release
+ version="$tag"
+ read_commits=1
+ fi
+
+ parse-commit "$hash"
+ done
+
+ display-release
+
+ if (( truncate )); then
+ echo " ...more commits omitted"
+ echo
+ fi
+}
+
+cd "$ZSH"
+
+# Use raw output if stdout is not a tty
+if [[ ! -t 1 && -z "$3" ]]; then
+ main "$1" "$2" --raw
+else
+ main "$@"
+fi
diff --git a/tools/check_for_upgrade.sh b/tools/check_for_upgrade.sh
index c8dedcf77..157b0cce2 100644
--- a/tools/check_for_upgrade.sh
+++ b/tools/check_for_upgrade.sh
@@ -1,62 +1,153 @@
-#!/usr/bin/env zsh
+# Migrate .zsh-update file to $ZSH_CACHE_DIR
+if [[ -f ~/.zsh-update && ! -f "${ZSH_CACHE_DIR}/.zsh-update" ]]; then
+ mv ~/.zsh-update "${ZSH_CACHE_DIR}/.zsh-update"
+fi
-zmodload zsh/datetime
+# Get user's update preferences
+#
+# Supported update modes:
+# - prompt (default): the user is asked before updating when it's time to update
+# - auto: the update is performed automatically when it's time
+# - reminder: a reminder is shown to the user when it's time to update
+# - disabled: automatic update is turned off
+zstyle -s ':omz:update' mode update_mode || update_mode=prompt
-function _current_epoch() {
- echo $(( $EPOCHSECONDS / 60 / 60 / 24 ))
-}
+# Support old-style settings
+[[ "$DISABLE_UPDATE_PROMPT" != true ]] || update_mode=auto
+[[ "$DISABLE_AUTO_UPDATE" != true ]] || update_mode=disabled
+
+# Cancel update if:
+# - the automatic update is disabled.
+# - the current user doesn't have write permissions nor owns the $ZSH directory.
+# - git is unavailable on the system.
+if [[ "$update_mode" = disabled ]] \
+ || [[ ! -w "$ZSH" || ! -O "$ZSH" ]] \
+ || ! command -v git &>/dev/null; then
+ unset update_mode
+ return
+fi
-function _update_zsh_update() {
- echo "LAST_EPOCH=$(_current_epoch)" >! ${ZSH_CACHE_DIR}/.zsh-update
+function current_epoch() {
+ zmodload zsh/datetime
+ echo $(( EPOCHSECONDS / 60 / 60 / 24 ))
}
-function _upgrade_zsh() {
- env ZSH=$ZSH sh $ZSH/tools/upgrade.sh
- # update the zsh file
- _update_zsh_update
+function is_update_available() {
+ local branch
+ branch=${"$(git -C "$ZSH" config --local oh-my-zsh.branch)":-master}
+
+ local remote remote_url remote_repo
+ remote=${"$(git -C "$ZSH" config --local oh-my-zsh.remote)":-origin}
+ remote_url=$(git -C "$ZSH" config remote.$remote.url)
+
+ local repo
+ case "$remote_url" in
+ https://github.com/*) repo=${${remote_url#https://github.com/}%.git} ;;
+ git@github.com:*) repo=${${remote_url#git@github.com:}%.git} ;;
+ *)
+ # If the remote is not using GitHub we can't check for updates
+ # Let's assume there are updates
+ return 0 ;;
+ esac
+
+ # If the remote repo is not the official one, let's assume there are updates available
+ [[ "$repo" = ohmyzsh/ohmyzsh ]] || return 0
+ local api_url="https://api.github.com/repos/${repo}/commits/${branch}"
+
+ # Get local and remote HEADs and compare them. If we can't get either assume there are updates
+ local local_head remote_head
+ local_head=$(git -C "$ZSH" rev-parse $branch 2>/dev/null) || return 0
+
+ remote_head=$(curl -fsSL -H 'Accept: application/vnd.github.v3.sha' $api_url 2>/dev/null) \
+ || remote_head=$(wget -O- --header='Accept: application/vnd.github.v3.sha' $api_url 2>/dev/null) \
+ || remote_head=$(HTTP_ACCEPT='Accept: application/vnd.github.v3.sha' fetch -o - $api_url 2>/dev/null) \
+ || return 0
+
+ # Compare local and remote HEADs
+ [[ "$local_head" != "$remote_head" ]]
}
-epoch_target=$UPDATE_ZSH_DAYS
-if [[ -z "$epoch_target" ]]; then
- # Default to old behavior
- epoch_target=13
-fi
+function update_last_updated_file() {
+ echo "LAST_EPOCH=$(current_epoch)" >! "${ZSH_CACHE_DIR}/.zsh-update"
+}
-# Cancel upgrade if the current user doesn't have write permissions for the
-# oh-my-zsh directory.
-[[ -w "$ZSH" ]] || return 0
+function update_ohmyzsh() {
+ if ZSH="$ZSH" zsh -f "$ZSH/tools/upgrade.sh" --interactive; then
+ update_last_updated_file
+ fi
+}
-# Cancel upgrade if git is unavailable on the system
-whence git >/dev/null || return 0
+() {
+ emulate -L zsh
-if mkdir "$ZSH/log/update.lock" 2>/dev/null; then
- if [ -f ${ZSH_CACHE_DIR}/.zsh-update ]; then
- . ${ZSH_CACHE_DIR}/.zsh-update
+ local epoch_target mtime option LAST_EPOCH
- if [[ -z "$LAST_EPOCH" ]]; then
- _update_zsh_update
- rmdir $ZSH/log/update.lock # TODO: fix later
- return 0
+ # Remove lock directory if older than a day
+ zmodload zsh/datetime
+ zmodload -F zsh/stat b:zstat
+ if mtime=$(zstat +mtime "$ZSH/log/update.lock" 2>/dev/null); then
+ if (( (mtime + 3600 * 24) < EPOCHSECONDS )); then
+ command rm -rf "$ZSH/log/update.lock"
fi
+ fi
- epoch_diff=$(($(_current_epoch) - $LAST_EPOCH))
- if [ $epoch_diff -gt $epoch_target ]; then
- if [ "$DISABLE_UPDATE_PROMPT" = "true" ]; then
- _upgrade_zsh
- else
- echo "[Oh My Zsh] Would you like to update? [Y/n]: \c"
- read line
- if [[ "$line" == Y* ]] || [[ "$line" == y* ]] || [ -z "$line" ]; then
- _upgrade_zsh
- else
- _update_zsh_update
- fi
- fi
- fi
+ # Check for lock directory
+ if ! command mkdir "$ZSH/log/update.lock" 2>/dev/null; then
+ return
+ fi
+
+ # Remove lock directory on exit. `return $ret` is important for when trapping a SIGINT:
+ # The return status from the function is handled specially. If it is zero, the signal is
+ # assumed to have been handled, and execution continues normally. Otherwise, the shell
+ # will behave as interrupted except that the return status of the trap is retained.
+ # This means that for a CTRL+C, the trap needs to return the same exit status so that
+ # the shell actually exits what it's running.
+ trap "
+ ret=\$?
+ unset update_mode
+ unset -f current_epoch is_update_available update_last_updated_file update_ohmyzsh 2>/dev/null
+ command rm -rf '$ZSH/log/update.lock'
+ return \$ret
+ " EXIT INT QUIT
+
+ # Create or update .zsh-update file if missing or malformed
+ if ! source "${ZSH_CACHE_DIR}/.zsh-update" 2>/dev/null || [[ -z "$LAST_EPOCH" ]]; then
+ update_last_updated_file
+ return
+ fi
+
+ # Number of days before trying to update again
+ zstyle -s ':omz:update' frequency epoch_target || epoch_target=${UPDATE_ZSH_DAYS:-13}
+ # Test if enough time has passed until the next update
+ if (( ( $(current_epoch) - $LAST_EPOCH ) < $epoch_target )); then
+ return
+ fi
+
+ # Check if there are updates available before proceeding
+ if ! is_update_available; then
+ return
+ fi
+
+ # Ask for confirmation before updating unless in auto mode
+ if [[ "$update_mode" = auto ]]; then
+ update_ohmyzsh
+ elif [[ "$update_mode" = reminder ]]; then
+ echo "[oh-my-zsh] It's time to update! You can do that by running \`omz update\`"
else
- # create the zsh file
- _update_zsh_update
+ # input sink to swallow all characters typed before the prompt
+ # and add a newline if there wasn't one after characters typed
+ while read -t -k 1 option; do true; done
+ [[ "$option" != ($'\n'|"") ]] && echo
+
+ echo -n "[oh-my-zsh] Would you like to update? [Y/n] "
+ read -r -k 1 option
+ [[ "$option" != $'\n' ]] && echo
+ case "$option" in
+ [yY$'\n']) update_ohmyzsh ;;
+ [nN]) update_last_updated_file ;;
+ esac
fi
+}
- rmdir $ZSH/log/update.lock
-fi
+unset update_mode
+unset -f current_epoch is_update_available update_last_updated_file update_ohmyzsh
diff --git a/tools/install.sh b/tools/install.sh
index 08f3db1e3..7704107c8 100755
--- a/tools/install.sh
+++ b/tools/install.sh
@@ -2,8 +2,10 @@
#
# This script should be run via curl:
# sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
-# or wget:
+# or via wget:
# sh -c "$(wget -qO- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
+# or via fetch:
+# sh -c "$(fetch -o - https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
#
# As an alternative, you can first download the install script and run it afterwards:
# wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
@@ -30,9 +32,14 @@
# --keep-zshrc: sets KEEP_ZSHRC to 'yes'
# For example:
# sh install.sh --unattended
+# or:
+# sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
#
set -e
+# Track if $ZSH was provided
+custom_zsh=${ZSH:+yes}
+
# Default settings
ZSH=${ZSH:-~/.oh-my-zsh}
REPO=${REPO:-ohmyzsh/ohmyzsh}
@@ -46,244 +53,368 @@ KEEP_ZSHRC=${KEEP_ZSHRC:-no}
command_exists() {
- command -v "$@" >/dev/null 2>&1
+ command -v "$@" >/dev/null 2>&1
+}
+
+# The [ -t 1 ] check only works when the function is not called from
+# a subshell (like in `$(...)` or `(...)`, so this hack redefines the
+# function at the top level to always return false when stdout is not
+# a tty.
+if [ -t 1 ]; then
+ is_tty() {
+ true
+ }
+else
+ is_tty() {
+ false
+ }
+fi
+
+# This function uses the logic from supports-hyperlinks[1][2], which is
+# made by Kat Marchán (@zkat) and licensed under the Apache License 2.0.
+# [1] https://github.com/zkat/supports-hyperlinks
+# [2] https://crates.io/crates/supports-hyperlinks
+#
+# Copyright (c) 2021 Kat Marchán
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+supports_hyperlinks() {
+ # $FORCE_HYPERLINK must be set and be non-zero (this acts as a logic bypass)
+ if [ -n "$FORCE_HYPERLINK" ]; then
+ [ "$FORCE_HYPERLINK" != 0 ]
+ return $?
+ fi
+
+ # If stdout is not a tty, it doesn't support hyperlinks
+ is_tty || return 1
+
+ # DomTerm terminal emulator (domterm.org)
+ if [ -n "$DOMTERM" ]; then
+ return 0
+ fi
+
+ # VTE-based terminals above v0.50 (Gnome Terminal, Guake, ROXTerm, etc)
+ if [ -n "$VTE_VERSION" ]; then
+ [ $VTE_VERSION -ge 5000 ]
+ return $?
+ fi
+
+ # If $TERM_PROGRAM is set, these terminals support hyperlinks
+ case "$TERM_PROGRAM" in
+ Hyper|iTerm.app|terminology|WezTerm) return 0 ;;
+ esac
+
+ # kitty supports hyperlinks
+ if [ "$TERM" = xterm-kitty ]; then
+ return 0
+ fi
+
+ # Windows Terminal or Konsole also support hyperlinks
+ if [ -n "$WT_SESSION" ] || [ -n "$KONSOLE_VERSION" ]; then
+ return 0
+ fi
+
+ return 1
}
-error() {
- echo ${RED}"Error: $@"${RESET} >&2
+fmt_link() {
+ # $1: text, $2: url, $3: fallback mode
+ if supports_hyperlinks; then
+ printf '\033]8;;%s\a%s\033]8;;\a\n' "$2" "$1"
+ return
+ fi
+
+ case "$3" in
+ --text) printf '%s\n' "$1" ;;
+ --url|*) fmt_underline "$2" ;;
+ esac
+}
+
+fmt_underline() {
+ is_tty && printf '\033[4m%s\033[24m\n' "$*" || printf '%s\n' "$*"
+}
+
+# shellcheck disable=SC2016 # backtick in single-quote
+fmt_code() {
+ is_tty && printf '`\033[2m%s\033[22m`\n' "$*" || printf '`%s`\n' "$*"
+}
+
+fmt_error() {
+ printf '%sError: %s%s\n' "$BOLD$RED" "$*" "$RESET" >&2
}
setup_color() {
- # Only use colors if connected to a terminal
- if [ -t 1 ]; then
- RED=$(printf '\033[31m')
- GREEN=$(printf '\033[32m')
- YELLOW=$(printf '\033[33m')
- BLUE=$(printf '\033[34m')
- BOLD=$(printf '\033[1m')
- RESET=$(printf '\033[m')
- else
- RED=""
- GREEN=""
- YELLOW=""
- BLUE=""
- BOLD=""
- RESET=""
- fi
+ # Only use colors if connected to a terminal
+ if is_tty; then
+ RAINBOW="
+ $(printf '\033[38;5;196m')
+ $(printf '\033[38;5;202m')
+ $(printf '\033[38;5;226m')
+ $(printf '\033[38;5;082m')
+ $(printf '\033[38;5;021m')
+ $(printf '\033[38;5;093m')
+ $(printf '\033[38;5;163m')
+ "
+ RED=$(printf '\033[31m')
+ GREEN=$(printf '\033[32m')
+ YELLOW=$(printf '\033[33m')
+ BLUE=$(printf '\033[34m')
+ BOLD=$(printf '\033[1m')
+ RESET=$(printf '\033[m')
+ else
+ RAINBOW=""
+ RED=""
+ GREEN=""
+ YELLOW=""
+ BLUE=""
+ BOLD=""
+ RESET=""
+ fi
}
setup_ohmyzsh() {
- # Prevent the cloned repository from having insecure permissions. Failing to do
- # so causes compinit() calls to fail with "command not found: compdef" errors
- # for users with insecure umasks (e.g., "002", allowing group writability). Note
- # that this will be ignored under Cygwin by default, as Windows ACLs take
- # precedence over umasks except for filesystems mounted with option "noacl".
- umask g-w,o-w
-
- echo "${BLUE}Cloning Oh My Zsh...${RESET}"
-
- command_exists git || {
- error "git is not installed"
- exit 1
- }
-
- if [ "$OSTYPE" = cygwin ] && git --version | grep -q msysgit; then
- error "Windows/MSYS Git is not supported on Cygwin"
- error "Make sure the Cygwin git package is installed and is first on the \$PATH"
- exit 1
- fi
-
- git clone -c core.eol=lf -c core.autocrlf=false \
- -c fsck.zeroPaddedFilemode=ignore \
- -c fetch.fsck.zeroPaddedFilemode=ignore \
- -c receive.fsck.zeroPaddedFilemode=ignore \
- --depth=1 --branch "$BRANCH" "$REMOTE" "$ZSH" || {
- error "git clone of oh-my-zsh repo failed"
- exit 1
- }
-
- echo
+ # Prevent the cloned repository from having insecure permissions. Failing to do
+ # so causes compinit() calls to fail with "command not found: compdef" errors
+ # for users with insecure umasks (e.g., "002", allowing group writability). Note
+ # that this will be ignored under Cygwin by default, as Windows ACLs take
+ # precedence over umasks except for filesystems mounted with option "noacl".
+ umask g-w,o-w
+
+ echo "${BLUE}Cloning Oh My Zsh...${RESET}"
+
+ command_exists git || {
+ fmt_error "git is not installed"
+ exit 1
+ }
+
+ ostype=$(uname)
+ if [ -z "${ostype%CYGWIN*}" ] && git --version | grep -q msysgit; then
+ fmt_error "Windows/MSYS Git is not supported on Cygwin"
+ fmt_error "Make sure the Cygwin git package is installed and is first on the \$PATH"
+ exit 1
+ fi
+
+ git clone -c core.eol=lf -c core.autocrlf=false \
+ -c fsck.zeroPaddedFilemode=ignore \
+ -c fetch.fsck.zeroPaddedFilemode=ignore \
+ -c receive.fsck.zeroPaddedFilemode=ignore \
+ -c oh-my-zsh.remote=origin \
+ -c oh-my-zsh.branch="$BRANCH" \
+ --depth=1 --branch "$BRANCH" "$REMOTE" "$ZSH" || {
+ fmt_error "git clone of oh-my-zsh repo failed"
+ exit 1
+ }
+
+ echo
}
setup_zshrc() {
- # Keep most recent old .zshrc at .zshrc.pre-oh-my-zsh, and older ones
- # with datestamp of installation that moved them aside, so we never actually
- # destroy a user's original zshrc
- echo "${BLUE}Looking for an existing zsh config...${RESET}"
-
- # Must use this exact name so uninstall.sh can find it
- OLD_ZSHRC=~/.zshrc.pre-oh-my-zsh
- if [ -f ~/.zshrc ] || [ -h ~/.zshrc ]; then
- # Skip this if the user doesn't want to replace an existing .zshrc
- if [ $KEEP_ZSHRC = yes ]; then
- echo "${YELLOW}Found ~/.zshrc.${RESET} ${GREEN}Keeping...${RESET}"
- return
- fi
- if [ -e "$OLD_ZSHRC" ]; then
- OLD_OLD_ZSHRC="${OLD_ZSHRC}-$(date +%Y-%m-%d_%H-%M-%S)"
- if [ -e "$OLD_OLD_ZSHRC" ]; then
- error "$OLD_OLD_ZSHRC exists. Can't back up ${OLD_ZSHRC}"
- error "re-run the installer again in a couple of seconds"
- exit 1
- fi
- mv "$OLD_ZSHRC" "${OLD_OLD_ZSHRC}"
-
- echo "${YELLOW}Found old ~/.zshrc.pre-oh-my-zsh." \
- "${GREEN}Backing up to ${OLD_OLD_ZSHRC}${RESET}"
- fi
- echo "${YELLOW}Found ~/.zshrc.${RESET} ${GREEN}Backing up to ${OLD_ZSHRC}${RESET}"
- mv ~/.zshrc "$OLD_ZSHRC"
- fi
-
- echo "${GREEN}Using the Oh My Zsh template file and adding it to ~/.zshrc.${RESET}"
-
- sed "/^export ZSH=/ c\\
+ # Keep most recent old .zshrc at .zshrc.pre-oh-my-zsh, and older ones
+ # with datestamp of installation that moved them aside, so we never actually
+ # destroy a user's original zshrc
+ echo "${BLUE}Looking for an existing zsh config...${RESET}"
+
+ # Must use this exact name so uninstall.sh can find it
+ OLD_ZSHRC=~/.zshrc.pre-oh-my-zsh
+ if [ -f ~/.zshrc ] || [ -h ~/.zshrc ]; then
+ # Skip this if the user doesn't want to replace an existing .zshrc
+ if [ "$KEEP_ZSHRC" = yes ]; then
+ echo "${YELLOW}Found ~/.zshrc.${RESET} ${GREEN}Keeping...${RESET}"
+ return
+ fi
+ if [ -e "$OLD_ZSHRC" ]; then
+ OLD_OLD_ZSHRC="${OLD_ZSHRC}-$(date +%Y-%m-%d_%H-%M-%S)"
+ if [ -e "$OLD_OLD_ZSHRC" ]; then
+ fmt_error "$OLD_OLD_ZSHRC exists. Can't back up ${OLD_ZSHRC}"
+ fmt_error "re-run the installer again in a couple of seconds"
+ exit 1
+ fi
+ mv "$OLD_ZSHRC" "${OLD_OLD_ZSHRC}"
+
+ echo "${YELLOW}Found old ~/.zshrc.pre-oh-my-zsh." \
+ "${GREEN}Backing up to ${OLD_OLD_ZSHRC}${RESET}"
+ fi
+ echo "${YELLOW}Found ~/.zshrc.${RESET} ${GREEN}Backing up to ${OLD_ZSHRC}${RESET}"
+ mv ~/.zshrc "$OLD_ZSHRC"
+ fi
+
+ echo "${GREEN}Using the Oh My Zsh template file and adding it to ~/.zshrc.${RESET}"
+
+ sed "/^export ZSH=/ c\\
export ZSH=\"$ZSH\"
" "$ZSH/templates/zshrc.zsh-template" > ~/.zshrc-omztemp
- mv -f ~/.zshrc-omztemp ~/.zshrc
+ mv -f ~/.zshrc-omztemp ~/.zshrc
- echo
+ echo
}
setup_shell() {
- # Skip setup if the user wants or stdin is closed (not running interactively).
- if [ $CHSH = no ]; then
- return
- fi
-
- # If this user's login shell is already "zsh", do not attempt to switch.
- if [ "$(basename "$SHELL")" = "zsh" ]; then
- return
- fi
-
- # If this platform doesn't provide a "chsh" command, bail out.
- if ! command_exists chsh; then
- cat <<-EOF
- I can't change your shell automatically because this system does not have chsh.
- ${BLUE}Please manually change your default shell to zsh${RESET}
- EOF
- return
- fi
-
- echo "${BLUE}Time to change your default shell to zsh:${RESET}"
-
- # Prompt for user choice on changing the default login shell
- printf "${YELLOW}Do you want to change your default shell to zsh? [Y/n]${RESET} "
- read opt
- case $opt in
- y*|Y*|"") echo "Changing the shell..." ;;
- n*|N*) echo "Shell change skipped."; return ;;
- *) echo "Invalid choice. Shell change skipped."; return ;;
- esac
-
- # Check if we're running on Termux
- case "$PREFIX" in
- *com.termux*) termux=true; zsh=zsh ;;
- *) termux=false ;;
- esac
-
- if [ "$termux" != true ]; then
- # Test for the right location of the "shells" file
- if [ -f /etc/shells ]; then
- shells_file=/etc/shells
- elif [ -f /usr/share/defaults/etc/shells ]; then # Solus OS
- shells_file=/usr/share/defaults/etc/shells
- else
- error "could not find /etc/shells file. Change your default shell manually."
- return
- fi
-
- # Get the path to the right zsh binary
- # 1. Use the most preceding one based on $PATH, then check that it's in the shells file
- # 2. If that fails, get a zsh path from the shells file, then check it actually exists
- if ! zsh=$(which zsh) || ! grep -qx "$zsh" "$shells_file"; then
- if ! zsh=$(grep '^/.*/zsh$' "$shells_file" | tail -1) || [ ! -f "$zsh" ]; then
- error "no zsh binary found or not present in '$shells_file'"
- error "change your default shell manually."
- return
- fi
- fi
- fi
-
- # We're going to change the default shell, so back up the current one
- if [ -n "$SHELL" ]; then
- echo $SHELL > ~/.shell.pre-oh-my-zsh
- else
- grep "^$USER:" /etc/passwd | awk -F: '{print $7}' > ~/.shell.pre-oh-my-zsh
- fi
-
- # Actually change the default shell to zsh
- if ! chsh -s "$zsh"; then
- error "chsh command unsuccessful. Change your default shell manually."
- else
- export SHELL="$zsh"
- echo "${GREEN}Shell successfully changed to '$zsh'.${RESET}"
- fi
-
- echo
+ # Skip setup if the user wants or stdin is closed (not running interactively).
+ if [ "$CHSH" = no ]; then
+ return
+ fi
+
+ # If this user's login shell is already "zsh", do not attempt to switch.
+ if [ "$(basename -- "$SHELL")" = "zsh" ]; then
+ return
+ fi
+
+ # If this platform doesn't provide a "chsh" command, bail out.
+ if ! command_exists chsh; then
+ cat <<EOF
+I can't change your shell automatically because this system does not have chsh.
+${BLUE}Please manually change your default shell to zsh${RESET}
+EOF
+ return
+ fi
+
+ echo "${BLUE}Time to change your default shell to zsh:${RESET}"
+
+ # Prompt for user choice on changing the default login shell
+ printf '%sDo you want to change your default shell to zsh? [Y/n]%s ' \
+ "$YELLOW" "$RESET"
+ read -r opt
+ case $opt in
+ y*|Y*|"") echo "Changing the shell..." ;;
+ n*|N*) echo "Shell change skipped."; return ;;
+ *) echo "Invalid choice. Shell change skipped."; return ;;
+ esac
+
+ # Check if we're running on Termux
+ case "$PREFIX" in
+ *com.termux*) termux=true; zsh=zsh ;;
+ *) termux=false ;;
+ esac
+
+ if [ "$termux" != true ]; then
+ # Test for the right location of the "shells" file
+ if [ -f /etc/shells ]; then
+ shells_file=/etc/shells
+ elif [ -f /usr/share/defaults/etc/shells ]; then # Solus OS
+ shells_file=/usr/share/defaults/etc/shells
+ else
+ fmt_error "could not find /etc/shells file. Change your default shell manually."
+ return
+ fi
+
+ # Get the path to the right zsh binary
+ # 1. Use the most preceding one based on $PATH, then check that it's in the shells file
+ # 2. If that fails, get a zsh path from the shells file, then check it actually exists
+ if ! zsh=$(command -v zsh) || ! grep -qx "$zsh" "$shells_file"; then
+ if ! zsh=$(grep '^/.*/zsh$' "$shells_file" | tail -1) || [ ! -f "$zsh" ]; then
+ fmt_error "no zsh binary found or not present in '$shells_file'"
+ fmt_error "change your default shell manually."
+ return
+ fi
+ fi
+ fi
+
+ # We're going to change the default shell, so back up the current one
+ if [ -n "$SHELL" ]; then
+ echo "$SHELL" > ~/.shell.pre-oh-my-zsh
+ else
+ grep "^$USERNAME:" /etc/passwd | awk -F: '{print $7}' > ~/.shell.pre-oh-my-zsh
+ fi
+
+ # Actually change the default shell to zsh
+ if ! chsh -s "$zsh"; then
+ fmt_error "chsh command unsuccessful. Change your default shell manually."
+ else
+ export SHELL="$zsh"
+ echo "${GREEN}Shell successfully changed to '$zsh'.${RESET}"
+ fi
+
+ echo
+}
+
+# shellcheck disable=SC2183 # printf string has more %s than arguments ($RAINBOW expands to multiple arguments)
+print_success() {
+ printf '%s %s__ %s %s %s %s %s__ %s\n' $RAINBOW $RESET
+ printf '%s ____ %s/ /_ %s ____ ___ %s__ __ %s ____ %s_____%s/ /_ %s\n' $RAINBOW $RESET
+ printf '%s / __ \%s/ __ \ %s / __ `__ \%s/ / / / %s /_ / %s/ ___/%s __ \ %s\n' $RAINBOW $RESET
+ printf '%s/ /_/ /%s / / / %s / / / / / /%s /_/ / %s / /_%s(__ )%s / / / %s\n' $RAINBOW $RESET
+ printf '%s\____/%s_/ /_/ %s /_/ /_/ /_/%s\__, / %s /___/%s____/%s_/ /_/ %s\n' $RAINBOW $RESET
+ printf '%s %s %s %s /____/ %s %s %s %s....is now installed!%s\n' $RAINBOW $GREEN $RESET
+ printf '\n'
+ printf '\n'
+ printf "%s %s %s\n" "Before you scream ${BOLD}${YELLOW}Oh My Zsh!${RESET} look over the" \
+ "$(fmt_code "$(fmt_link ".zshrc" "file://$HOME/.zshrc" --text)")" \
+ "file to select plugins, themes, and options."
+ printf '\n'
+ printf '%s\n' "• Follow us on Twitter: $(fmt_link @ohmyzsh https://twitter.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 "Planet Argon Shop" https://shop.planetargon.com/collections/oh-my-zsh)"
+ printf '%s\n' $RESET
}
main() {
- # Run as unattended if stdin is closed
- if [ ! -t 0 ]; then
- RUNZSH=no
- CHSH=no
- fi
-
- # Parse arguments
- while [ $# -gt 0 ]; do
- case $1 in
- --unattended) RUNZSH=no; CHSH=no ;;
- --skip-chsh) CHSH=no ;;
- --keep-zshrc) KEEP_ZSHRC=yes ;;
- esac
- shift
- done
-
- setup_color
-
- if ! command_exists zsh; then
- echo "${YELLOW}Zsh is not installed.${RESET} Please install zsh first."
- exit 1
- fi
-
- if [ -d "$ZSH" ]; then
- cat <<-EOF
- ${YELLOW}You already have Oh My Zsh installed.${RESET}
- You'll need to remove '$ZSH' if you want to reinstall.
- EOF
- exit 1
- fi
-
- setup_ohmyzsh
- setup_zshrc
- setup_shell
-
- printf "$GREEN"
- cat <<-'EOF'
- __ __
- ____ / /_ ____ ___ __ __ ____ _____/ /_
- / __ \/ __ \ / __ `__ \/ / / / /_ / / ___/ __ \
- / /_/ / / / / / / / / / / /_/ / / /_(__ ) / / /
- \____/_/ /_/ /_/ /_/ /_/\__, / /___/____/_/ /_/
- /____/ ....is now installed!
-
-
- Please look over the ~/.zshrc file to select plugins, themes, and options.
-
- p.s. Follow us on https://twitter.com/ohmyzsh
-
- p.p.s. Get stickers, shirts, and coffee mugs at https://shop.planetargon.com/collections/oh-my-zsh
-
- EOF
- printf "$RESET"
-
- if [ $RUNZSH = no ]; then
- echo "${YELLOW}Run zsh to try it out.${RESET}"
- exit
- fi
-
- exec zsh -l
+ # Run as unattended if stdin is not a tty
+ if [ ! -t 0 ]; then
+ RUNZSH=no
+ CHSH=no
+ fi
+
+ # Parse arguments
+ while [ $# -gt 0 ]; do
+ case $1 in
+ --unattended) RUNZSH=no; CHSH=no ;;
+ --skip-chsh) CHSH=no ;;
+ --keep-zshrc) KEEP_ZSHRC=yes ;;
+ esac
+ shift
+ done
+
+ setup_color
+
+ if ! command_exists zsh; then
+ echo "${YELLOW}Zsh is not installed.${RESET} Please install zsh first."
+ exit 1
+ fi
+
+ if [ -d "$ZSH" ]; then
+ echo "${YELLOW}The \$ZSH folder already exists ($ZSH).${RESET}"
+ if [ "$custom_zsh" = yes ]; then
+ cat <<EOF
+
+You ran the installer with the \$ZSH setting or the \$ZSH variable is
+exported. You have 3 options:
+
+1. Unset the ZSH variable when calling the installer:
+ $(fmt_code "ZSH= sh install.sh")
+2. Install Oh My Zsh to a directory that doesn't exist yet:
+ $(fmt_code "ZSH=path/to/new/ohmyzsh/folder sh install.sh")
+3. (Caution) If the folder doesn't contain important information,
+ you can just remove it with $(fmt_code "rm -r $ZSH")
+
+EOF
+ else
+ echo "You'll need to remove it if you want to reinstall."
+ fi
+ exit 1
+ fi
+
+ setup_ohmyzsh
+ setup_zshrc
+ setup_shell
+
+ print_success
+
+ if [ $RUNZSH = no ]; then
+ echo "${YELLOW}Run zsh to try it out.${RESET}"
+ exit
+ fi
+
+ exec zsh -l
}
main "$@"
diff --git a/tools/theme_chooser.sh b/tools/theme_chooser.sh
index 82ae5857c..3883f1d37 100755
--- a/tools/theme_chooser.sh
+++ b/tools/theme_chooser.sh
@@ -25,6 +25,7 @@ function theme_preview() {
print "$fg[blue]${(l.((${COLUMNS}-${#THEME_NAME}-5))..─.)}$reset_color $THEME_NAME $fg[blue]───$reset_color"
source "$THEMES_DIR/$THEME"
cols=$(tput cols)
+ (exit 1)
print -P "$PROMPT $RPROMPT"
}
diff --git a/tools/uninstall.sh b/tools/uninstall.sh
index b327a0163..6a0e7b4c7 100644
--- a/tools/uninstall.sh
+++ b/tools/uninstall.sh
@@ -9,20 +9,20 @@ if [ -d ~/.oh-my-zsh ]; then
rm -rf ~/.oh-my-zsh
fi
+if [ -e ~/.zshrc ]; then
+ ZSHRC_SAVE=~/.zshrc.omz-uninstalled-$(date +%Y-%m-%d_%H-%M-%S)
+ echo "Found ~/.zshrc -- Renaming to ${ZSHRC_SAVE}"
+ mv ~/.zshrc "${ZSHRC_SAVE}"
+fi
+
echo "Looking for original zsh config..."
ZSHRC_ORIG=~/.zshrc.pre-oh-my-zsh
if [ -e "$ZSHRC_ORIG" ]; then
echo "Found $ZSHRC_ORIG -- Restoring to ~/.zshrc"
-
- if [ -e ~/.zshrc ]; then
- ZSHRC_SAVE=~/.zshrc.omz-uninstalled-$(date +%Y-%m-%d_%H-%M-%S)
- echo "Found ~/.zshrc -- Renaming to ${ZSHRC_SAVE}"
- mv ~/.zshrc "${ZSHRC_SAVE}"
- fi
-
mv "$ZSHRC_ORIG" ~/.zshrc
-
echo "Your original zsh config was restored."
+else
+ echo "No original zsh config found"
fi
if hash chsh >/dev/null 2>&1 && [ -f ~/.shell.pre-oh-my-zsh ]; then
diff --git a/tools/upgrade.sh b/tools/upgrade.sh
index 19ac88c73..7642858fe 100644..100755
--- a/tools/upgrade.sh
+++ b/tools/upgrade.sh
@@ -1,38 +1,141 @@
-# Use colors, but only if connected to a terminal, and that terminal
-# supports them.
+#!/usr/bin/env zsh
+
+# Protect against running with shells other than zsh
+if [ -z "$ZSH_VERSION" ]; then
+ exec zsh "$0" "$@"
+fi
+
+# Protect against unwanted sourcing
+case "$ZSH_EVAL_CONTEXT" in
+ *:file) echo "error: this file should not be sourced" && return ;;
+esac
+
+cd "$ZSH"
+
+# Use colors, but only if connected to a terminal
+# and that terminal supports them.
+
+# The [ -t 1 ] check only works when the function is not called from
+# a subshell (like in `$(...)` or `(...)`, so this hack redefines the
+# function at the top level to always return false when stdout is not
+# a tty.
if [ -t 1 ]; then
- RB_RED=$(printf '\033[38;5;196m')
- RB_ORANGE=$(printf '\033[38;5;202m')
- RB_YELLOW=$(printf '\033[38;5;226m')
- RB_GREEN=$(printf '\033[38;5;082m')
- RB_BLUE=$(printf '\033[38;5;021m')
- RB_INDIGO=$(printf '\033[38;5;093m')
- RB_VIOLET=$(printf '\033[38;5;163m')
+ is_tty() {
+ true
+ }
+else
+ is_tty() {
+ false
+ }
+fi
+
+# This function uses the logic from supports-hyperlinks[1][2], which is
+# made by Kat Marchán (@zkat) and licensed under the Apache License 2.0.
+# [1] https://github.com/zkat/supports-hyperlinks
+# [2] https://crates.io/crates/supports-hyperlinks
+#
+# Copyright (c) 2021 Kat Marchán
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+supports_hyperlinks() {
+ # $FORCE_HYPERLINK must be set and be non-zero (this acts as a logic bypass)
+ if [ -n "$FORCE_HYPERLINK" ]; then
+ [ "$FORCE_HYPERLINK" != 0 ]
+ return $?
+ fi
+
+ # If stdout is not a tty, it doesn't support hyperlinks
+ is_tty || return 1
+
+ # DomTerm terminal emulator (domterm.org)
+ if [ -n "$DOMTERM" ]; then
+ return 0
+ fi
+
+ # VTE-based terminals above v0.50 (Gnome Terminal, Guake, ROXTerm, etc)
+ if [ -n "$VTE_VERSION" ]; then
+ [ $VTE_VERSION -ge 5000 ]
+ return $?
+ fi
+
+ # If $TERM_PROGRAM is set, these terminals support hyperlinks
+ case "$TERM_PROGRAM" in
+ Hyper|iTerm.app|terminology|WezTerm) return 0 ;;
+ esac
+
+ # kitty supports hyperlinks
+ if [ "$TERM" = xterm-kitty ]; then
+ return 0
+ fi
+
+ # Windows Terminal or Konsole also support hyperlinks
+ if [ -n "$WT_SESSION" ] || [ -n "$KONSOLE_VERSION" ]; then
+ return 0
+ fi
+
+ return 1
+}
+
+fmt_link() {
+ # $1: text, $2: url, $3: fallback mode
+ if supports_hyperlinks; then
+ printf '\033]8;;%s\a%s\033]8;;\a\n' "$2" "$1"
+ return
+ fi
+
+ case "$3" in
+ --text) printf '%s\n' "$1" ;;
+ --url|*) fmt_underline "$2" ;;
+ esac
+}
+
+fmt_underline() {
+ is_tty && printf '\033[4m%s\033[24m\n' "$*" || printf '%s\n' "$*"
+}
+
+setopt typeset_silent
+typeset -a RAINBOW
+
+if is_tty; then
+ RAINBOW=(
+ "$(printf '\033[38;5;196m')"
+ "$(printf '\033[38;5;202m')"
+ "$(printf '\033[38;5;226m')"
+ "$(printf '\033[38;5;082m')"
+ "$(printf '\033[38;5;021m')"
+ "$(printf '\033[38;5;093m')"
+ "$(printf '\033[38;5;163m')"
+ )
RED=$(printf '\033[31m')
GREEN=$(printf '\033[32m')
YELLOW=$(printf '\033[33m')
BLUE=$(printf '\033[34m')
BOLD=$(printf '\033[1m')
- RESET=$(printf '\033[m')
-else
- RB_RED=""
- RB_ORANGE=""
- RB_YELLOW=""
- RB_GREEN=""
- RB_BLUE=""
- RB_INDIGO=""
- RB_VIOLET=""
-
- RED=""
- GREEN=""
- YELLOW=""
- BLUE=""
- BOLD=""
- RESET=""
+ RESET=$(printf '\033[0m')
fi
-cd "$ZSH"
+# Update upstream remote to ohmyzsh org
+git remote -v | while read remote url extra; do
+ case "$url" in
+ https://github.com/robbyrussell/oh-my-zsh(|.git))
+ git remote set-url "$remote" "https://github.com/ohmyzsh/ohmyzsh.git"
+ break ;;
+ git@github.com:robbyrussell/oh-my-zsh(|.git))
+ git remote set-url "$remote" "git@github.com:ohmyzsh/ohmyzsh.git"
+ break ;;
+ esac
+done
# Set git-config values known to fix git errors
# Line endings (#4069)
@@ -43,33 +146,66 @@ git config fsck.zeroPaddedFilemode ignore
git config fetch.fsck.zeroPaddedFilemode ignore
git config receive.fsck.zeroPaddedFilemode ignore
# autostash on rebase (#7172)
-resetAutoStash=$(git config --bool rebase.autoStash 2>&1)
+resetAutoStash=$(git config --bool rebase.autoStash 2>/dev/null)
git config rebase.autoStash true
-# Update upstream remote to ohmyzsh org
-remote=$(git remote -v | awk '/https:\/\/github\.com\/robbyrussell\/oh-my-zsh\.git/{ print $1; exit }')
-if [ -n "$remote" ]; then
- git remote set-url "$remote" "https://github.com/ohmyzsh/ohmyzsh.git"
-fi
+local ret=0
+
+# repository settings
+remote=${"$(git config --local oh-my-zsh.remote)":-origin}
+branch=${"$(git config --local oh-my-zsh.branch)":-master}
+
+# repository state
+last_head=$(git symbolic-ref --quiet --short HEAD || git rev-parse HEAD)
+# checkout update branch
+git checkout -q "$branch" -- || exit 1
+# branch commit before update (used in changelog)
+last_commit=$(git rev-parse "$branch")
+# Update Oh My Zsh
printf "${BLUE}%s${RESET}\n" "Updating Oh My Zsh"
-if git pull --rebase --stat origin master
-then
- printf '%s %s__ %s %s %s %s %s__ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
- printf '%s ____ %s/ /_ %s ____ ___ %s__ __ %s ____ %s_____%s/ /_ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
- printf '%s / __ \%s/ __ \ %s / __ `__ \%s/ / / / %s /_ / %s/ ___/%s __ \ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
- printf '%s/ /_/ /%s / / / %s / / / / / /%s /_/ / %s / /_%s(__ )%s / / / %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
- printf '%s\____/%s_/ /_/ %s /_/ /_/ /_/%s\__, / %s /___/%s____/%s_/ /_/ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
- printf '%s %s %s %s /____/ %s %s %s %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
- printf "${BLUE}%s\n" "Hooray! Oh My Zsh has been updated and/or is at the current version."
- printf "${BLUE}${BOLD}%s${RESET}\n" "To keep up on the latest news and updates, follow us on twitter: https://twitter.com/ohmyzsh"
- printf "${BLUE}${BOLD}%s${RESET}\n" "Get your Oh My Zsh swag at: https://shop.planetargon.com/collections/oh-my-zsh"
+if git pull --rebase --stat $remote $branch; then
+ # Check if it was really updated or not
+ if [[ "$(git rev-parse HEAD)" = "$last_commit" ]]; then
+ message="Oh My Zsh is already at the latest version."
+ else
+ message="Hooray! Oh My Zsh has been updated!"
+
+ # Save the commit prior to updating
+ git config oh-my-zsh.lastVersion "$last_commit"
+
+ # Print changelog to the terminal
+ if [[ "$1" = --interactive ]]; then
+ "$ZSH/tools/changelog.sh" HEAD "$last_commit"
+ fi
+
+ printf "${BLUE}%s \`${BOLD}%s${RESET}${BLUE}\`${RESET}\n" "You can see the changelog with" "omz changelog"
+ fi
+
+ printf '%s %s__ %s %s %s %s %s__ %s\n' $RAINBOW $RESET
+ printf '%s ____ %s/ /_ %s ____ ___ %s__ __ %s ____ %s_____%s/ /_ %s\n' $RAINBOW $RESET
+ printf '%s / __ \%s/ __ \ %s / __ `__ \%s/ / / / %s /_ / %s/ ___/%s __ \ %s\n' $RAINBOW $RESET
+ printf '%s/ /_/ /%s / / / %s / / / / / /%s /_/ / %s / /_%s(__ )%s / / / %s\n' $RAINBOW $RESET
+ printf '%s\____/%s_/ /_/ %s /_/ /_/ /_/%s\__, / %s /___/%s____/%s_/ /_/ %s\n' $RAINBOW $RESET
+ 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 Twitter:" "$(fmt_link @ohmyzsh https://twitter.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 "Planet Argon Shop" https://shop.planetargon.com/collections/oh-my-zsh)"
else
+ ret=$?
printf "${RED}%s${RESET}\n" 'There was an error updating. Try again later?'
fi
+# go back to HEAD previous to update
+git checkout -q "$last_head" --
+
# Unset git-config values set just for the upgrade
case "$resetAutoStash" in
"") git config --unset rebase.autoStash ;;
*) git config rebase.autoStash "$resetAutoStash" ;;
esac
+
+# Exit with `1` if the update failed
+exit $ret