diff --git a/.editorconfig b/.editorconfig index 361cd4145..d308d9465 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,6 +2,14 @@ indent_style = space indent_size = 2 +[migration/*] +indent_style = space +indent_size = 2 + +[*.sh] +indent_style = space +indent_size = 2 + [.zshrc] indent_style = space indent_size = 2 diff --git a/bin/dotly b/bin/dotly index e35a6e3d2..bd53f68f4 100755 --- a/bin/dotly +++ b/bin/dotly @@ -6,6 +6,7 @@ source "$DOTLY_PATH/scripts/core/_main.sh" ##? Usage: ##? dotly self-update +##? dotly autoupdate docs::parse "$@" case $1 in diff --git a/bin/open b/bin/open new file mode 100755 index 000000000..8bf814d83 --- /dev/null +++ b/bin/open @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Open command if open exists in system +SCRIPT_PATH="$(cd -- "$(dirname "$0")" && pwd -P)" +FULL_SCRIPT_PATH="$SCRIPT_PATH/$(basename "$0")" + +mapfile -c 1 -t < <( which -a open | grep -v "$FULL_SCRIPT_PATH" ) +OPEN_BIN="" +if [[ "${#MAPFILE[@]}" -gt 0 ]]; then + OPEN_BIN="${MAPFILE[0]}" +fi + +os=$(uname | tr '[:upper:]' '[:lower:]') + +case "$os" in + *darwin*) + if [[ -n "$OPEN_BIN" && -x "$OPEN_BIN" ]]; then + "$OPEN_BIN" "$@" + else + echo -e "\033[0;31mNot possible to use \`open\` command in this system\033[0m" + exit 4 + fi + ;; + *linux*) + if grep -q Microsoft /proc/version; then + cmd.exe /C start "$@" + elif [[ -n "$OPEN_BIN" && -x "$OPEN_BIN" ]]; then + "$OPEN_BIN" "$@" + elif ! which xdg-open | grep 'not found'; then + xdg-open "$@" + elif ! which gnome-open | grep 'not found'; then + gnome-open "$@" + else + echo -e "\033[0;31mNot possible to use \`open\` command in this system\033[0m" + exit 4 + fi + ;; + *cygwin*) + if command -v realpath &>/dev/null; then + cygstart "$@" + else + echo -e "\033[0;31mNot possible to use \`open\` command in this system\033[0m" + exit 4 + fi + ;; + *) + echo -e "\033[0;31m\`open\` command or any other known alternative does not exists in this system\033[0m" + exit 1 + ;; +esac diff --git a/dotfiles_template/.gitignore b/dotfiles_template/.gitignore new file mode 100644 index 000000000..49a7065f6 --- /dev/null +++ b/dotfiles_template/.gitignore @@ -0,0 +1,7 @@ +# START OF DOTLY GITIGNORE +.dotly_force_current_version +.dotly_update_available +.dotly_update_available_is_major +.dotly_updated +.cached_github_api_calls +# END OF DOTLY GITIGNORE diff --git a/dotfiles_template/shell/bash/.bash_profile b/dotfiles_template/shell/bash/.bash_profile index 86795d706..3ee6b293e 100644 --- a/dotfiles_template/shell/bash/.bash_profile +++ b/dotfiles_template/shell/bash/.bash_profile @@ -1 +1 @@ -source ~/.bashrc +. "$HOME/.bashrc" diff --git a/dotfiles_template/shell/bash/.bashrc b/dotfiles_template/shell/bash/.bashrc index 8bc9a70a2..a17ea8d26 100644 --- a/dotfiles_template/shell/bash/.bashrc +++ b/dotfiles_template/shell/bash/.bashrc @@ -2,42 +2,9 @@ export DOTFILES_PATH="XXX_DOTFILES_PATH_XXX" export DOTLY_PATH="$DOTFILES_PATH/modules/dotly" export DOTLY_THEME="codely" -if [[ "$(ps -p $$ -ocomm=)" =~ (bash$) ]]; then - __right_prompt() { - RIGHT_PROMPT="" - [[ -n $RPS1 ]] && RIGHT_PROMPT=$RPS1 || RIGHT_PROMPT=$RPROMPT - if [[ -n $RIGHT_PROMPT ]]; then - n=$(($COLUMNS - ${#RIGHT_PROMPT})) - printf "%${n}s$RIGHT_PROMPT\\r" - fi - } - export PROMPT_COMMAND="__right_prompt" -fi - -source "$DOTFILES_PATH/shell/init.sh" - -PATH=$( - IFS=":" - echo "${path[*]}" -) -export PATH - -themes_paths=( - "$DOTFILES_PATH/shell/bash/themes" - "$DOTLY_PATH/shell/bash/themes" -) - -for THEME_PATH in ${themes_paths[@]}; do - THEME_PATH="${THEME_PATH}/$DOTLY_THEME.sh" - [ -f "$THEME_PATH" ] && source "$THEME_PATH" && break -done - -for bash_file in "$DOTLY_PATH"/shell/bash/completions/_*; do - source "$bash_file" -done - -if [ -n "$(ls -A "$DOTFILES_PATH/shell/bash/completions/")" ]; then - for bash_file in "$DOTFILES_PATH"/shell/bash/completions/_*; do - source "$bash_file" - done +if [[ -f "$DOTLY_PATH/shell/init-dotly.sh" ]] +then + . "$DOTLY_PATH/shell/init-dotly.sh" +else + echo "\033[0;31m\033[1mDOTLY Loader could not be found, check \$DOTFILES_PATH variable\033[0m" fi diff --git a/dotfiles_template/shell/exports.sh b/dotfiles_template/shell/exports.sh index cd5e49d1d..51e1dbefc 100644 --- a/dotfiles_template/shell/exports.sh +++ b/dotfiles_template/shell/exports.sh @@ -1,3 +1,17 @@ +# ------------------------------------------------------------------------------ +# GENERAL INFORMATION ABOUT THIS FILE +# The variables here are loaded previously PATH is defined. Use full path if you +# need to do something like JAVA_HOME here or consider to add a init-script +# ------------------------------------------------------------------------------ + + +# ------------------------------------------------------------------------------ +# Dotly config +# ------------------------------------------------------------------------------ +export DOTLY_AUTO_UPDATE_PERIOD_IN_DAYS=7 +export DOTLY_AUTO_UPDATE_MODE="auto" # silent, auto, info, prompt +export DOTLY_UPDATE_VERSION="stable" # latest, stable, minor + # ------------------------------------------------------------------------------ # Codely theme config # ------------------------------------------------------------------------------ @@ -8,9 +22,10 @@ export CODELY_THEME_PROMPT_IN_NEW_LINE=false # ------------------------------------------------------------------------------ # Languages # ------------------------------------------------------------------------------ -export JAVA_HOME='/Library/Java/JavaVirtualMachines/amazon-corretto-15.jdk/Contents/Home' -export GEM_HOME="$HOME/.gem" -export GOPATH="$HOME/.go" +JAVA_HOME="$(/usr/libexec/java_home 2>&1 /dev/null)" +GEM_HOME="$HOME/.gem" +GOPATH="$HOME/.go" +export JAVA_HOME GEM_HOME GOPATH # ------------------------------------------------------------------------------ # Apps @@ -22,25 +37,3 @@ else fi export FZF_DEFAULT_OPTS="--color=$fzf_colors --reverse" - -# ------------------------------------------------------------------------------ -# Path - The higher it is, the more priority it has -# ------------------------------------------------------------------------------ -export path=( - "$HOME/bin" - "$DOTLY_PATH/bin" - "$DOTFILES_PATH/bin" - "$JAVA_HOME/bin" - "$GEM_HOME/bin" - "$GOPATH/bin" - "$HOME/.cargo/bin" - "/usr/local/opt/ruby/bin" - "/usr/local/opt/python/libexec/bin" - "/opt/homebrew/bin" - "/usr/local/bin" - "/usr/local/sbin" - "/bin" - "/usr/bin" - "/usr/sbin" - "/sbin" -) diff --git a/dotfiles_template/shell/functions.sh b/dotfiles_template/shell/functions.sh index 0db2a5f27..8b1378917 100644 --- a/dotfiles_template/shell/functions.sh +++ b/dotfiles_template/shell/functions.sh @@ -1,19 +1 @@ -function cdd() { - cd "$(ls -d -- */ | fzf)" || echo "Invalid directory" -} -function j() { - fname=$(declare -f -F _z) - - [ -n "$fname" ] || source "$DOTLY_PATH/modules/z/z.sh" - - _z "$1" -} - -function recent_dirs() { - # This script depends on pushd. It works better with autopush enabled in ZSH - escaped_home=$(echo $HOME | sed 's/\//\\\//g') - selected=$(dirs -p | sort -u | fzf) - - cd "$(echo "$selected" | sed "s/\~/$escaped_home/")" || echo "Invalid directory" -} diff --git a/dotfiles_template/shell/init-scripts.enabled/.gitkeep b/dotfiles_template/shell/init-scripts.enabled/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dotfiles_template/shell/init-scripts/.gitkeep b/dotfiles_template/shell/init-scripts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dotfiles_template/shell/init.sh b/dotfiles_template/shell/init.sh deleted file mode 100644 index a6a0f4b39..000000000 --- a/dotfiles_template/shell/init.sh +++ /dev/null @@ -1,5 +0,0 @@ -# This is a useful file to have the same aliases/functions in bash and zsh - -source "$DOTFILES_PATH/shell/aliases.sh" -source "$DOTFILES_PATH/shell/exports.sh" -source "$DOTFILES_PATH/shell/functions.sh" diff --git a/dotfiles_template/shell/paths.sh b/dotfiles_template/shell/paths.sh new file mode 100644 index 000000000..397c7580a --- /dev/null +++ b/dotfiles_template/shell/paths.sh @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------ +# Path - The higher it is, the more priority it has +# ------------------------------------------------------------------------------ +# JAVA_HOME, GEM_HOME, GOHOME, deno($HOME/.deno/bin), cargo are now loaded +# in init-dotly.sh +# Mandatory paths: /usr/local/{bin,sbin}, /bin, /usr/{bin,sbin} /sbin +# are also loaded in init-dotly +# paths defined here are loaded first +# +export path=( + "$HOME/bin" + "$DOTLY_PATH/bin" + "$DOTFILES_PATH/bin" +) diff --git a/dotfiles_template/shell/zsh/.zshrc b/dotfiles_template/shell/zsh/.zshrc index 514b62698..f53e2955d 100644 --- a/dotfiles_template/shell/zsh/.zshrc +++ b/dotfiles_template/shell/zsh/.zshrc @@ -1,26 +1,9 @@ # Uncomment for debuf with `zprof` # zmodload zsh/zprof -# ZSH Ops -setopt HIST_IGNORE_ALL_DUPS -setopt HIST_FCNTL_LOCK -setopt +o nomatch -# setopt autopushd - -# Start zim -source "$ZIM_HOME/init.zsh" - -# Async mode for autocompletion -ZSH_AUTOSUGGEST_USE_ASYNC=true -ZSH_HIGHLIGHT_MAXLENGTH=300 - -source "$DOTFILES_PATH/shell/init.sh" - -fpath=("$DOTFILES_PATH/shell/zsh/themes" "$DOTFILES_PATH/shell/zsh/autocompletions" "$DOTLY_PATH/shell/zsh/themes" "$DOTLY_PATH/shell/zsh/completions" $fpath) - -autoload -Uz promptinit && promptinit -prompt ${DOTLY_THEME:-codely} - -source "$DOTLY_PATH/shell/zsh/bindings/dot.zsh" -source "$DOTLY_PATH/shell/zsh/bindings/reverse_search.zsh" -source "$DOTFILES_PATH/shell/zsh/key-bindings.zsh" +if [[ -f "$DOTLY_PATH/shell/init-dotly.sh" ]] +then + . "$DOTLY_PATH/shell/init-dotly.sh" +else + echo "\033[0;31m\033[1mDOTLY Loader could not be found, check \$DOTFILES_PATH variable\033[0m" +fi diff --git a/dotfiles_template/symlinks/conf.macos-intel.yaml b/dotfiles_template/symlinks/conf.macos-intel.yaml index 8c7ac1f2c..7b2dfd536 100644 --- a/dotfiles_template/symlinks/conf.macos-intel.yaml +++ b/dotfiles_template/symlinks/conf.macos-intel.yaml @@ -10,9 +10,3 @@ - link: ~/.dotly: os/mac/.dotly - ~/bin/bash: /usr/local/bin/bash - ~/bin/date: /usr/local/bin/gdate - ~/bin/find: /usr/local/opt/findutils/bin/gfind - ~/bin/make: /usr/local/opt/make/libexec/gnubin/make - ~/bin/sed: /usr/local/opt/gnu-sed/libexec/gnubin/sed - ~/bin/touch: /usr/local/bin/gtouch diff --git a/dotfiles_template/symlinks/conf.macos.yaml b/dotfiles_template/symlinks/conf.macos.yaml index bdbf60b11..653ede0a4 100644 --- a/dotfiles_template/symlinks/conf.macos.yaml +++ b/dotfiles_template/symlinks/conf.macos.yaml @@ -10,10 +10,3 @@ - link: ~/.dotly: os/mac/.dotly - ~/bin/bash: /opt/homebrew/bin/bash - ~/bin/date: /opt/homebrew/bin/gdate - ~/bin/find: /opt/homebrew/bin/gfind - ~/bin/make: /opt/homebrew/bin/gmake - ~/bin/sed: /opt/homebrew/bin/gsed - ~/bin/touch: /opt/homebrew/bin/gtouch - ~/bin/zsh: /opt/homebrew/bin/zsh \ No newline at end of file diff --git a/dotfiles_template/symlinks/conf.yaml b/dotfiles_template/symlinks/conf.yaml index 3fa33d884..159364545 100644 --- a/dotfiles_template/symlinks/conf.yaml +++ b/dotfiles_template/symlinks/conf.yaml @@ -8,6 +8,9 @@ - create: - $DOTFILES_PATH/shell/bash/completions - $DOTFILES_PATH/shell/bash/themes + - $DOTLY_PATH/shell/init-scripts + - $DOTFILES_PATH/shell/init-scripts + - $DOTFILES_PATH/shell/init-scripts.enabled - link: ~/.bash_profile: shell/bash/.bash_profile diff --git a/migration/v0.1 b/migration/v0.1 new file mode 100755 index 000000000..0bcd15f41 --- /dev/null +++ b/migration/v0.1 @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +echo "No migration actions needed for v0.1" diff --git a/migration/v2.0.0 b/migration/v2.0.0 new file mode 100755 index 000000000..c1f1965e6 --- /dev/null +++ b/migration/v2.0.0 @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +DOTFILES_TEMPLATE_PATH="$DOTLY_PATH/dotfiles_template" + +files_to_backup=( + "$DOTFILES_PATH/shell/bash/.bashrc" + "$DOTFILES_PATH/shell/zsh/.zshrc" + "$DOTFILES_PATH/shell/paths.sh" +) + +for item in "${files_to_backup[@]}"; do + bk_path="" + + if [ -f "$item" ] && [ ! -L "$HOME/bin/script" ]; then + bk_path="$(files::backup_if_file_exists "$item")" + [[ -z "$bk_path" ]] && continue + output::write "File '$item' exits and was moved as backup to:" + output::write " $bk_path" + output::empty_line + fi +done + +# Always copy using interactive mode: cp -i +output::answer "Copying .bashrc and .zshrc files" +rm -f "$DOTFILES_PATH/shell/bash/.bashrc" +cp -i "$DOTFILES_TEMPLATE_PATH/shell/bash/.bashrc" "$DOTFILES_PATH/shell/bash/" +cp -i "$DOTFILES_TEMPLATE_PATH/shell/zsh/.zshrc" "$DOTFILES_PATH/shell/zsh/" +output::solution ".bashrc and .zshrc copied" +output::empty_line + +# Edit .bashrc file templating +templating::replace "$DOTFILES_PATH/shell/bash/.bashrc" --dotfiles-path="${DOTFILES_PATH//$HOME/\$HOME}" + +# Create new paths.sh file +output::answer "Creating paths.sh file with your current values" +if [ -n "${path[*]:-}" ]; then + { + printf "path=(" + printf " \"%s\"\n" "${path[@]}" | sort | uniq + printf ")\n" + printf "export path\n" + } >| "$DOTFILES_PATH/shell/paths.sh" +else + # Copy paths from dotfiles_template + cp -i "$DOTFILES_TEMPLATE_PATH/shell/paths.sh" "$DOTFILES_PATH/shell/" +fi +output::solution "paths.sh file created" +output::answer "Remember to delete your current paths if you have it in your exports.sh" +output::empty_line + +# Gitignore +if [ ! -f "$DOTFILES_PATH/.gitignore" ]; then + cp -i "$DOTFILES_TEMPLATE_PATH/.gitignore" "$DOTFILES_PATH/" +else + cat "$DOTFILES_TEMPLATE_PATH/.gitignore" >> "$DOTFILES_PATH/.gitignore" +fi diff --git a/modules/dotbot b/modules/dotbot index cf366bbf6..aa9335089 160000 --- a/modules/dotbot +++ b/modules/dotbot @@ -1 +1 @@ -Subproject commit cf366bbf6676d1c95f412eb514509f16322b5c9c +Subproject commit aa9335089b54475940bb41e2a4eed38affeb5916 diff --git a/modules/zimfw b/modules/zimfw index dfbe53543..7d533fcec 160000 --- a/modules/zimfw +++ b/modules/zimfw @@ -1 +1 @@ -Subproject commit dfbe535430271c5ee0bbd7cfac6df42222e8cdf0 +Subproject commit 7d533fcecd7fbf410ccf71e188dcc9af06c0d5d8 diff --git a/scripts/core/_main.sh b/scripts/core/_main.sh index 3434ad854..e0d4b7bd7 100755 --- a/scripts/core/_main.sh +++ b/scripts/core/_main.sh @@ -1,6 +1,7 @@ if ! ${DOT_MAIN_SOURCED:-false}; then - for file in $DOTLY_PATH/scripts/core/{args,collections,documentation,dot,git,log,platform,output,script,str}.sh; do - source "$file" + for file in $DOTLY_PATH/scripts/core/{args,array,async,collections,documentation,dot,files,git,json,log,platform,output,script,str,yaml}.sh; do + #shellcheck source=/dev/null + . "$file" || exit 5 done unset file diff --git a/scripts/core/array.sh b/scripts/core/array.sh new file mode 100644 index 000000000..ef2a0494f --- /dev/null +++ b/scripts/core/array.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Usage: array::* "${arr1[@]}" "${arr2[@]}" +array::union() { echo "${@}" | tr ' ' '\n' | sort | uniq; } +array::disjunction() { echo "${@}" | tr ' ' '\n' | sort | uniq -u; } +array::difference() { echo "${@}" | tr ' ' '\n' | sort | uniq -d; } +array::exists_value() { + local value array_value + value="${1:-}"; shift + + for array_value in "$@"; do + [[ "$array_value" == "$value" ]] && return 0 + done + + return 1 +} + diff --git a/scripts/core/async.sh b/scripts/core/async.sh new file mode 100644 index 000000000..18e71e3ff --- /dev/null +++ b/scripts/core/async.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# This was literally copied from: https://github.com/zombieleet/async-bash +# check the README.md for information on how to use this script + +# set +eu + +declare -a JOB_IDS +declare -i JOBS=1; + +killJob() { + local jobToKill signal __al__signals isSig + + jobToKill="$1" + signal="$2" + signal=${signal^^} + + [[ ! $jobToKill =~ ^[[:digit:]]+$ ]] && { + printf "%s\n" "\"$jobToKill\" should be an integer "; + return 1; + } + + + { + [[ -z "$signal" ]] && { + signal="SIGTERM" + } + } || { + # for loop worked better than read line in this case + __al__signals=$(kill -l); + isSig=0; + for sig in ${__al__signals}; do + [[ ! $sig =~ ^[[:digit:]]+\)$ ]] && { + [[ $signal == $sig ]] && { + isSig=1; + break; + } + } + done + + (( isSig != 1 )) && { + signal="SIGTERM" + } + } + + + + for job in ${JOB_IDS[@]};do + # increment job to 1 since array index starts from 0 + read -r -d " " -a __kunk__ <<< "${JOB_IDS[$job]}" + (( __kunk__ == jobToKill )) && { + read -r -d " " -a __kunk__ <<< "${JOB_IDS[$job]}" + + kill -${signal} %${__kunk__} + + status=$? + + (( status != 0 )) && { + printf "cannot kill %s %d\n" "${JOB_IDS[$job]}" "${__kunk__}" + return 1; + } + + printf "%d killed with %s\n" "${__kunk__}" "${signal}" + + return 0; + } + done +} + +async() { + local cmdToExec resolve reject _c __temp status + set +e # Avoid crash if any function fail + + cmdToExec="$1" + resolve="$2" + reject="$3" + + [[ -z "$cmdToExec" ]] || [[ -z "$reject" ]] || [[ -z "$resolve" ]] && { + printf "%s\n" "Insufficient number of arguments"; + return 1; + } + + + + __temp=( "$cmdToExec" "$reject" "$resolve" ) + + + for _c in "${__temp[@]}"; do + read -r -d " " comm <<<"${_c}" + type "${comm}" &>/dev/null + + status=$? + + (( status != 0 )) && { + printf "\"%s\" is neither a function nor a recognized cmd\n" "${_c}"; + unset _c + return 1; + } + done + + unset __temp _c + + { + __result=$($cmdToExec) + status=$? + + if (( status == 0 )) + then + $resolve "${__result}" + else + $reject "${status}" + fi + unset __result + } & + + JOB_IDS+=( "${JOBS} ${cmd}" ) + + read -r -d " " -a __kunk__ <<< "${JOB_IDS[$(( ${#JOB_IDS[@]} - 1))]}" + + #echo ${__kunk__} + + + : $(( JOBS++ )) + +} \ No newline at end of file diff --git a/scripts/core/dot.sh b/scripts/core/dot.sh index 0c1c48696..9266f1dd9 100644 --- a/scripts/core/dot.sh +++ b/scripts/core/dot.sh @@ -1,3 +1,7 @@ +#!/usr/bin/env bash + +[[ -z "${SCRIPT_LOADED_LIBS[*]:-}" ]] && SCRIPT_LOADED_LIBS=() + dot::list_contexts() { dotly_contexts=$(ls "$DOTLY_PATH/scripts") dotfiles_contexts=$(ls "$DOTFILES_PATH/scripts") @@ -30,3 +34,65 @@ dot::list_scripts_path() { printf "%s\n%s" "$dotly_contexts" "$dotfiles_contexts" | sort -u } + +dot::get_script_path() { + echo "$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +} + +dot::get_full_script_path() { + echo "$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/$(basename "$0")" +} + +# Old name: dot::get_script_src_path +# If you find any old name replace by +# new one: +dot::load_library() { + local lib lib_path lib_paths lib_full_path + lib="${1:-}" + lib_full_path="" + + if [[ -n "${lib:-}" ]]; then + lib_paths=() + if [[ -n "${2:-}" ]]; then + lib_paths+=("$DOTFILES_PATH/scripts/$2/src" "$DOTLY_PATH/scripts/$2/src" "$2") + else + lib_paths+=( + "$(dot::get_script_path)/src" + ) + fi + + lib_paths+=( + "$DOTLY_PATH/scripts/core" + "." + ) + + for lib_path in "${lib_paths[@]}"; do + [[ -f "$lib_path/$lib" ]] &&\ + lib_full_path="$lib_path/$lib" &&\ + break + + [[ -f "$lib_path/$lib.sh" ]] &&\ + lib_full_path="$lib_path/$lib.sh" &&\ + break + done + + # Library loading + if [[ -n "${lib_full_path:-}" ]] && [[ -r "${lib_full_path:-}" ]]; then + if ! array::exists_value "${lib_full_path:-}" "${SCRIPT_LOADED_LIBS[@]:-}"; then + #shellcheck disable=SC1090 + . "$lib_full_path" + SCRIPT_LOADED_LIBS+=( + "$lib_full_path" + ) + fi + + return 0 + else + output::error "🚨 Library loading error with: \"${lib_full_path:-No lib path found}\"" + exit 1 + fi + fi + + # No arguments + return 1 +} diff --git a/scripts/core/files.sh b/scripts/core/files.sh new file mode 100644 index 000000000..fdb36f8bc --- /dev/null +++ b/scripts/core/files.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +files::check_if_path_is_older() { + local path_to_check number_of period + path_to_check="$1" + number_of="${2:-0}" + period="${3:-days}" + [[ -e "$path_to_check" ]] && [[ $(date -r "$path_to_check" +%s) -lt $(date -d "now - $number_of $period" +%s) ]] +} + +files::backup_if_file_exists() { + local file_path bk_suffix bk_file_path + file_path="$(eval realpath -q -m "${1:-}")" + bk_suffix="${2:-$(date +%s)}" + bk_file_path="$file_path.${bk_suffix}" + + if [[ -n "$file_path" ]] &&\ + { [[ -f "$file_path" ]] || [[ -d "$file_path" ]]; } + then + eval mv "$file_path" "$bk_file_path" && echo "$bk_file_path" && return 1 + fi + + return 0 +} diff --git a/scripts/core/git.sh b/scripts/core/git.sh index f2f419b38..a7cba7c55 100755 --- a/scripts/core/git.sh +++ b/scripts/core/git.sh @@ -2,6 +2,180 @@ git::is_in_repo() { git rev-parse HEAD >/dev/null 2>&1 } +# shellcheck disable=SC2120 git::current_branch() { - git branch + git branch --show-current "$@" +} + +git::get_local_HEAD_hash() { + local branch + branch="${1:-HEAD}" + git::is_in_repo && git rev-parse "$branch" +} + +git::get_remote_branch_HEAD_hash() { + local remote branch + remote="${1:-origin}" + branch="${2:-master}" + git::is_in_repo && git ls-remote --heads "$remote" "$branch" +} + +git::local_current_branch_commit_exists_remote() { + local remote branch local_commit + remote="${1:-origin}" + branch="${2:-master}" + local_commit="$(git::get_local_HEAD_hash "$branch")" + + [ -n "$local_commit" ] &&\ + git::is_in_repo && + git ls-remote --symref "$remote" | tail -n +2 | grep -q "$local_commit" +} + +git::remote_branch_by_hash() { + [[ -n "${1:-}" ]] && git ls-remote --symref origin | tail -n +2 | grep "${1:-}" | awk '{print $2}' | grep "refs/heads" | sed 's#refs/heads/##' +} + +# shellcheck disable=SC2120 +git::current_commit_hash() { + git rev-parse HEAD "$@" +} + +# shellcheck disable=SC2120 +git::get_commit_tag() { + local commit + if git::is_in_repo; then + commit="${1:-}" + { [[ -n "$commit" ]] && shift; } || commit="$(git::current_commit_hash)" + + git show-ref --tags "$@" | grep "$commit" | awk '{print $2}' | sed 's#refs/tags/##' + fi +} + +git::get_current_latest_tag() { + git::get_all_local_tags | head -n1 +} + +# shellcheck disable=SC2120 +git::get_all_local_tags() { + #git tag -l --sort="-version:refname" "$@" + git show-ref --tags | sort --reverse | awk '{print $2}' | sed 's#refs/tags/##' +} + +git::get_all_remote_tags() { + local repository + repository="${1:-origin}" + git ls-remote --tags --sort "-version:refname" "$repository" "$@" +} + +git::get_all_remote_tags_version_only() { + local repository + repository="${1:-}" + { [[ -n "$repository" ]] && shift; } || repository="origin" + git::get_all_remote_tags "$repository" "${@:-*.*.*}" 2>/dev/null | sed 's/.*\///; s/\^{}//' | uniq +} + +git::check_local_tag_exists() { + local repository tag_version + repository="${1:-}" + tag_version="${2:-}" + + { [[ -z "$repository" ]] || [[ -z "$tag_version" ]]; } && return 1 + + git::get_all_local_tags | grep -q "$tag_version" +} + +git::check_remote_tag_exists() { + local repository tag_version + repository="${1:-}" + tag_version="${2:-}" + + { [[ -z "$repository" ]] || [[ -z "$tag_version" ]]; } && return 1 + + [[ -n "$(git::get_all_remote_tags_version_only "$repository" "$tag_version")" ]] +} + +git::get_submodule_property() { + local gitmodules_path submodule_directory property + + if [ $# -gt 2 ]; then + gitmodules_path="$1"; shift + submodule_directory="$1" + fi + + gitmodules_path="${gitmodules_path:-$DOTFILES_PATH/.gitmodules}" + submodule_directory="${submodule_directory:-modules/${1:-}}" + property="${2:-}" + + [[ -f "$gitmodules_path" ]] && [[ -n "$submodule_directory" ]] && [[ -n "$property" ]] && git config -f "$gitmodules_path" submodule."$submodule_directory"."$property" +} + +git::check_file_exists_in_previous_commit() { + [[ -n "${1:-}" ]] && ! git rev-parse @~:"${1:-}" > /dev/null 2>&1 +} + +git::get_file_last_commit_timestamp() { + [[ -n "${1:-}" ]] && git rev-list --all --date-order --timestamp -1 "${1:-}" 2>/dev/null | awk '{print $1}' +} + +git::get_commit_timestamp() { + [[ -n "${1:-}" ]] && git rev-list --all --date-order --timestamp | grep "${1:-}" | awk '{print $1}' +} + +git::check_file_is_modified_after_commit() { + local file_path file_commit_date commit_to_check commit_to_check_date + file_path="${1:-}" + commit_to_check="${2:-}" + { [[ -z "$file_path" ]] || [[ -z "${commit_to_check:-}" ]] || [[ ! -e "$file_path" ]]; } && return 1 + + file_commit_date="$(git::get_file_last_commit_timestamp "${file_path:-}" 2>/dev/null)" + + [[ -z "$file_commit_date" ]] && return 0 # File path did not exists previously then + # it is more recent than any commit 😅 + + commit_to_check_date="$(git::get_commit_timestamp "$commit_to_check")" + [[ "$file_commit_date" -gt "$commit_to_check_date" ]] +} + +# PR Note to reviewer: This function could be replaced by the next one, the +# function name that use update is this one +# git::check_local_repo_is_updated() { +# local repo_path remote return_code current_dir remote_head_hash remote_head_branch local_head_remote_branch_hash +# remote="${1:-origin}" +# repo_path="${2:-.}" + +# current_dir="$(pwd)" +# return_code=1 + +# cd "$repo_path" || return 1 + +# if git::is_in_repo; then +# remote_head_hash="$(git ls-remote --symref "$remote" | tail -n +2 | head -n 1 | awk '{print $1}')" # remote: HEAD +# remote_head_branch="$(git::remote_branch_by_hash "$remote_head_hash")" +# local_head_remote_branch_hash="$(git rev-parse "$remote_head_branch")" + +# git::local_current_branch_commit_exists_remote "$remote" "$local_head_remote_branch_hash" && [ "$remote_head_hash" == "$local_head_remote_branch_hash" ] +# return_code=$? +# fi + +# cd "$current_dir" || return $return_code + +# return $return_code +# } + +git::check_local_repo_is_updated() { + git::is_in_repo && ! git status -sb 2>/dev/null | grep -q 'behind' +} + +git::dotly_repository_exec() { + local return_code + return_code=0 + cd "$DOTLY_PATH" || return 1 + + if git::is_in_repo; then + eval "$@" + else + return_code=1 + fi + + return "$return_code" } diff --git a/scripts/core/json.sh b/scripts/core/json.sh new file mode 100644 index 000000000..0e3a13381 --- /dev/null +++ b/scripts/core/json.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +json::to_yaml() { + if [[ -t 0 ]]; then + [[ -f "${1:-}" ]] && yq --yaml-output <"$1" + else + yq --yaml-output /dev/null + else + jq -e '.' /dev/null + fi +} diff --git a/scripts/core/output.sh b/scripts/core/output.sh index a19d1b1a8..3ad5fea58 100644 --- a/scripts/core/output.sh +++ b/scripts/core/output.sh @@ -12,25 +12,54 @@ _output::parse_code() { } output::write() { - local -r text="${1:-}" - + local with_code_parsed + local -r text="${*:-}" with_code_parsed=$(_output::parse_code "$text") - echo -e "$with_code_parsed" } -output::answer() { output::write " > $1"; } -output::error() { output::answer "${red}$1${normal}"; } -output::solution() { output::answer "${green}$1${normal}"; } +output::answer() { output::write " > ${*:-}"; } +output::error() { output::answer "${red}${*:-}${normal}"; } +output::solution() { output::answer "${green}${*:-}${normal}"; } output::question() { - with_code_parsed=$(_output::parse_code "$1") + [[ $# -ne 2 ]] && return 1 if [ "${DOTLY_ENV:-PROD}" == "CI" ] || [ "${DOTLY_INSTALLER:-false}" = true ]; then - answer="y" + echo "y" + elif declare -F platform::is_macos &>/dev/null && platform::is_macos; then + echo -n " > 🤔 $1: "; + read -r "$2"; + else + read -rp "🤔 $1: " "$2" + fi +} +output::question_default() { + local question default_value var_name + + [[ $# -ne 3 ]] && return 1 + + question="${1:-}" + default_value="${2:-}" + var_name="${3:-}" + + output::question "$question? [$default_value]" "$var_name" + eval "$var_name=\"\${$var_name:-$default_value}\"" +} +output::yesno() { + local question default PROMPT_REPLY values + + [[ $# -eq 0 ]] && return 1 + + question="$1" + default="${2:-Y}" + + if [[ "$default" =~ ^[Yy] ]]; then + values="Y/n" else - read -rp "🤔 $with_code_parsed: " "answer" + values="y/N" fi - echo "$answer" + output::question "$question? [$values]" "PROMPT_REPLY" + [[ "${PROMPT_REPLY:-$default}" =~ ^[Yy] ]] } output::answer_is_yes() { @@ -45,18 +74,18 @@ output::empty_line() { echo ''; } output::header() { output::empty_line - output::write "${bold_blue}---- $1 ----${normal}" + output::write "${bold_blue}---- ${*:-} ----${normal}" } -output::h1_without_margin() { output::write "${bold_blue}# $1${normal}"; } +output::h1_without_margin() { output::write "${bold_blue}# ${*:-}${normal}"; } output::h1() { output::empty_line - output::h1_without_margin "$1" + output::h1_without_margin "${*:-}" } output::h2() { output::empty_line - output::write "${bold_blue}## $1${normal}" + output::write "${bold_blue}## ${*:-}${normal}" } output::h3() { output::empty_line - output::write "${bold_blue}### $1${normal}" + output::write "${bold_blue}### ${*:-}${normal}" } diff --git a/scripts/core/platform.sh b/scripts/core/platform.sh index 019244693..352d9acc8 100644 --- a/scripts/core/platform.sh +++ b/scripts/core/platform.sh @@ -21,3 +21,74 @@ platform::is_wsl() { platform::wsl_home_path() { wslpath "$(wslvar USERPROFILE 2>/dev/null)" } + +platform::normalize_ver() { + local version + version="${1//./ }" + echo "${version//v/}" +} + +platform::compare_ver() { + [[ $1 -lt $2 ]] && echo -1 && return + [[ $1 -gt $2 ]] && echo 1 && return + + echo 0 +} + +# It does not support beta, rc and similar suffix +platform::semver_compare() { + if [ -z "${1:-}" ] || [ -z "${2:-}" ]; then + return 1 + fi + + v1="$(platform::normalize_ver "${1:-}")" + v2="$(platform::normalize_ver "${2:-}")" + + major1="$(echo "$v1" | awk '{print $1}')" + major2="$(echo "$v2" | awk '{print $1}')" + + minor1="$(echo "$v1" | awk '{print $2}')" + minor2="$(echo "$v2" | awk '{print $2}')" + + patch1="$(echo "$v1" | awk '{print $3}')" + patch2="$(echo "$v2" | awk '{print $3}')" + + compare_major="$(platform::compare_ver "$major1" "$major2")" + compare_minor="$(platform::compare_ver "$minor1" "$minor2")" + compare_patch="$(platform::compare_ver "$patch1" "$patch2")" + + if [[ $compare_major -ne 0 ]]; then + echo "$compare_major" + elif [[ $compare_minor -ne 0 ]]; then + echo "$compare_minor" + else + echo "$compare_patch" + fi +} + +# It does not support beta, rc and similar suffix +# First argument is the current version to say if second argument is +# a version update that is no a major update +platform::semver_is_minor_or_patch_update() { + v1="$(platform::normalize_ver "$1")" + v2="$(platform::normalize_ver "$2")" + + major1="$(echo "$v1" | awk '{print $1}')" + major2="$(echo "$v2" | awk '{print $1}')" + + minor1="$(echo "$v1" | awk '{print $2}')" + minor2="$(echo "$v2" | awk '{print $2}')" + + patch1="$(echo "$v1" | awk '{print $3}')" + patch2="$(echo "$v2" | awk '{print $3}')" + + compare_major="$(platform::compare_ver "$major1" "$major2")" + compare_minor="$(platform::compare_ver "$minor1" "$minor2")" + compare_patch="$(platform::compare_ver "$patch1" "$patch2")" + + [[ $compare_major -eq 0 ]] && { # Only equals major are minor or patch updates + [[ $compare_minor -eq -1 ]] || { # If minor is over current minor is and update + [[ $compare_minor -eq 0 ]] && [[ $compare_patch -eq -1 ]] # If minor is equal and patch is greater + } + } +} diff --git a/scripts/core/str.sh b/scripts/core/str.sh index 31b52aed9..cad602725 100755 --- a/scripts/core/str.sh +++ b/scripts/core/str.sh @@ -8,3 +8,9 @@ str::split() { str::contains() { [[ $2 == *$1* ]] } + +str::to_upper() { echo "${@:-$(|] [...] +# echo "template string" | templating::replace_var [...] +# +# echo "Those are my family names: XXX_FAMILY_NAMES_XXX" |\ +# templating::replace_var family-names Miguel Manuel +# +# This will print: +# "Those are my family names: Miguel Manuel" +templating::replace_var () { + local file_path string var_name value + + [[ $# -lt 2 ]] && return 1 + + # Replacer + if [[ -t 0 ]] && [[ -f "$1" ]]; then + file_path="$1"; shift + var_name="XXX_$(str::to_upper "$1" | tr '-' '_')_XXX"; shift + value="${*:-}" + sed -i -e "s|${var_name}|${value}|g" "$file_path" + elif [[ -t 0 ]]; then + string="$1"; shift + var_name="XXX_$(str::to_upper "$1" | tr '-' '_')_XXX"; shift + value="${*:-}" + echo "${string//$var_name/$value}" + else + var_name="XXX_$(str::to_upper "$1" | tr '-' '_')_XXX"; shift + value="${*:-}" + sed -e "s|${var_name}|${value}|g" [...] +# +# echo "Those are common names in spain: XXX_NAMES_XXX" |\ +# templating::replace_var_join names ', ' Manuel Jorge David Luis Pedro +# +# This will print: +# "Those are common names in spain: Manuel, Jorge, David, Luis, Pedro" +# +templating::replace_var_join() { + local string var_name glue joined_str + + { [[ -t 0 && $# -lt 3 ]] || [[ $# -lt 2 ]]; } && return 1 + + if [[ -t 0 ]]; then + string="$1"; + var_name="$1"; shift + glue="$1"; shift + joined_str="$(str::join "$glue" "$@")" + templating::replace_var "$string" "$var_name" "$joined_str" + else + var_name="$1"; shift + glue="$1"; shift + joined_str="$(str::join "$glue" "$@")" + templating::replace_var "$var_name" "$joined_str" " --name=Gabriel --email-address=no-email@example.com +# templating::replace "XXX_NAME_XXX " --name Gabriel --email-address no-email@example.com +# templating::replace "XXX_NAME_XXX " name Gabriel email-address no-email@example.com +# echo "XXX_NAME_XXX " |\ +# templating::replace name Gabriel email-address no-email@example.com +# templating::replace /path/to/file --name=Gabriel --email-address=no-email@example.com +# templating::replace /path/to/file --name Gabriel --email-address no-email@example.com +# templating::replace /path/to/file name Gabriel email-address no-email@example.com +# +# This will print +# "Gabriel " +# +templating::replace() { + local var_name var_value output + case "${1:-}" in + --*=*|--*) + output=$( +docs::parse "$@" + +SCRIPT_NAME="dot core wrong" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +"$DOTLY_PATH/bin/open" "https://www.youtube.com/watch?v=t3otBjVZzT0" diff --git a/scripts/core/yaml.sh b/scripts/core/yaml.sh new file mode 100644 index 000000000..8e30d384a --- /dev/null +++ b/scripts/core/yaml.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +yaml::to_json() { + if [[ -t 0 ]]; then + [[ -f "${1:-}" ]] && yq -r '.' - <"$1" + else + yq -r '.' /dev/null && ! json::is_json "$1" + else + input="$(/dev/null && ! echo "$input" | json::is_json + fi +} diff --git a/scripts/dotfiles/create b/scripts/dotfiles/create index f7033018d..6ffcb3332 100755 --- a/scripts/dotfiles/create +++ b/scripts/dotfiles/create @@ -2,7 +2,8 @@ set -euo pipefail -source "$DOTLY_PATH/scripts/core/_main.sh" +. "$DOTLY_PATH/scripts/core/_main.sh" +. "$DOTLY_PATH/scripts/core/templating.sh" ##? Create the dotfiles structure ##? @@ -12,9 +13,15 @@ source "$DOTLY_PATH/scripts/core/_main.sh" docs::parse "$@" dotfiles::apply_templating() { - sed -i -e "s|XXX_DOTFILES_PATH_XXX|$DOTFILES_PATH|g" "$DOTFILES_PATH/bin/sdot" - sed -i -e "s|XXX_DOTFILES_PATH_XXX|$DOTFILES_PATH|g" "$DOTFILES_PATH/shell/bash/.bashrc" - sed -i -e "s|XXX_DOTFILES_PATH_XXX|$DOTFILES_PATH|g" "$DOTFILES_PATH/shell/zsh/.zshenv" + local tpl_files=( + "$DOTFILES_PATH/bin/sdot" + "$DOTFILES_PATH/shell/bash/.bashrc" + "$DOTFILES_PATH/shell/zsh/.zshenv" + ) + + for file in "${tpl_files[@]}"; do + templating::replace "$file" --dotfiles-path="${DOTFILES_PATH//$HOME/\$HOME}" + done } if [ ! -d "$DOTFILES_PATH/shell" ]; then diff --git a/scripts/init/disable b/scripts/init/disable new file mode 100755 index 000000000..a8ddf2362 --- /dev/null +++ b/scripts/init/disable @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +set -euo pipefail + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +#shellcheck source=/dev/null +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "init.sh" + +##? Disable init scripts +##? +##? +##? Usage: +##? disable [-h | --help] +##? disable [-v | --version] +##? disable [] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? +##? Author: +##? Gabriel Trabanco Llano +docs::parse "$@" + +SCRIPT_NAME="dot init disable" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +if [[ ${DOTLY_INIT_SCRIPTS:-true} != true ]]; then + output::error "Init scripts are disabled" + exit 1 +fi + +# Get the scripts +#shellcheck disable=SC2207 +enabled_scripts=($(init::get_enabled)) + +# If there is script_name +if [[ -n "${script_name:-}" ]]; then + status=0 + if init::exists_script "$script_name"; then + init::disable "$script_name" + if ! init::status "$script_name"; then + output::solution "Disabled '$script_name'" + fi + + init::status "$script_name" && output::error "Could not be disabled." && status=1 + else + output::error "$script_name does not exists." + status=1 + fi + + exit $status +fi + +# If there is no script_name +# If there are no enabled scripts or nothing to be disabled, exit +if [[ -n "${enabled_scripts[*]:-}" ]]; then + #shellcheck disable=SC2207 + to_disable=($(printf "%s\n" "${enabled_scripts[@]}" | init::fzf "Choose one or more (Shift + Tab) scripts to disable from init terminal")) +else + output::answer "Nothing to be disabled" +fi + +[[ -z "${to_disable[*]:-}" ]] && exit 0 + +for item in "${to_disable[@]}"; do + + init::disable "$item" + + if ! init::status "$item"; then + output::solution "Init script '$item'... Disabled" + else + output::error "Init script '$item'... Could not be disabled." + fi + +done diff --git a/scripts/init/enable b/scripts/init/enable new file mode 100755 index 000000000..4e064612b --- /dev/null +++ b/scripts/init/enable @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +set -euo pipefail + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +#shellcheck source=/dev/null +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "init.sh" + +##? Enable init scripts +##? +##? +##? Usage: +##? enable [-h | --help] +##? enable [-v | --version] +##? enable [] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? +##? Author: +##? Gabriel Trabanco Llano +docs::parse "$@" + +SCRIPT_NAME="dot init enable" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +if [[ ${DOTLY_INIT_SCRIPTS:-true} != true ]]; then + output::error "Init scripts are disabled" + exit 1 +fi + +# Get the scripts +init_scripts=("$(init::get_scripts)") +enabled_scripts=("$(init::get_enabled)") + +# If the user gives the script_name +if [[ -n "${script_name:-}" ]]; then + status=0 + if init::exists_script "$script_name"; then + init::enable "$script_name" + init::status "$script_name" && output::solution "Enabled" + ! init::status "$script_name" && output::error "Could not be enabled." && status=1 + else + output::error "$script_name does not exists." + status=1 + fi + exit $status +fi + +# If there is no script_name +# If there is nothing that can be enabled or not select scripts to +# be enabled, exit +not_enabled_scripts=("$(array::disjunction "${init_scripts[@]}" "${enabled_scripts[@]}")") +if [[ -n "${not_enabled_scripts[*]:-}" ]]; then + #shellcheck disable=SC2207 + to_enable=($(array::disjunction "${init_scripts[@]}" "${enabled_scripts[@]}" | init::fzf "Choose one or more (Shift + Tab) scripts to enable when init terminal")) +else + output::answer "Nothing can be enabled" +fi +[[ -z "$to_enable" ]] && exit 0 + +for item in "${to_enable[@]}"; do + init::enable "$item" + + if init::status "$item"; then + output::solution "Init script '$item'... Enabled" + else + output::error "Init script '$item' error... It could not be enabled." + exit 1 + fi +done diff --git a/scripts/init/src/init.sh b/scripts/init/src/init.sh new file mode 100644 index 000000000..54140c16a --- /dev/null +++ b/scripts/init/src/init.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +# PR annotation +# If you change this folders you should also change them in init-dotly.sh +DOTLY_INIT_SCRIPTS_PATH="$DOTLY_PATH/shell/init-scripts" +DOTFILES_INIT_SCRIPTS_PATH="$DOTFILES_PATH/shell/init-scripts" +ENABLED_INIT_SCRIPTS_PATH="$DOTFILES_PATH/shell/init-scripts.enabled" + +[[ ! -d "$ENABLED_INIT_SCRIPTS_PATH" ]] &&\ + output::error "The folder path to enable scripts does not exists." &&\ + output::write "If you want to disble init script add in your exports \`export DOTLY_INIT_SCRIPTS=false\` " &&\ + output::write "If you want to enable. Execute \`dot self migration v2.0.0\` first." &&\ + exit 1 + +[[ ! -d "$DOTLY_INIT_SCRIPTS_PATH" ]] &&\ + output::error "The init scripts of DOTLY does not exists." &&\ + output::write "Try with \`dot self migration v2.0.0\` first." &&\ + exit 1 + +init::exists_script() { + [[ -e "$DOTLY_INIT_SCRIPTS_PATH/$1" ]] || [[ -e "$DOTFILES_INIT_SCRIPTS_PATH" ]] +} + +init::status() { + init::exists_script "$1" && [[ -f "$ENABLED_INIT_SCRIPTS_PATH/$1" ]] +} + +init::get_scripts() { + [[ -d "$DOTLY_INIT_SCRIPTS_PATH" ]] &&\ + [[ -d "$DOTFILES_INIT_SCRIPTS_PATH" ]] &&\ + find "$DOTLY_INIT_SCRIPTS_PATH" \ + "$DOTFILES_INIT_SCRIPTS_PATH" -name "*" -type f,l -print0 -exec echo {} \; |\ + xargs -0 -I _ basename _ | sort | uniq +} + +init::get_enabled() { + [[ -d "$ENABLED_INIT_SCRIPTS_PATH" ]] &&\ + find "$ENABLED_INIT_SCRIPTS_PATH" -name "*" -type l -print0 -exec echo {} \; |\ + xargs -0 -I _ basename _ | sort | uniq +} + +init::fzf() { + local piped_values preview_cmd + piped_values="$(] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? +##? Author: +##? Gabriel Trabanco Llano +docs::parse "$@" + +SCRIPT_NAME="dot init status" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +if [[ ${DOTLY_INIT_SCRIPTS:-true} != true ]]; then + output::error "Init scripts are disabled" + exit 1 +fi + +# Get the scripts +#shellcheck disable=SC2207 +init_scripts=($(init::get_scripts)) + +# Check status, if user gives a script_name +if [ -n "${script_name:-}" ]; then + + if init::status "$script_name"; then + output::solution "'$script_name' is enabled" + else + output::error "'$script_name' is disabled" + fi + +else + + # If there is no script_name, gives the status of all + for item in "${init_scripts[@]}"; do + { + + init::status "$item" &&\ + output::solution "'$item'... Enabled." + + } || output::error "'$item'... Disabled." + done + +fi diff --git a/scripts/package/add b/scripts/package/add index b53b9ebe2..fdc9ce123 100755 --- a/scripts/package/add +++ b/scripts/package/add @@ -1,67 +1,83 @@ #!/usr/bin/env bash -source "$DOTLY_PATH/scripts/core/_main.sh" -source "$DOTLY_PATH/scripts/package/recipes/_registry.sh" +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "package.sh" +dot::load_library "_registry.sh" "$(dot::get_script_path)/recipes/" ##? Install a package ##? ##? Usage: -##? add [--skip-recipe] +##? add [-v | --version] +##? add [-h | --help] +##? add [--recipe-path ] [--skip-recipe] +##? +##? Options: +##? -h --help Show script help +##? -v --version Show script version ##? if ! ${DOTLY_INSTALLER:-false}; then docs::parse "$@" +else + package_name="" + skip_recipe=false + recipe_path="$(dot::get_script_path)/recipes/" + while $# -gt 0; do + case "$1" in + --recipe-path) + if [[ "$2" != "_registry" ]]; then + recipe_path="$2" + fi + shift 2 + ;; + --skip-recipe) + skip_recipe=true; shift + ;; + --version|-v) + version=true; shift + ;; + --help|-h) + help=true; shift + ;; + *) + package_name="$1"; shift + ;; + esac + done fi -export PATH="$HOME/.cargo/bin:$PATH" - -package::install() { - local -r package_manager="$1" - local -r package_to_install="$2" - - platform::command_exists "$package_manager" || exit 3 - - local -r file="$DOTLY_PATH/scripts/package/package_managers/$package_manager.sh" - - # unsupported package manager - [ ! -f "$file" ] && exit 4 +SCRIPT_NAME="dot package add" +SCRIPT_VERSION="3.0.0" - source "$file" - - local -r install_command="${package_manager}::install" +if ${version:-}; then + output::write "${SCRIPT_NAME} v${SCRIPT_VERSION}" + exit 0 +elif ${help:-}; then + grep '^##?' "$0" | tr '##?' ' ' + exit 0 +fi - "$install_command" "$package_to_install" -} +# No package +[[ -z "${package_name:-}" ]] && exit 1 -package_name="$1" -skip_recipe="$2" +export PATH="$HOME/.cargo/bin:$PATH" -platform::command_exists "$package_name" || registry::is_installed "$package_name" && log::success "$package_name already installed" && exit 0 +package::is_installed "$package_name" && log::success "$package_name already installed" && exit 0 -if [ -z "$skip_recipe" ] && registry::install "$package_name"; then +if [[ -z "$skip_recipe" ]] &&\ + [[ -n "$(registry::recipe_exists "$package_name" "${recipe_path:-}")" ]] &&\ + registry::install "$package_name" "${recipe_path:-}" &&\ + registry::is_installed "$package_name" "${recipe_path:-}" +then output::write "✅ \`$package_name\` installed" exit 0 else - if platform::is_macos; then - for package_manager in brew mas ports cargo; do - package::install $package_manager "$package_name" 2>&1 | log::file "Trying to install $package_name using $package_manager" || true - - if platform::command_exists "$package_name"; then - output::write "✅ \`$package_name\` installed" - - exit 0 - fi - done - else - for package_manager in apt dnf yum brew pacman cargo; do - package::install $package_manager "$package_name" 2>&1 | log::file "Trying to install $package_name using $package_manager" + package::command install "$package_name" 2>&1 | log::file "Trying to install $package_name using $package_manager" || true - if platform::command_exists "$package_name"; then - output::write "✅ \`$package_name\` installed" + if package::is_installed "$package_name"; then + output::write "✅ \`$package_name\` installed" - exit 0 - fi - done + exit 0 fi fi diff --git a/scripts/package/package_managers/apt.sh b/scripts/package/package_managers/apt.sh index 7c5af7475..13ddf9666 100644 --- a/scripts/package/package_managers/apt.sh +++ b/scripts/package/package_managers/apt.sh @@ -1,3 +1,7 @@ apt::install() { sudo apt-get -y install "$@" } + +apt::is_installed() { + apt list -a "$@" | grep -q 'installed' +} diff --git a/scripts/package/package_managers/brew.sh b/scripts/package/package_managers/brew.sh index 0fce0c66a..66e4b8b4a 100644 --- a/scripts/package/package_managers/brew.sh +++ b/scripts/package/package_managers/brew.sh @@ -7,3 +7,7 @@ brew::install() { brew install "$package" } + +brew::is_installed() { + brew list "$@" &>/dev/null +} diff --git a/scripts/package/package_managers/cargo.sh b/scripts/package/package_managers/cargo.sh index a66f25e24..272874032 100644 --- a/scripts/package/package_managers/cargo.sh +++ b/scripts/package/package_managers/cargo.sh @@ -1,3 +1,18 @@ cargo::install() { cargo install "$@" } + +cargo::is_installed() { + local package + if [[ $# -gt 1 ]]; then + for package in "$@"; do + if ! cargo install --list | grep -q "$package"; then + return 1 + fi + done + + return 0 + else + [[ -n "${1:-}" ]] && cargo install --list | grep -q "${1:-}" + fi +} diff --git a/scripts/package/package_managers/dnf.sh b/scripts/package/package_managers/dnf.sh index 8e6e59e3e..19b106474 100644 --- a/scripts/package/package_managers/dnf.sh +++ b/scripts/package/package_managers/dnf.sh @@ -1,3 +1,18 @@ dnf::install() { sudo dnf -y install "$@" } + +dnf::is_installed() { + local package + if [[ $# -gt 1 ]]; then + for package in "$@"; do + if ! rpm -qa | grep -qw "$package"; then + return 1 + fi + done + + return 0 + else + [[ -n "${1:-}" ]] && rpm -qa | grep -qw "${1:-}" + fi +} diff --git a/scripts/package/package_managers/pacman.sh b/scripts/package/package_managers/pacman.sh index 8074b2dfc..6ccab0f89 100644 --- a/scripts/package/package_managers/pacman.sh +++ b/scripts/package/package_managers/pacman.sh @@ -5,3 +5,7 @@ pacman::install() { sudo pacman -S --noconfirm "$@" fi } + +pacman::is_installed() { + pacman -Qs "$@" | grep -q 'local' +} diff --git a/scripts/package/package_managers/yum.sh b/scripts/package/package_managers/yum.sh index 082ef454a..6d91ff5a0 100644 --- a/scripts/package/package_managers/yum.sh +++ b/scripts/package/package_managers/yum.sh @@ -1,3 +1,18 @@ yum::install() { yes | sudo yum install "$@" } + +yum::is_installed() { + local package + if [[ $# -gt 1 ]]; then + for package in "$@"; do + if ! sudo yum list --installed | grep -q "$package"; then + return 1 + fi + done + + return 0 + else + [[ -n "${1:-}" ]] && sudo yum list --installed | grep -q "$2" + fi +} diff --git a/scripts/package/recipes/_registry.sh b/scripts/package/recipes/_registry.sh index b747b016a..8ed30be73 100644 --- a/scripts/package/recipes/_registry.sh +++ b/scripts/package/recipes/_registry.sh @@ -1,28 +1,52 @@ -if ! ${DOT_REGISTRY_SOURCED:-false}; then - for file in $DOTLY_PATH/scripts/package/recipes/{docpars,cargo,git-delta}.sh; do - source "$file" +registry::recipe_exists() { + local -r recipe="${1:-}" + local recipe_path recipe_paths=() + + if [[ -n "${2:-}" ]]; then + recipe_paths+=("$2") + fi + + recipe_paths+=( + "$DOTLY_PATH/scripts/package/recipes/${recipe}.sh" + "$DOTFILES_PATH/recipes/${recipe}.sh" + ) + [[ -z "$recipe" ]] && return + + for recipe_path in "${recipe_paths[@]}"; do + [[ -f "$recipe_path" ]] && break done - unset file - readonly DOT_REGISTRY_SOURCED=true -fi + echo "$recipe_path" +} registry::install() { - local -r installation_command="$1::install" + local -r recipe="${1:-}" + local -r install_command="${recipe}::install" + local -r recipe_path="$(registry::recipe_exists "$recipe" "${2:-}" || echo -n "")" + + [[ -z "$recipe" || -z "$recipe_path" || ! -f "$recipe_path" ]] && return 1 - if [ "$(command -v "$installation_command")" ]; then - "$installation_command" - else - return 1 + dot::load_library "${recipe}.sh" "$(dirname "$recipe_path")" + + if [[ "$(command -v "$install_command")" ]]; then + "$install_command" + return $? fi + + return 1 } registry::is_installed() { - local -r is_installed_command="$1::is_installed" + local -r recipe="${1:-}" + local -r is_installed_command="${recipe}::is_installed" + local -r recipe_path="$(registry::recipe_exists "$recipe" "${2:-}")" + [[ -z "$recipe" || -z "$recipe_path" || ! -f "$recipe_path" ]] && return 1 + dot::load_library "${recipe}.sh" "$(dirname "$recipe_path")" - if [ "$(command -v "$is_installed_command")" ]; then + if [[ "$(command -v "$is_installed_command")" ]]; then "$is_installed_command" - else - return 1 + return $? fi + + return 1 } diff --git a/scripts/package/recipes/python-yq.sh b/scripts/package/recipes/python-yq.sh new file mode 100644 index 000000000..f6de359bd --- /dev/null +++ b/scripts/package/recipes/python-yq.sh @@ -0,0 +1,25 @@ +dot::get_script_src_path "yq.sh" "package" + +yq::install() { + local binary_name yq_path + + binary_name="yq_$(installer::get_os)_$(installer::get_arch)" + yq_path="$HOME/bin/yq" + output::error "yq not installed, installing" + + "$DOTLY_PATH/bin/dot" package add python-yq --skip-recipe | log::file "Installing yq" + if package::is_installed python-yq; then + return 0 + fi + + if platform::command_exists pip3 &&\ + pip3 install yq | log::file "Installing yq from pip3" &&\ + platform::command_exists yq + then + output::solution "yq installed!" + return 0 + fi + + output::error "yq could not be installed" + return 1 +} diff --git a/scripts/package/src/package.sh b/scripts/package/src/package.sh new file mode 100644 index 000000000..58e492003 --- /dev/null +++ b/scripts/package/src/package.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +package::command() { + local package_manager + local -r command="$1" + local -r args=("${@:2}") + + # Package manager + if platform::is_macos; then + for package_manager in brew mas ports cargo none; do + if platform::command_exists "$package_manager"; then + break + fi + done + else + for package_manager in apt dnf yum brew pacman cargo none; do + if platform::command_exists "$package_manager"; then + break + fi + done + fi + + if [[ "$package_manager" == "none" ]]; then + return 1 + fi + + local -r file="$DOTLY_PATH/scripts/package/package_managers/$package_manager.sh" + + [[ ! -f "$file" ]] && exit 4 + . "$file" + declare -F "$package_manager::$command" &>/dev/null && "$package_manager::$command" "${args[@]}" +} + +package::is_installed() { + [[ -z "${1:-}" ]] && return 1 + + platform::command_exists "$1" ||\ + package::command is_installed "$1" ||\ + registry::is_installed "$1" +} diff --git a/scripts/package/src/yq.sh b/scripts/package/src/yq.sh new file mode 100644 index 000000000..359024a02 --- /dev/null +++ b/scripts/package/src/yq.sh @@ -0,0 +1,49 @@ +#!/bin/user/env bash + +installer::get_arch() { + local architecture="" + case $(uname -m) in + i386|i686) + architecture="386" + ;; + x86_64) + architecture="amd64" + ;; + arm) + architecture="arm" + ;; + esac + + echo "$architecture" +} + +installer::get_os() { + uname | tr '[:upper:]' '[:lower:]' +} + +installer::update_in_home() { + local binary_name yq_path + binary_name="yq_$(installer::get_os)_$(installer::get_arch)" + yq_path="$HOME/bin/yq" + output::error "yq not installed, installing" + + if platform::command_exists curl; then + { + curl --silent "https://github.com/mikefarah/yq/releases/latest/download/$binary_name" >| "$yq_path" 2>/dev/null &&\ + chmod +x "$yq_path" + } || { + output::error "Could not install yq!" + exit 5 + } + fi + + if platform::command_exists wget; then + { + wget "https://github.com/mikefarah/yq/releases/latest/download/$binary_name" -O "$yq_path" >/dev/null 2>&1 &&\ + chmod +x "$yq_path" + } || { + output::error "Could not install yq!" + exit 5 + } + fi +} diff --git a/scripts/package/update_all b/scripts/package/update_all index e61750e78..b77633577 100755 --- a/scripts/package/update_all +++ b/scripts/package/update_all @@ -2,13 +2,14 @@ set -uo pipefail -source "$DOTLY_PATH/scripts/core/_main.sh" -source "$DOTLY_PATH/scripts/package/src/package_managers/brew.sh" -source "$DOTLY_PATH/scripts/package/src/package_managers/composer.sh" -source "$DOTLY_PATH/scripts/package/src/package_managers/gem.sh" -source "$DOTLY_PATH/scripts/package/src/package_managers/mas.sh" -source "$DOTLY_PATH/scripts/package/src/package_managers/npm.sh" -source "$DOTLY_PATH/scripts/package/src/package_managers/pip.sh" +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::get_script_src_path "package_managers/brew.sh" +dot::get_script_src_path "package_managers/composer.sh" +dot::get_script_src_path "package_managers/gem.sh" +dot::get_script_src_path "package_managers/mas.sh" +dot::get_script_src_path "package_managers/npm.sh" +dot::get_script_src_path "package_managers/pip.sh" +dot::get_script_src_path "yq.sh" ##? Update all packages ##? @@ -31,6 +32,8 @@ platform::command_exists rustup && output::h2 '☢️ Rust compiler' && rustup u platform::command_exists deno && output::h2 '🦕 deno' && deno upgrade platform::command_exists tldr && output::h2 '📜 tldr database' && tldr --update +[[ -f "$HOME/bin/yq" ]] && files::check_if_path_is_older "$HOME/bin/yq" "30" "days" && output::h2 "🛠 Update yq tool" && installer::update_in_home + output::h2 "Zim Framework" && zsh "$DOTLY_PATH/modules/zimfw/zimfw.zsh" upgrade 2>&1 | log::file "Updating Zim" output::empty_line diff --git a/scripts/self/async-update b/scripts/self/async-update new file mode 100755 index 000000000..4387a2751 --- /dev/null +++ b/scripts/self/async-update @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "dotly_autoupdate.sh" + +##? Async autoupdate dotly to avoid load all core functions in default bash +##? +##? Usage: +##? autoupdate +docs::parse "$@" + +set +eu # Needed to use async +async autoupdate::dotly_updater autoupdate::dotly_success autoupdate::dotly_reject \ No newline at end of file diff --git a/scripts/self/install b/scripts/self/install index cb3e8eec4..4888e79e9 100755 --- a/scripts/self/install +++ b/scripts/self/install @@ -1,30 +1,157 @@ #!/usr/bin/env bash -source "$DOTLY_PATH/scripts/core/_main.sh" -source "$DOTLY_PATH/scripts/self/utils/install.sh" +set -euo pipefail + +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::get_script_src_path "install.sh" +dot::get_script_src_path "dotbot_yaml.sh" "symlinks" +dot::get_script_src_path "symlinks.sh" "symlinks" export ZIM_HOME="$DOTLY_PATH/modules/zimfw" export PATH="$HOME/.cargo/bin:$PATH" +backup_and_warn_if_exists() { + local file_path suffix bk_file_path + file_path="${1:-}" + suffix="${2:-}" + + [[ -n "$file_path" ]] && bk_file_path="$(files::backup_if_file_exists "$file_path" "$suffix")" + [[ -n "$bk_file_path" ]] && output::error "'$file_path' exists and was moved to '$bk_file_path'" +} + +yaml_file_backup() { + local yaml_file links + yaml_file="${1:-}" + + for link in $(dotbot::get_all_keys_in "link" "$(symlinks::get_file_path "$yaml_file")"); do + link="$(eval realpath -q -m -s "$link")" + + if [[ -f "$link" ]]; then + if ! ${quiet_symlinks:-} && + output::yesno "File '$link' exists. Do you want to backup it" + then + backup_and_warn_if_exists "$link" "$rename_suffix" + fi + fi + done +} + +dotly_install_symlinks() { + local -r CONFIG="$DOTLY_PATH/scripts/self/src/symlinks/${1:-}" + shift + + [[ ! -f "$CONFIG" ]] && return + + echo + "$DOTLY_PATH/modules/dotbot/bin/dotbot" "$@" "$CONFIG" || true + echo +} + if platform::is_macos; then output::answer "🍎 Setting up macOS platform" install_macos_custom fi -script::depends_on docpars fzf zsh python +script::depends_on docpars fzf zsh python jq +# PR Review note +# script::depends_on will fail for `moreutils` package +# because moreutils is not a binary +"$DOTLY_PATH/bin/dot" package add moreutils +# The same for python-yq +"$DOTLY_PATH/bin/dot" package add python-yq ##? Install dotly and setup dotfiles ##? ##? Usage: -##? install +##? install [[-n | --never-backup] | [-b |--always-backup]] [-s | --quiet-symlinks] +##? +##? Options: +##? -h --help Prints this help +##? -n --never-backup Never do a backup without prompt +##? -b --always-backup Always do a backup without prompt +##? -s --quiet-symlinks Run dotbot in super quiet mode. If backup, do it +##? without asking. +##? docs::parse "$@" output::answer "Creating dotfiles structure" "$DOTLY_PATH/bin/dot" dotfiles create | log::file "Creating dotfiles structure" || exit 1 -# @todo Backup if exists before +# Which yaml files +yaml_files=("conf.yaml") + +if platform::is_macos; then + if platform::is_macos_arm; then + yaml_files+=("conf.macos.yaml") + else + yaml_files+=("conf.macos-intel.yaml") + fi +else + yaml_files+=("conf.linux.yaml") +fi + +_args=() +if ${quiet_symlinks:-false}; then + _args+=(--super-quiet) +fi + +_args+=( + -d + "$DOTFILES_PATH" + -c +) + +# Dotly install symlinks +for yaml_file in "${yaml_files[@]}"; do + dotly_install_symlinks "$yaml_file" "${_args[@]}" +done + +if platform::command_exists yq && platform::command_exists jq; then + # Make a backup of existing current dotfiles + if ! ${always_backup:-} &&\ + ! ${never_backup:-} &&\ + output::yesno "Do you want to perform a backup of your current existing dotfiles if they exists" + then + always_backup=true + fi + + if ${always_backup:-false}; then + rename_suffix=$(date +%s) + + for yaml_file in "${yaml_files[@]}"; do + yaml_file="$(symlinks::get_file_path "$yaml_file")" + + [[ ! -f "$yaml_file" ]] && continue + links=($(dotbot::get_all_keys_in "link" "$(symlinks::get_file_path "$yaml_file")")) + + for link in "${links[@]}"; do + link="$(eval realpath -q -m -s "$link")" + + if [[ -f "$link" ]]; then + if ! ${quiet_symlinks:-} && + ! output::yesno "File '$link' exists. Do you want to backup it" + then + continue + fi + + backup_and_warn_if_exists "$link" "$rename_suffix" + fi + done + done + fi +else + output::error "🚨 Backup can not be performed because \`yq\` or \`jq\` commands are not available." + output::error "If you continue be sure to make a backup first of you existing .bashrc, .zshrc and any other shell files." + output::yesno "Do you want to continue now" || exit 5 +fi + +# Apply user symlinks output::answer "Setting up symlinks" -"$DOTLY_PATH/bin/dot" symlinks apply | log::file "Applying symlinks" || true +if ${quiet_symlinks:-false}; then + "$DOTLY_PATH/bin/dot" symlinks apply --quiet | log::file "Applying symlinks" || true +else + "$DOTLY_PATH/bin/dot" symlinks apply | log::file "Applying symlinks" || output::write "Error applying symlinks, see `dot self backup`" +fi touch "$HOME/.z" if ! str::contains zsh "$SHELL"; then @@ -32,11 +159,20 @@ if ! str::contains zsh "$SHELL"; then sudo chsh -s "$(command -v zsh)" | log::file "Setting zsh as default shell" fi +output::empty_line output::answer "Installing zim" -zsh "$ZIM_HOME/zimfw.zsh" install | log::file "Installing zim" +if zsh "$ZIM_HOME/zimfw.zsh" install | log::file "Installing zim"; then + output::solution "ZIM Framework Installed" +else + output::error "ZIM Framework could not be installed" + output::write "Use `dot self debug` to view what happened" + output::write "You will need to run manually the ZIM Framework install command" + output::write " zsh \"$ZIM_HOME/zimfw.zsh\" install" +fi +output::empty_line output::answer "Installing completions" -"$DOTLY_PATH/bin/dot" shell zsh reload_completions +"$DOTLY_PATH/bin/dot" shell zsh reload_completions || output::error "Error reloading completions" output::answer "Executing custom restoration scripts" install_scripts_path="$DOTFILES_PATH/restoration_scripts" @@ -49,3 +185,6 @@ if [ -d "$install_scripts_path" ]; then } done fi + +output::empty_line +output::solution "🏁 Now restart your terminal to finish the installation" diff --git a/scripts/self/migration b/scripts/self/migration new file mode 100755 index 000000000..50b002282 --- /dev/null +++ b/scripts/self/migration @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +set -uo pipefail +#set +e # Avoid crash if any function return false + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +. "$DOTLY_PATH/scripts/core/_main.sh" +. "$DOTLY_PATH/scripts/core/templating.sh" + +##? Executes migration scripts for dotfiles. If your dotly is updated and no +##? script version is provided then try to guess the latest necessary migration +##? script. +##? +##? Usage: +##? migration [-h | --help] +##? migration [-v | --version] +##? migration [] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? +##? Author: +##? Gabriel Trabanco Llano +docs::parse "$@" + +SCRIPT_NAME="dot self version" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +output::header "DOTLY migration wizard" + +[[ -z "${to_version:-}" ]] && [[ -f "$DOTFILES_PATH/.dotly_updated" ]] && to_version="$(uptate::migration_script_exits)" + +if [[ ${to_version:-false} == false ]]; then + to_version="$(find "$DOTLY_PATH/migration/" -name "*" -type f,l -executable -print0 -exec echo {} \; | xargs -I _ basename _ | sort --reverse | fzf --header "Select migration script version")" +fi + +if [[ -n "$to_version" ]] && [[ -x "$DOTLY_PATH/migration/$to_version" ]]; then + output::write "You will execute migration script for '$to_version' this could" + output::write "result in a damage of your current dotfiles if they are not" + output::write "organized as expected." + output::write "PLEASE PERFORM A BACKUP OF YOUR DOTFILES BEFORE CONTINUE" + output::empty_line + + ! output::yesno "Sure you want to continue" && exit 1 + output::empty_line + + #shellcheck source=/dev/null + . "$DOTLY_PATH/migration/$to_version" || output::error "Migration script '$to_version' could not be executed" +else + output::error "There is no migration script for version '$to_version' or is not a executable file" +fi + +if [[ -n "$to_version" ]] && { [[ -f "$DOTLY_PATH/symlinks/$to_version.yaml" ]] || [[ -f "$DOTLY_PATH/symlinks/$to_version.yml" ]]; }; then + output::header "Applying symlinks for '$to_version'" + "$DOTLY_PATH/bin/dot" symlinks update "$to_version" +fi diff --git a/scripts/self/src/dotly_autoupdate.sh b/scripts/self/src/dotly_autoupdate.sh new file mode 100644 index 000000000..df8cba549 --- /dev/null +++ b/scripts/self/src/dotly_autoupdate.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +. "$DOTLY_PATH/scripts/self/src/update.sh" + +autoupdate::dotly_updater() { + local CURRENT_DIR remote_dotly_minor + set +e # Avoid crash if any function return an error + + # Other needed variables + CURRENT_DIR="$(pwd)" + + # Change to dotly path + cd "$DOTLY_PATH" || return 1 + + [[ -f "$DOTFILES_PATH/.dotly_updated" ]] &&\ + [[ "${DOTLY_AUTO_UPDATE_MODE:-auto}" != "silent" ]] && { + output::empty_line + output::write " 🥳 🎉 🍾 DOTLY UPDATED 🥳 🎉 🍾 " + output::empty_line + migration_script="$(uptate::migration_script_exits)" + if [[ -n "$migration_script" ]]; then + output::write "Migration script is neccesary to be executed and must be done syncronously by executing:" + output::answer "dot self migration $migration_script" + output::empty_line + fi + + [[ -z "$migration_script" ]] && rm "$DOTFILES_PATH/.dotly_updated" + } + + [[ -f "$DOTFILES_PATH/.dotly_update_available" ]] && return 0 + + if files::check_if_path_is_older "$DOTLY_PATH" "${DOTLY_AUTO_UPDATE_PERIOD_IN_DAYS:-7}" "days" &&\ + ! git::check_local_repo_is_updated "origin" "$DOTLY_PATH" + then + touch "$DOTFILES_PATH/.dotly_update_available" + + remote_dotly_minor="$(update::check_minor_update)" + if [[ -z "$remote_dotly_minor" ]]; then + touch "$DOTFILES_PATH/.dotly_update_available_is_major" + fi + fi + + cd "$CURRENT_DIR" || return 1 +} + +autoupdate::dotly_success() { + if [[ -f "$DOTFILES_PATH/.dotly_update_available" ]] && [[ ! -f "$DOTFILES_PATH/.dotly_force_current_version" ]]; then + if [[ -f "$DOTFILES_PATH/.dotly_update_available_is_major" ]] && [[ "$(str::to_lower "$DOTLY_UPDATE_VERSION")" =~ minor$ ]]; then + return 0 + fi + + case "$(str::to_lower "${DOTLY_AUTO_UPDATE_MODE:-auto}")" in + "silent") + update::update_local_dotly_module + rm -f "$DOTFILES_PATH/.dotly_update_available" + ;; + "info") + output::empty_line + output::write " ---------------------------------------------" + output::write "| 🥳🎉🍾 NEW DOTLY VERSION AVAILABLE 🥳🎉🍾 |" + output::write " ---------------------------------------------" + output::empty_line + ;; + "prompt") + # Nothing to do here + ;; + *) # auto + output::answer "🚀 Updating DOTLY Automatically" + update::update_local_dotly_module + output::solution "Updated, restart your terminal." + rm -f "$DOTFILES_PATH/.dotly_update_available" + ;; + esac + fi +} + +autoupdate::dotly_reject() { + # Nothing to be updated + return 0 +} diff --git a/scripts/self/utils/install.sh b/scripts/self/src/install.sh similarity index 96% rename from scripts/self/utils/install.sh rename to scripts/self/src/install.sh index 74530292a..1c4565f49 100755 --- a/scripts/self/utils/install.sh +++ b/scripts/self/src/install.sh @@ -4,7 +4,7 @@ install_macos_custom() { if ! platform::command_exists brew; then output::error "brew not installed, installing" - if [ "$DOTLY_ENV" == "CI" ]; then + if [ "${DOTLY_ENV:-}" == "CI" ]; then export CI=1 fi diff --git a/scripts/self/src/symlinks/conf.macos-intel.yaml b/scripts/self/src/symlinks/conf.macos-intel.yaml new file mode 100644 index 000000000..ce95b1234 --- /dev/null +++ b/scripts/self/src/symlinks/conf.macos-intel.yaml @@ -0,0 +1,15 @@ +- clean: ['~'] + +- defaults: + link: + create: true + force: true + +- link: + ~/bin/bash: /usr/local/bin/bash + ~/bin/date: /usr/local/bin/gdate + ~/bin/find: /usr/local/opt/findutils/bin/gfind + ~/bin/make: /usr/local/opt/make/libexec/gnubin/make + ~/bin/sed: /usr/local/opt/gnu-sed/libexec/gnubin/sed + ~/bin/touch: /usr/local/bin/gtouch + \ No newline at end of file diff --git a/scripts/self/src/symlinks/conf.macos.yaml b/scripts/self/src/symlinks/conf.macos.yaml new file mode 100644 index 000000000..44e74b2fa --- /dev/null +++ b/scripts/self/src/symlinks/conf.macos.yaml @@ -0,0 +1,15 @@ +- clean: ['~'] + +- defaults: + link: + create: true + force: true + +- link: + ~/bin/bash: /opt/homebrew/bin/bash + ~/bin/date: /opt/homebrew/bin/gdate + ~/bin/find: /opt/homebrew/bin/gfind + ~/bin/make: /opt/homebrew/bin/gmake + ~/bin/sed: /opt/homebrew/bin/gsed + ~/bin/touch: /opt/homebrew/bin/gtouch + ~/bin/zsh: /opt/homebrew/bin/zsh \ No newline at end of file diff --git a/scripts/self/src/update.sh b/scripts/self/src/update.sh new file mode 100644 index 000000000..398da8baa --- /dev/null +++ b/scripts/self/src/update.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC2120 +update::check_minor_update() { + local local_dotly_version remote_dotly_versions tags_number tag_version + tags_number="${1:-10}" + local_dotly_version="$(git::dotly_repository_exec git::get_current_latest_tag)" + remote_dotly_versions=($(git::get_all_remote_tags_version_only $(git::get_submodule_property dotly url) | head -n${tags_number})) + + [ -n "$local_dotly_version" ] && for tag_version in "${remote_dotly_versions[@]}"; do + [ -z "$tag_version" ] && continue # I am not sure if this can happen + [ "$(platform::semver_compare "$local_dotly_version" "$tag_version")" -le 0 ] && break # Older version no check + + if platform::semver_is_minor_or_patch_update "$local_dotly_version" "$tag_version"; then + echo "$tag_version" + return 0 + fi + done + + return 1 +} + +# Get the latest minor using the HEAD as it could be +update::get_latest_minor_local_head() { + local current_tag_version latest_local_tag latest_tags_version tag_version return_code + current_tag_version="$(git::dotly_repository_exec git::get_commit_tag)" # Current HEAD tag + latest_local_tag="$(git::get_current_latest_tag)" + return_code=1 + + if [[ -z "$current_tag_version" ]] && [[ -n "$latest_local_tag" ]]; then + echo "$latest_local_tag" + return_code=0 + + elif [[ -n "$current_tag_version" ]]; then + latest_tags_version=($(git::get_all_local_tags)) + + # Select latest local minor tag taking the current HEAD tag as main + for tag_version in "${latest_tags_version[@]}"; do + [[ "$(platform::semver_compare "$current_tag_version" "$tag_version")" -le 0 ]] && break + if "$(platform::semver_is_minor_or_patch_update "$current_tag_version" "$tag_version")"; then + current_tag_version="$tag_version" + return_code=0 + break + fi + done + fi + + [[ -n "$current_tag_version" ]] && echo "$current_tag_version" + + return "$return_code" +} + +update::check_if_is_stable_update() { + local local_dotly_version remote_dotly_versions tags_number + set +e + + local_dotly_version="$(git::dotly_repository_exec git::get_current_latest_tag)" + remote_dotly_version="$(git::get_all_remote_tags_version_only $(git::get_submodule_property dotly url) | head -n1)" + + [[ "$(platform::semver_compare "$local_dotly_version" "$remote_dotly_version")" -eq -1 ]] && echo "$remote_dotly_version" +} + +# shellcheck disable=SC2120 +update::update_dotly_repository() { + local current_directory current_branch update_submodules return_code + set +e + + # Defaults values that are needed here + current_directory="$(pwd)" + return_code=0 + current_branch="$(git::get_submodule_property dotly branch)" + + # Arguments + update_submodules="${1:-}" + + { [[ -d "$DOTLY_PATH" ]] && cd "$DOTLY_PATH"; } || return 1 + + if git::is_in_repo; then + git discard >/dev/null 2>&1 + git checkout "$current_branch" >/dev/null 2>&1 + git pull >/dev/null 2>&1 + return_code=$? + + if [[ -n "$update_submodules" ]] && [[ $return_code -eq 0 ]]; then + git submodule update --init --recursive "$@" > /dev/null 2>&1 # $@ because maybe you want to update specific submodule only + fi + fi + + cd "$current_directory" || return $return_code + + return $return_code +} + +update::check_consistency_with_dotly_version() { + local local_commit_tag + local_commit_tag="$(git::dotly_repository_exec git::get_commit_tag)" + + case "$(str::to_lower "$DOTLY_UPDATE_VERSION")" in + "stable"|"minor") + if [ -z "$local_commit_tag" ] && [ ! -f "$DOTFILES_PATH/.dotly_force_current_version" ]; then + output::error "Error in your Dotly configuration, 'DOTLY_UPDATE_VERSION'" + output::empty_line + output::answer "You have selected to update to $DOTLY_UPDATE_VERSION but you are not" + output::write "\tusing any stable version. Modify DOTLY_UPDATE_VERSION variable or use" + output::write "\tthe script:" + output::write "\t\tdot self version" + output::empty_line + output::write "You can also disable updates by using: 'dot self update --disable'" + output::empty_line + return 1 + fi + ;; + *) + return 0 + ;; + esac +} + +update::update_local_dotly_module() { + local current_dotly_hash local_dotly_version remote_dotly_minor remote_dotly_tag + set +e # Avoid crash if any function fail + + current_dotly_hash="$(git::get_local_HEAD_hash)" + local_dotly_version="$(git::dotly_repository_exec git::get_commit_tag)" + remote_dotly_minor="$(update::check_minor_update)" + remote_dotly_tag="$(update::check_if_is_stable_update)" + + # No update + if [ ! -f "$DOTFILES_PATH/.dotly_force_current_version" ]; then + return 1 + fi + + # Version consistency + if ! update::check_consistency_with_dotly_version >/dev/null; then + return 1 + fi + + # Update local repository + if ! git::check_local_repo_is_updated "origin" "$DOTLY_PATH"; then + update::update_dotly_repository + [[ -n "$local_dotly_version" ]] && git checkout "$local_dotly_version" # Keep current tag + fi + + case "$(str::to_lower "${DOTLY_AUTO_UPDATE_VERSION:-stable}")" in + "latest"|"beta") + ;; + "minor"|"only_minor") + if [ -n "$remote_dotly_minor" ]; then + git::dotly_repository_exec git checkout "$remote_dotly_minor" + fi + ;; + *) #Stable + git::dotly_repository_exec git checkout -q "$remote_dotly_tag" + ;; + esac + + rm -f "$DOTFILES_PATH/.dotly_force_current_version" + rm -f "$DOTFILES_PATH/.dotly_update_available" + rm -f "$DOTFILES_PATH/.dotly_update_available_is_major" + echo "$current_dotly_hash" >| "$DOTFILES_PATH/.dotly_updated" +} + +uptate::migration_script_exits() { + local latest_migration_script update_previous_commit + latest_migration_script="$(find "$DOTLY_PATH/migration/" -name "*" -type f,l -executable -print0 -exec echo {} \; | sort --reverse | head -n 1 | xargs)" + + # No update no migration necessary + if [[ ! -f "$DOTFILES_PATH/.dotly_updated" ]] || [[ -z "$latest_migration_script" ]]; then + return 1 + fi + + # If was added in previous commit + if ! git::check_file_exists_in_previous_commit "$latest_migration_script"; then + echo "$latest_migration_script" + return 0 + fi + + # Get previous commit and check if was added after + update_previous_commit="$(cat "$DOTFILES_PATH/.dotly_updated")" + [[ -z "$update_previous_commit" ]] && return 1 # Could not be checked if migration script should be executed + + git::check_file_is_modified_after_commit "$latest_migration_script" "$update_previous_commit" && echo "$latest_migration_script" +} diff --git a/scripts/self/update b/scripts/self/update index 9d0ca7df3..d1e339fb6 100755 --- a/scripts/self/update +++ b/scripts/self/update @@ -2,18 +2,33 @@ set -euo pipefail -source "$DOTLY_PATH/scripts/core/_main.sh" +#shellcheck source=/dev/null +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "update.sh" ##? Update dotly to the latest stable release ##? ##? Usage: -##? update +##? update [--disable | --enable] +##? +##? Options: +##? --disable Deactivate the dotly update command +##? --enable Activate the dotly update command +##? docs::parse "$@" -cd "$DOTLY_PATH" -git discard >/dev/null 2>&1 -git checkout master >/dev/null 2>&1 -git pull >/dev/null 2>&1 -git submodule update --init --recursive >/dev/null 2>&1 +if ${disable:-enable}; then + touch "$DOTFILES_PATH/.dotly_force_current_version" + exit 0 +elif ${enable:-false}; then + rm -f "$DOTFILES_PATH/.dotly_force_current_version" + exit 0 +fi + +update::update_local_dotly_module -output::answer '✅ dotly updated to the latest version' +if [[ -f "$DOTFILES_PATH/.dotly_updated" ]]; then + output::answer '✅ dotly updated to the latest version' +else + output::answer '👌 You already have latest dotly version' +fi diff --git a/scripts/self/version b/scripts/self/version new file mode 100755 index 000000000..65684e9b8 --- /dev/null +++ b/scripts/self/version @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -uo pipefail +set +e # Avoid crash if any function return false + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +. "$DOTLY_PATH/scripts/core/_main.sh" +. "$DOTLY_PATH/scripts/core/templating.sh" +dot::load_library "update.sh" + +##? Select a specific dotly version from local dotly module. If you select +##? any of: latest, stable, minor, it also modifies you 'exports.sh' file +##? +##? Usage: +##? version [-h | --help] +##? version [-v | --version] +##? version [-r | --view-remote] +##? version +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? -r --view-remote View remote versions (only view) +##? +##? Author: +##? Gabriel Trabanco Llano +docs::parse "$@" + +SCRIPT_NAME="dot self version" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +if ${view_remote:-false}; then + git::get_all_remote_tags_version_only + exit 0 +fi + +local_tags=($(git::get_all_local_tags) "minor" "stable" "latest") +selected_tag="$(printf "%s\n" "${local_tags[@]}" | fzf --header "Select a dotly version" --preview "")" + +case "$selected_tag" in + "minor") + current_tag_version="$(update::get_latest_minor_local_head)" # Current HEAD tag or stable + if [[ -n "$current_tag_version" ]]; then + git::dotly_repository_exec git checkout -q "$current_tag_version" + output::solution "Switched to latest DOTLY stable version $current_tag_version" + modify_bash_file_variable "$DOTFILES_PATH/shell/exports.sh" "DOTLY_UPDATE_VERSION" "minor" + else + output::error "No releases locally yet" + fi + ;; + "stable") + latest_stable="$(git::get_current_latest_tag)" + if [[ -n "$latest_stable" ]]; then + git::dotly_repository_exec git checkout -q "$latest_stable" + output::solution "Switched to latest DOTLY stable version $latest_stable" + modify_bash_file_variable "$DOTFILES_PATH/shell/exports.sh" "DOTLY_UPDATE_VERSION" "stable" + else + output::error "No releases locally yet" + fi + ;; + "latest") + git::dotly_repository_exec git checkout -q "master" + ;; + *) + if [[ -n "$selected_tag" ]]; then + git::dotly_repository_exec git checkout -q "$selected_tag" + output::solution "Switched to DOTLY version $selected_tag" + fi + ;; +esac + +if [[ -n "$selected_tag" ]]; then + output::answer "DOTLY is update locked. To unlock updates use: dot self update --enable" + touch "$DOTFILES_PATH/.dotly_force_current_version" +fi diff --git a/scripts/shell/zsh b/scripts/shell/zsh index a52e54d8e..427a08566 100755 --- a/scripts/shell/zsh +++ b/scripts/shell/zsh @@ -9,6 +9,7 @@ source "$DOTLY_PATH/scripts/core/_main.sh" ##? zsh test_performance ##? zsh reload_completions ##? zsh clean_cache +##? zsh fix_permissions docs::parse "$@" case $1 in @@ -48,6 +49,11 @@ case $1 in output::empty_line output::answer 'Now restart your terminal' ;; +"fix_permissions") + sudo -v + sudo chown -R $(whoami) /usr/local/share/zsh /usr/local/share/zsh/site-functions + chmod u+w /usr/local/share/zsh /usr/local/share/zsh/site-functions + ;; *) exit 1 ;; diff --git a/scripts/symlinks/apply b/scripts/symlinks/apply index 2731cac40..2233a7417 100755 --- a/scripts/symlinks/apply +++ b/scripts/symlinks/apply @@ -2,34 +2,46 @@ set -euo pipefail -source "$DOTLY_PATH/scripts/core/_main.sh" +. "$DOTLY_PATH/scripts/core/_main.sh" symlinks::apply() { + [[ -z "${1:-}" ]] && return 1 local -r CONFIG="$DOTFILES_PATH/symlinks/$1" shift echo - "$DOTLY_PATH/modules/dotbot/bin/dotbot" -d "$DOTFILES_PATH" -c "$CONFIG" "$@" + "$DOTLY_PATH/modules/dotbot/bin/dotbot" "$@" "$CONFIG" || output::error "Error applying symlinks file name '$(basename "$CONFIG")'" echo } ##? Apply all symlinks ##? ##? Usage: -##? apply +##? apply [-h | --help] +##? apply [-v | --version] +##? apply [-q | --quiet] ##? docs::parse "$@" +SCRIPT_NAME="dot symlinks apply" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-false}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + symlinks::apply "conf.yaml" if platform::is_macos; then if platform::is_macos_arm; then - symlinks::apply "conf.macos.yaml" + symlinks::apply "conf.macos.yaml" "${_args[@]}" else - symlinks::apply "conf.macos-intel.yaml" + symlinks::apply "conf.macos-intel.yaml" "${_args[@]}" fi else - symlinks::apply "conf.linux.yaml" + symlinks::apply "conf.linux.yaml" "${_args[@]}" fi log::success "Done!" diff --git a/scripts/symlinks/delete b/scripts/symlinks/delete new file mode 100755 index 000000000..75b8b832c --- /dev/null +++ b/scripts/symlinks/delete @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +#shellcheck disable=SC2016,SC2207 + +# TODO Test, after all tests work delete this +# [ ] Test Delete by value skiping file deletion +# [ ] Test Delete by value +# [ ] Test Delete by link skiping file deletion +# [ ] Test Delete by link +# [ ] All task completed + +set -eou pipefail + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "dotbot_yaml.sh" +dot::load_library "symlinks.sh" + +##? Delete a symbolic link in the "link" directive of your dotbot yaml file. +##? This also delete the file if no option --skip is provided. +##? +##? If not is provided, then use: +##? - '$DOTFILES_PATH/symlinks/conf.yaml' +##? +##? Usage: +##? delete [-h | --help] +##? delete [-v | --version] +##? delete [-b | --by-value] [-s | --skip] [--yaml=] [] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? -b --by-value Delete by the value of the link in the instead +##? of deleting by the link. +##? -s --skip Skip the deletion of the file in DOTFILES and delete only +##? the value in the symlink yaml file. +##? --yaml= Use a custom yaml file instead of default one which is +##? DOTFILES_PATH/symlinks/conf.yaml +##? +##? Author: +##? Gabriel Trabanco Llano +##? +docs::parse "$@" + +SCRIPT_NAME="dot dotfiles link" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +yaml_file="$(dotbot::yaml_file_path "${yaml:-conf}")" +[[ -z "${yaml_file:-}" ]] && output::error "The dotbot yaml file \`$yaml_file\` does not exists." && exit 1 + +links=("${origin_link_path:-}") + +preview_cmd='. "$DOTLY_PATH/scripts/core/_main.sh";' +preview_cmd+='dot::load_library "dotbot_yaml.sh" "symlinks";' +preview_cmd+="yaml_file=\"$yaml_file\";" +preview_cmd+='echo "Press Tab+Shift to select multiple options.\n' +preview_cmd+='Press Ctrl+C to exit with no selection.\n\n' + + +if [ -z "${links[*]}" ] && ${by_value:-false}; then + preview_cmd+='Linked in:\n' + preview_cmd+='\t$(symlinks::get_link_by_linked_path "$yaml_file" {};)";' + dotfiles=($(dotbot::get_all_values_in "link" "$yaml_file" | symlinks::fzf -m --preview "${preview_cmd[*]}" --header "Select dotfile(s) link to delete.")) + + for df in "${dotfiles[@]}"; do + if ${skip:-false}; then + if symlinks::delete_link_by_link_value "$df" "$yaml_file"; then + output::solution "Link for \`$df\` deleted from yaml file." + else + output::error "Link for \`$df\` could not be deleted from yaml file." + fi + else + symlinks::delete_link_and_files_by_link_value "$yaml_file" "$df" + output::solution "\`$df\` deleted." + fi + done +else + if [ -z "${links[*]}" ]; then + preview_cmd+='Point to file in \"DOTFILES_PATH\":\n' + preview_cmd+='\t$(symlinks::get_linked_path_by_link "$yaml_file" {})";' + links=($(symlinks::get_all_links "$yaml_file" | symlinks::fzf -m --preview "${preview_cmd[*]}" --header "Select link(s) to delete and it 'dotfile' file.")) + fi + + for link in "${links[@]}"; do + link="$(dotbot::relative_path "$link")" + if ${skip:-false}; then + # Delete only the symlink in yaml and symbolic link + if symlinks::delete_link "$yaml_file" "$link"; then + output::solution "\`$link\` link deleted from yaml file." + else + output::error "Error deleting \`$link\` symlink." + fi + else + # Delete the symliks, files and the symlink in yaml + if symlinks::delete_link_and_files "$yaml_file" "$link"; then + output::solution "\`$link\` deleted from yaml and symlinked files." + else + output::error "Error deleting \`$link\` and the files it is linking to." + fi + fi + done +fi diff --git a/scripts/symlinks/edit b/scripts/symlinks/edit new file mode 100755 index 000000000..b279172d0 --- /dev/null +++ b/scripts/symlinks/edit @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +#shellcheck disable=SC1091,SC2016,SC2207 + +# TODO Review that is using new symlinks.sh and dotbot_yaml.sh libraries +# TODO Tests +# [ ] Test Edit by value skiping file deletion +# [ ] Test Edit by value +# [ ] Test Edit by value not providing any origin +# [ ] Test Edit not providing any origin +# [ ] Test Edit by link without any new value +# [ ] Test Edit by link wit a new value +# [ ] Test Edit by link skiping file deletion +# [ ] All task completed + +set -eou pipefail + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "dotbot_yaml.sh" +dot::load_library "symlinks.sh" + +##? Edit the link section of dotbot yaml file. With this you could edit a +##? current symbolic link that is applied when you use `dot symlinks apply' +##? Important fact is that this command do not delete or replace any file +##? in your DOTFILES_PATH. +##? +##? If not is provided, then use: +##? - '$DOTFILES_PATH/symlinks/conf.yaml' +##? +##? Usage: +##? edit [-h | --help] +##? edit [-v | --version] +##? edit [-b | --by-value] [--yaml=] [ []] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? -b --by-value Edit by the value of the link in the . +##? --yaml= Use a custom yaml file instead of default one which is +##? DOTFILES_PATH/symlinks/conf.yaml +##? +##? Author: +##? Gabriel Trabanco Llano +##? +docs::parse "$@" + +SCRIPT_NAME="dot dotfiles link" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +output::empty_line + +yaml_file="$(dotbot::yaml_file_path "${yaml:-}")" +[[ -z "${yaml_file:-}" ]] && output::error "The dotbot yaml file \`$yaml_file\` does not exists." && exit 1 + +if [[ -n "${origin_link_path:-}" ]]; then + yaml_link_value="$(symlinks::link_exists "$yaml_file" "$origin_link_path")" + [[ -z "${yaml_link_value:-}" ]] && output::error "Given link does not exists so it is no possible to edit." && exit 1 + + [[ -z "${new_origin_link_path:-}" ]] && output::question_default "Write the new link for \`$origin_link_path\`" "$origin_link_path" "new_origin_link_path" + [[ "$origin_link_path" == "$new_origin_link_path" ]] && output::error "Skipping, nothing to change." && exit 1 + + if symlinks::edit_link_by_link_path "$yaml_file" "$origin_link_path" "$new_origin_link_path"; then + output::error "Link could not be edited" + exit 1 + else + output::solution "Link \`$origin_link_path\` changed to \`$new_origin_link_path\` for file: $yaml_link_value" + fi +else + preview_cmd='. "$DOTLY_PATH/scripts/core/_main.sh";' + preview_cmd+='dot::load_library "dotbot_yaml.sh";' + preview_cmd+="yaml_file='$yaml_file';" + preview_cmd+='echo "Press Tab+Shift to select multiple options.\n' + preview_cmd+='Press Ctrl+C to exit with no selection.\n\n' + preview_cmd+='----\n\nLinks:\n\n' + preview_cmd+='$(cat "$yaml_file" | dotbot::jq_yaml_file -r ".[].link | values")";' + + if [ -z "${links[*]}" ] && $by_value; then + dotfiles=($(symlinks::get_all_link_values "$yaml_file" | symlinks::fzf -m --preview "${preview_cmd[*]}" --header "Select dotfile(s) to edit the link.")) + + for df in "${dotfiles[@]}"; do + links=("$(dotbot::get_key_by_value_in "link" "$df" "$yaml_file")") + done + elif [ -z "${links[*]}" ]; then + links=($(symlinks::get_all_links "$yaml_file" | symlinks::fzf -m --preview "${preview_cmd[*]}" --header "Select link(s) to edit the link.")) + fi + + for current_link in "${links[@]}"; do + yaml_link_value="$(dotbot::get_value_of_key_in "link" "$current_link" "$yaml_file")" + + output::question_default "Write the new link for '$current_link'" "$current_link" "new_link" + + if [[ -n "$new_link" && "$new_link" != "$current_link" ]]; then + symlinks::edit_link_by_link_path "$yaml_file" "$current_link" "$new_link" + + if [[ -n "$(symlinks::link_exists "$yaml_file" "$current_link")" ]]; then + if [[ -n "$(symlinks::link_exists "$yaml_file" "$new_link")" ]]; then + output::error "Link '$new_link' was added to point the same link as '$current_link' but could not be deleted from '$yaml_file'" + output::error "Delete it manually or you will preserve old and new symlink." + else + output::error "Link '$current_link' could not be replaced for '$new_link' in the '$yaml_file'." + fi + exit 1 + else + output::solution "Link '$current_link' changed to '$new_link' for file: $yaml_link_value" + fi + else + output::answer "No new link or point to the same link" + exit 0 + fi + done +fi diff --git a/scripts/symlinks/link b/scripts/symlinks/link new file mode 100755 index 000000000..609b3033d --- /dev/null +++ b/scripts/symlinks/link @@ -0,0 +1,119 @@ +#!/usr/bin/env bash + +# TODO Review that is using new symlinks.sh and dotbot_yaml.sh libraries + +set -eou pipefail + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "dotbot_yaml +.sh" +dot::load_library "symlinks.sh" + +edit_link_question() { + local current_link dotfile_relative_path + current_link="${1:-}" + dotfile_relative_path="${2:-$current_link}" + [[ -z "$current_link" ]] && return 1 + + output::question_default "Write the new path to link '$dotfile_relative_path'" "$current_link" "new_link" + echo "${new_link:-$current_link}" +} + +##? Create a symbolic link using dotbot with the command 'dot symlinks apply' +##? without manual work. Useful if you want to create symlinks in your +##? machine without moving files manually. Create a symbolic link using 'ln -s' +##? and append that link to default or given . +##? +##? If not is provided, then use: +##? - '$DOTFILES_PATH/symlinks/conf.yaml' +##? +##? Usage: +##? link [-h | --help] +##? link [-v | --version] +##? link [-s | --skip] [-m | --only-move] [--yaml=] [] +##? link +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? -s --skip Skip moving files to your DOTFILES_PATH. This makes +##? possible to make a symbolic link from your desktop to +##? iCloud folder and preserver across machines using your +##? dotfiles. +##? -m --only-move Only move file but no add it to yaml file +##? --yaml= Use a custom yaml file instead of default one which is +##? DOTFILES_PATH/symlinks/conf.yaml +##? +##? Author: +##? Gabriel Trabanco Llano +##? +docs::parse "$@" + +SCRIPT_NAME="dot dotfiles link" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +if [ -z "${origin_link_path:-}" ]; then + output::error "Wrong call to script" + output::write " See usage:" + output::write " $SCRIPT_NAME --help" + output::empty_line + "$0" --help + exit 1 +fi + +yaml_file="$(dotbot::yaml_file_path "${yaml:-}")" + +[[ -z "${yaml_file:-}" ]] && output::error "The dotbot yaml file '$yaml_file' does not exists." && exit 1 + +if [[ -z "$link_dotfile_directory" ]]; then + while [ -z "$link_dotfile_directory" ]; do + output::write "Write the relative path to your DOTFILES_PATH (path inside) for the file or" + output::write "directory. Empty to put it inside your DOTFILES_PATH root directory." + output::question "Write the relative path to your DOTFILES_PATH" "link_dotfile_directory" + output::empty_line + + [[ -z "$link_dotfile_directory" ]] &&\ + output::yesno "Do you want to store the file in your DOTFILES_PATH root directory" &&\ + output::empty_line &&\ + link_dotfile_directory="$DOTFILES_PATH" &&\ + break + done +fi + +if ${only_move:-}; then + # Fixme symlinks::move_from_pwd_to_dotfiles is not called symlinks::move_from_pwd_to_dotbot + symlinks::move_from_pwd_to_dotfiles "$origin_link_path" "$link_dotfile_directory" + exit $? +elif ${skip:-false}; then + # FIXME Replace dotbot::create_relative_link + # FIXME use symlinks::new_link ? + dotbot::add_or_edit_json_value_to_directive "link" "$(dotbot::create_relative_link "$origin_link_path")" "$(dotbot::create_relative_link "$link_dotfile_directory")" "$yaml_file" +else + symlinks::add_yaml_and_move_files "$yaml_file" "$origin_link_path" "$link_dotfile_directory" +fi + +# FIXME Replace dotbot::create_relative_link +link_value="$(dotbot::get_value_of_key_in "link" "$(dotbot::create_relative_link "$origin_link_path")" "$yaml_file")" + +if [ -z "$link_value" ] && [ ! -f "$origin_link_path" ]; then + output::error "The link could not be created and the file was no moved" + exit 1 +elif [ -z "$link_value" ]; then + output::error "The file was moved but the link could not be added" + exit 1 +elif [ ! -e "$DOTFILES_PATH/$link_value" ] && ! $skip; then + output::error "The link was added but the file was no moved" + output::answer "Deleting from yaml file." + dotbot::delete_by_key_in "link" "$(realpath -m -q -s "$origin_link_path")" "$yaml_file" + exit 1 +fi + +output::solution "File moved and link created." diff --git a/scripts/symlinks/restore b/scripts/symlinks/restore new file mode 100755 index 000000000..e2b8d4ef9 --- /dev/null +++ b/scripts/symlinks/restore @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# TODO Review that is using new symlinks.sh and dotbot_yaml.sh libraries + +set -euo pipefail + +[[ -z "$DOTLY_PATH" ]] && exit 1 + +. "$DOTLY_PATH/scripts/core/_main.sh" +dot::load_library "dotbot_yaml.sh" +dot::load_library "symlinks.sh" + +##? Restore moved file to inside your DOTFILES_PATH to its current link and +##? delete the link from your dotbot yaml file and move the file out (if the +##? link is out) of your DOTFILES_PATH. +##? +##? You can use directly to any linked file or the file inside your +##? DOTFILES_PATH. If the file has no link or no file in your dotfiles will +##? return an error. +##? +##? If not is provided, then use: +##? - '$DOTFILES_PATH/symlinks/conf.yaml' +##? +##? Usage: +##? restore [-h | --help] +##? restore [-v | --version] +##? restore undoln +##? restore [--yaml=] [] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? --yaml= Use a custom yaml file instead of default one which +##? is: $DOTFILES_PATH/symlinks/conf.yaml +##? undoln Just to undo ln links without any yaml file +##? +##? +##? Author: +##? Gabriel Trabanco Llano +docs::parse "$@" + +SCRIPT_NAME="dot dotfiles restore" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + +case "$1" in + undoln) + if [[ -z "$link_file" ]] || [[ ! -L "$link_file" ]]; then + output::error "Symlink is needed" + exit 1 + fi + link_realpath="$(eval realpath --logical --no-symlinks --quiet "$link_file")" + linked_realpath="$(eval realpath --logical --quiet "$link_file")" + + if [[ -z "$link_realpath" ]] || [[ -z "$linked_realpath" ]]; then + output::error "No link found or link could not be resolved. Maybe is pointing to a missing file path." + exit 1 + fi + + rm -f "$link_realpath" + mv -i "$linked_realpath" "$link_realpath" + rmdir -p "$(dirname "$linked_realpath")" >/dev/null 2>&1 + output::solution "Link '$link_realpath' restored." + ;; + *) + if [[ ! -L "$link_or_dotfile_path" ]]; then + output::error "Given file is not a symbolic link" + exit 1 + fi + + yaml_file="$(dotbot::yaml_file_path "${yaml:-}")" + [[ -z "${yaml_file:-}" ]] && output::error "The dotbot yaml file '$yaml_file' does not exists." && exit 1 + + if [ -n "${link_or_dotfile_path:-}" ]; then + restore_links=("$(symlinks::link_exists "$yaml_file" "$link_or_dotfile_path")") + [ -z "${restore_links[*]:-}" ] &&\ + output::error "Nothing to be restored." &&\ + exit 1 + else + preview_cmd='. "$DOTLY_PATH/scripts/core/_main.sh";' + preview_cmd+='dot::load_library "dotbot_yaml.sh"";' + preview_cmd+="yaml_file='$yaml_file';" + preview_cmd+='echo "Press Tab+Shift to select multiple options.\n' + preview_cmd+='Press Ctrl+C to exit with no selection.\n\n' + preview_cmd+='Point to file in \"DOTFILES_PATH\":\n' + preview_cmd+='\t$(cat "$yaml_file" | dotbot::get_value_of_key_in "link" {})";' + restore_links=($(dotbot::get_all_keys_in "link" "$yaml_file" | symlinks::fzf -m --preview "${preview_cmd[*]}" --header "Select link(s) to be restored.")) + fi + + for restore_link in "${restore_links[@]}"; do + symlinks::restore_by_link "$yaml_file" "$restore_link" + output::solution "'$restore_link' restored." + done + ;; +esac diff --git a/scripts/symlinks/src/dotbot_yaml.sh b/scripts/symlinks/src/dotbot_yaml.sh new file mode 100644 index 000000000..a01814ddb --- /dev/null +++ b/scripts/symlinks/src/dotbot_yaml.sh @@ -0,0 +1,371 @@ +#!/usr/bin/env bash +#shellcheck disable=SC2016 + +DOTBOT_BASE_PATH="${DOTBOT_BASE_PATH:-$DOTFILES_PATH}" +DOTBOT_DEFAULT_YAML_FILES_BASE_PATH="${DOTBOT_DEFAULT_YAML_FILES_BASE_PATH:-$DOTBOT_BASE_PATH/symlinks}" + +dotbot::yaml_file_path() { + local yaml_file_posibilities yaml_file yaml_dir_path + yaml_file="${1:-}" + yaml_dir_path="${2:-$DOTBOT_DEFAULT_YAML_FILES_BASE_PATH}" + yaml_file_posibilities=( + "$yaml_file" + "$yaml_file.yaml" + "$yaml_file.yml" + "$yaml_dir_path/$yaml_file" + "$yaml_dir_path/$yaml_file.yaml" + "$yaml_dir_path/$yaml_file.yml" + ) + + for f in "${yaml_file_posibilities[@]}" ; do + [[ -f "$f" ]] && [[ -w "$f" ]] && yaml_file="$f" && break + done + + if [[ ! -w "$yaml_file" ]]; then + yaml_file="" + fi + + echo "$yaml_file" && return 0 +} + +dotbot::exec_in_dotbot_path() { + local return_code + [[ -z "$DOTBOT_BASE_PATH" ]] && [[ -d "$DOTBOT_BASE_PATH" ]] && output::error "Fatal error $DOTBOT_BASE_PATH not found" && exit 1 + cd "$DOTBOT_BASE_PATH" || return 1 + eval "$@" + return_code=$? + return ${return_code:-0} +} + +# Return a path to store in the dotbot yaml file +dotbot::relative_path() { + local link_path + [[ -z "${1:-}" ]] && return + + link_path="${1//\~/$HOME}" + link_path="$(realpath -qms --relative-base="$DOTBOT_BASE_PATH" "$link_path" 2>/dev/null || true)" + link_path="${link_path//$HOME/~}" + echo "$link_path" +} + +# Return a path to do any stuff with the file +dotbot::realpath() { + local file_path + [[ -z "${1:-}" ]] && return + + file_path="${1//\~/$HOME}" + dotbot::exec_in_dotbot_path realpath -qms "\"$file_path\"" 2>/dev/null || true +} + +# Move files by converting values to relative in dotbot or don't +dotbot::mv() { + local from_argn from to_argn to _args relative_dotbot + relative_dotbot=false + while true; do + case "$1" in + --relative-dotbot) + relative_dotbot=true + shift + ;; + *) + break 2 + ;; + esac + done + [[ $# -lt 2 ]] && return 1 + _args=( "$@" ) + from_argn=$(( ${#_args[@]:-2} - 2 )) + to_argn=$(( ${#_args[@]:-2} - 1 )) + from="$(dotbot::realpath "${_args[$from_argn]}")" + to="$(dotbot::realpath"${_args[$to_argn]}")" + unset "_args[$from_argn]" "_args[$to_argn]" + + if [[ -e "$from" ]]; then + # TODO Undo in production + echo mv "${_args[@]}" "$from" "$to" + else + return 1 + fi +} + +# Remove files by converting or not the path to relative to DOTBOT_BASE_PATH +dotbot::rm() { + local file_argn file_path _args rm_cmd relative_dotbot + rm_cmd="rm" + relative_dotbot=false + while true; do + case "$1" in + --rm-cmd) + rm_cmd="${2:-}" + shift 2 + ;; + --relative-dotbot) + relative_dotbot=true + shift + ;; + *) + break 2 + ;; + esac + done + [[ $# -lt 2 ]] && return 1 + _args=( "$@" ) + file_argn=$(( ${#_args[@]:-1} - 1 )) + file_path="$(dotbot::realpath "${_args[$file_argn]}")" + unset "_args[$file_argn]" + + # TODO Undo in production + if [[ -e "$file_path" ]]; then + #shellcheck disable=SC2086 + echo $rm_cmd "${_args[@]}" "\"$file_path\"" + fi +} + +# Save json as yaml +dotbot::save_as_yaml() { + local file_path input + file_path="${1:-}" + + [[ -z "$file_path" ]] && return 1 + eval mkdir -p "$(dirname "$file_path")" + touch "$file_path" + json::to_yaml /dev/null 2>&1 + fi +} + +# Not used but it worked +# TODO Production work +symlinks::restore_by_dotfile_file_path() { + local yaml_file link dotfiles_file_path + yaml_file="${1:-}" + + [[ ! -f "$yaml_file" ]] && return 1 + + dotfiles_file_path="$(dotbot::relative_path "${2:-}")" + link="$(dotbot::get_key_by_value_in "link" "$dotfiles_file_path" "$yaml_file")" + + # if [ -n "$link" ]; then + # symlinks::restore_by_link "$yaml_file" "$link" + # fi +} + +# TODO Production work +symlinks::edit_link_by_link_path() { + local yaml_file old_link_realpath old_link new_link link_value link_value_realpath + yaml_file="${1:-}" + old_link_realpath="$(realpath -qs "${2:-}" 2>/dev/null || true)" + old_link="$(dotbot::relative_path "$old_link_realpath")" + new_link="$(dotbot::relative_path "${3:-}")" + + [[ ! -f "$yaml_file" ]] && return 1 + + link_value="$(dotbot::get_value_of_key_in "link" "$old_link" "$yaml_file")" + + if [ -n "$link_value" ]; then + link_value_realpath="$(dotbot::realpath "$link_value")" + + # 1. Remove the old link + #shellcheck disable=SC2016 + dotobot::rm -f "$old_link" + + # 2. Create the new link + # TODO Unecho in production + echo ln -s "$link_value_realpath" "$(dotbot::realpath "$new_link")" + + # 3. Delete the old link + # TODO Uncoment in production + # dotbot::delete_by_key_in "link" "$old_link" "$yaml_file" || true + + # 4. Add it the new one + # TODO Uncomment in production + # dotbot::add_or_edit_json_value_to_directive "link" "$new_link" "$link_value" "$yaml_file" + [[ ! -f "$old_link" ]] && ! symlinks::link_exists "$old_link" + fi + + return 1 +} + +# TODO Production work +symlinks::edit_link_by_dotfile_file_path() { + local yaml_file dotfiles_file_path new_link old_link + yaml_file="${1:-}" + dotfiles_file_path="$(dotbot::relative_path "${2:-}")" + new_link="${3:-}" + + [[ ! -f "$yaml_file" ]] && return 1 + + old_link="$(dotbot::get_key_by_value_in "link" "$dotfiles_file_path" "$yaml_file")" + + # [ -n "$old_link" ] && symlinks::edit_link_by_link_path "$yaml_file" "$old_link" "$new_link" + echo "" +} + +# Delete dotobot link if exists in yaml file +# TODO Production work +symlinks::delete_link() { + local yaml_file link dotbot_file_path + yaml_file="${1:-}" + link="${2:-}" + + [[ -z "$link" || ! -f "$yaml_file" ]] && return 1 + + link="$(dotbot::relative_path "$link")" + + if [[ -n "$(symlinks::link_exists "$link")" ]]; then + # 1. Delete the link + dotbot::rm -f "$link" &>/dev/null + + # 2. Delete the value in yaml file + # dotbot::delete_by_key_in "link" "$link" "$yaml_file" + else + return 1 + fi +} + +# Delete by the provided link +# Executes "rm -i -rf" to the file in DOTBOT_BASE_PATH unless you pass more +# arguments which would be the argument to delete the file being the +# last param the link. Example +# symlinks::link_and_files "$yaml_file" "~/mylink" rm -i +# This will delete using "rm -i" +# TODO Production work +symlinks::delete_link_and_files() { + local yaml_file link delete_cmd dotbot_file_path + yaml_file="${1:-}" + link="${2:-}" + shift 2 + + { [[ -z "$yaml_file" ]] || [[ -z "$link" ]] || [[ ! -f "$yaml_file" ]]; } && return 1 + + if [[ -z "${*:-}" ]]; then + delete_cmd=(rm -i -rf) + else + delete_cmd=("$@") + fi + + link="$(dotbot::relative_path "$link")" + + if [[ -n "$(symlinks::link_exists "$yaml_file" "$link")" ]]; then + dotbot_file_path="$(dotbot::get_value_of_key_in "link" "$link" "$yaml_file")" + + if [[ -n "$dotbot_file_path" ]] && symlinks::delete_link "$yaml_file" "$link"; then + dotbot::rm --rm-cmd "${delete_cmd[*]}" "$dotbot_file_path" || true + else + return 1 + fi + else + return 1 + fi +} + +# Same as symlinks::delete_link but by the value of the link +symlinks::delete_link_by_link_value() { + local yaml_file link_value link delete_cmd + yaml_file="${1:-}" + link_value="$(dotbot::relative_path "${2:-}")" + + [[ ! -f "$yaml_file" || -z "${link_value:-}" ]] && return 1 + + link="$(dotbot::get_key_by_value_in "link" "$link_value" "$yaml_file")" + + if ! { [ -n "$link" ] && symlinks::delete_link "$yaml_file" "$link" "${@:-}"; }; then + return 1 + fi +} + +# Same as symlinks::delete_link_and_files but by the value of the link +symlinks::delete_link_and_files_by_link_value() { + local yaml_file link_value link delete_cmd + yaml_file="${1:-}" + link_value="$(dotbot::relative_path "${2:-}")" + shift 2 + + [[ ! -f "$yaml_file" || -z "${link_value:-}" ]] && return 1 + + link="$(dotbot::get_key_by_value_in "link" "$link_value" "$yaml_file")" + + if ! { [ -n "$link" ] && symlinks::delete_link_and_files "$yaml_file" "$link" "${@:-}"; }; then + return 1 + fi +} + +symlinks::find() { + local find_relative_path exclude_itself arguments preview + arguments=() + + case "${1:-}" in + --exclude) + exclude_itself=true; shift + ;; + *) + exclude_itself=false + ;; + esac + + find_relative_path="$DOTBOT_BASE_PATH/" + + if [ -e "$DOTBOT_BASE_PATH/${1:-}" ]; then + find_relative_path="$find_relative_path${1:-}"; shift + fi + + if $exclude_itself; then + arguments+=(-not -path "$find_relative_path") + fi + + arguments+=("$@") + + find "$find_relative_path" -not -iname ".*" "${arguments[@]}" -print0 -exec echo {} \; | while read -r item; do + printf "%s\n" "${item/$find_relative_path\//}" + done +} + +symlinks::fzf() { + local arguments preview multiple preview_cmd preview_path + preview=false + multiple=false + preview_cmd='echo "' + preview_path="$DOTBOT_BASE_PATH/" + arguments=() + + while [ ${#:-0} -gt 0 ]; do + case "${1:-}" in + --preview) + preview=true + arguments+=("${1:-}"); shift + arguments+=("${1:-}"); shift + ;; + -p|--preview-path) + [ -d "${2:-}" ] && preview_path="${2:-}/" + shift 2 + ;; + -m|--multi) + multiple=true + arguments+=(--multi); shift + + if [[ "${1:-}" =~ '^[0-9]+$' ]]; then + arguments+=("${1:-}"); shift + fi + ;; + *) + break 2 + ;; + esac + done + + arguments+=("$@") + (! $preview) && $multiple && preview_cmd+='Press Tab+Shift to select multiple options.\n' + + if ! $preview; then + preview_cmd+='Press Ctrl+C to exit with no selection.\n\nFile: {}\n\n--\n\n";' + preview_cmd+="cat $preview_path{}" + arguments+=(--preview) + arguments+=("$preview_cmd") + fi + + fzf "${arguments[@]}" +} diff --git a/scripts/symlinks/update b/scripts/symlinks/update new file mode 100755 index 000000000..7612b19ea --- /dev/null +++ b/scripts/symlinks/update @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -euo pipefail + +. "$DOTLY_PATH/scripts/core/_main.sh" + +symlinks::get_files() { + [[ -d "$DOTLY_PATH/symlinks" ]] &&\ + find "$DOTLY_PATH/symlinks" -name "*.yaml" -type f,l -print0 -exec echo {} \; |\ + xargs -I _ basename _ | sort +} + +symlinks::fzf() { + local piped_values + piped_values="$(] +##? +##? Options: +##? -h --help Show this help +##? -v --version Show the program version +##? -p --no-prompt Avoid warning the user about the consecuences of apply +##? a dotbot file. +##? +docs::parse "$@" + +SCRIPT_NAME="dot symlinks apply" +SCRIPT_VERSION="1.0.0" + +# Print name and version +if ${version:-false}; then + output::write "$SCRIPT_NAME v$SCRIPT_VERSION" + exit +fi + + +if [[ -z "$symlinks_file" ]]; then + symlinks_file="$(symlinks::get_files | symlinks::fzf)" + [[ -z "$symlinks_file" ]] && exit 0 + symlinks_file="$DOTLY_PATH/symlinks/$symlinks_file" +else + for f in "$symlinks_file" "$symlinks_file.yaml" "$symlinks_file.yml"; do + [[ -e "$f" ]] && symlinks_file="$f" && break + done + + if [[ ! -e "$symlinks_file" ]]; then + output::error "The file does not exists" + exit 1 + fi +fi + +if ! ${no_prompt:-false} && ! output::yesno "This could be danger your current dotfiles. Do you still want to continue"; then + exit 1 +fi + +output::header "Apply dotbot update to your dotfiles" +output::write "This will apply a selected symlinks to apply any dotly update" + +output::empty_line +"$DOTLY_PATH/modules/dotbot/bin/dotbot" -d "$DOTFILES_PATH" -c "$symlinks_file" +output::empty_line + +output::write "Remember to merge this symlinks file to yours in:" +output::answer "$DOTFILES_PATH/symlinks/conf.yaml" + \ No newline at end of file diff --git a/shell/bash/init.sh b/shell/bash/init.sh new file mode 100755 index 000000000..470017c87 --- /dev/null +++ b/shell/bash/init.sh @@ -0,0 +1,59 @@ +if [[ "$(ps -p $$ -ocomm=)" =~ (bash$) ]]; then + __right_prompt() { + RIGHT_PROMPT="" + [[ -n $RPS1 ]] && RIGHT_PROMPT=$RPS1 || RIGHT_PROMPT=$RPROMPT + if [[ -n $RIGHT_PROMPT ]]; then + n=$(($COLUMNS - ${#RIGHT_PROMPT})) + printf "%${n}s$RIGHT_PROMPT\\r" + fi + } + export PROMPT_COMMAND="__right_prompt" +fi + +PATH=$( + IFS=":" + echo "${path[*]:-}" +) +export PATH + +themes_paths=( + "$DOTFILES_PATH/shell/bash/themes" + "$DOTLY_PATH/shell/bash/themes" +) + +# bash completion +export BASH_COMPLETION_COMPAT_DIR="/usr/local/etc/bash_completion.d" +if [[ -r "/usr/local/etc/profile.d/bash_completion.sh" ]]; then + #shellcheck source=/dev/null + . "/usr/local/etc/profile.d/bash_completion.sh" +fi + +# brew Bash completion +if type brew &>/dev/null; then + HOMEBREW_PREFIX="$(brew --prefix)" + if [[ -r "${HOMEBREW_PREFIX}/etc/profile.d/bash_completion.sh" ]]; then + #shellcheck source=/dev/null + . "${HOMEBREW_PREFIX}/etc/profile.d/bash_completion.sh" + else + for COMPLETION in "${HOMEBREW_PREFIX}/etc/bash_completion.d/"*; do + #shellcheck source=/dev/null + [[ -r "$COMPLETION" ]] && . "$COMPLETION" + done + fi +fi +unset COMPLETION + +#shellcheck disable=SC2068 +for THEME_PATH in ${themes_paths[@]}; do + THEME_PATH="${THEME_PATH}/${DOTLY_THEME:-codely}.sh" + #shellcheck source=/dev/null + [ -f "$THEME_PATH" ] && . "$THEME_PATH" && break +done +unset THEME_PATH + +find {"$DOTLY_PATH","$DOTFILES_PATH"}"/shell/bash/completions/" -name "_*" -print0 -exec echo {} \; 2>/dev/null | xargs -0 -I _ echo _ | while read -r completion; do + [[ -z "$completion" ]] && continue + #shellcheck source=/dev/null + . "$completion" || echo -e "\033[0;31mBASH completion '$completion' could not be loaded\033[0m" +done +unset completion diff --git a/shell/bash/themes/codely.sh b/shell/bash/themes/codely.sh index 919c00b2e..68bdb2e20 100644 --- a/shell/bash/themes/codely.sh +++ b/shell/bash/themes/codely.sh @@ -4,6 +4,20 @@ MIDDLE_CHARACTER="◂" GREEN_COLOR="32" RED_COLOR="31" +prompt_dotly_autoupdate() { + if [ -f "$DOTFILES_PATH/.dotly_update_available" ] &&\ + { + [ "$(echo "$DOTLY_AUTO_UPDATE_MODE" | tr '[:upper:]' '[:lower:]')" != "minor" ] ||\ + { + [ "$(echo "$DOTLY_AUTO_UPDATE_MODE" | tr '[:upper:]' '[:lower:]')" == "minor" ] &&\ + [ ! -f "$DOTFILES_PATH/.dotly_update_available_is_major" ] + } + } + then + print -n "📥 | " + fi +} + codely_theme() { LAST_CODE="$?" current_dir=$(dot core short_pwd) @@ -15,8 +29,8 @@ codely_theme() { fi if [ -z "$CODELY_THEME_MINIMAL" ]; then - export PS1="\[\e[${STATUS_COLOR}m\]{\[\e[m\]${MIDDLE_CHARACTER}\[\e[${STATUS_COLOR}m\]}\[\e[m\] \[\e[33m\]${current_dir}\[\e[m\] " + export PS1="\$(prompt_dotly_autoupdate)\[\e[${STATUS_COLOR}m\]{\[\e[m\]${MIDDLE_CHARACTER}\[\e[${STATUS_COLOR}m\]}\[\e[m\] \[\e[33m\]${current_dir}\[\e[m\] " else - export PS1="\[\e[${STATUS_COLOR}m\]{\[\e[m\]${MIDDLE_CHARACTER}\[\e[${STATUS_COLOR}m\]}\[\e[m\] " + export PS1="\$(prompt_dotly_autoupdate)\[\e[${STATUS_COLOR}m\]{\[\e[m\]${MIDDLE_CHARACTER}\[\e[${STATUS_COLOR}m\]}\[\e[m\] " fi } diff --git a/shell/init-dotly.sh b/shell/init-dotly.sh new file mode 100644 index 000000000..ee13022b6 --- /dev/null +++ b/shell/init-dotly.sh @@ -0,0 +1,108 @@ +# Needed dotly functions +#shellcheck disable=SC2148 +function cdd() { + #shellcheck disable=SC2012 + cd "$(ls -d -- */ | fzf)" || echo "Invalid directory" +} + +function j() { + fname=$(declare -f -F _z) + + #shellcheck source=/dev/null + [ -n "$fname" ] || . "$DOTLY_PATH/modules/z/z.sh" + + _z "$1" +} + +function recent_dirs() { + # This script depends on pushd. It works better with autopush enabled in ZSH + escaped_home=$(echo "$HOME" | sed 's/\//\\\//g') + selected=$(dirs -p | sort -u | fzf) + + # shellcheck disable=SC2001 + cd "$(echo "$selected" | sed "s/\~/$escaped_home/")" || echo "Invalid directory" +} + +# Envs +# GPG TTY +GPG_TTY="$(tty)" +export GPG_TTY + +# shellcheck source=/dev/null +[[ -f "$DOTFILES_PATH/shell/exports.sh" ]] && . "$DOTFILES_PATH/shell/exports.sh" + +# Paths +# shellcheck source=/dev/null +[[ -f "$DOTFILES_PATH/shell/paths.sh" ]] && . "$DOTFILES_PATH/shell/paths.sh" + +# Add openssl if it exists +[[ -d "/usr/local/opt/openssl/bin" ]] && path+=("/usr/local/opt/openssl/bin") + +# Conditional paths +[[ -d "${JAVA_HOME:-}" ]] && path+=("$JAVA_HOME/bin") +[[ -d "${GEM_HOME:-}" ]] && path+=("$GEM_HOME/bin") +[[ -d "${GOHOME:-}" ]] && path+=("$GOHOME/bin") +[[ -d "$HOME/.deno/bin" ]] && path+=("$HOME/.deno/bin") +[[ -d "/usr/local/opt/ruby/bin" ]] && path+=("/usr/local/opt/ruby/bin") +[[ -d "/usr/local/opt/python/libexec/bin" ]] && path+=("/usr/local/opt/python/libexec/bin") +[[ -d "/usr/local/bin" ]] && path+=("/usr/local/bin") +[[ -d "/usr/local/sbin" ]] && path+=("/usr/local/sbin") +[[ -d "/bin" ]] && path+=("/bin") +[[ -d "/usr/bin" ]] && path+=("/usr/bin") +[[ -d "/usr/sbin" ]] && path+=("/usr/sbin") +[[ -d "/sbin" ]] && path+=("/sbin") + +# Brew add gnutools in macos only +# UNAME_BIN and BREW_BIN are necessary because paths are not yet loaded +UNAME_BIN="${UNAME_BIN:-/usr/bin/uname}" +if [[ -x "$UNAME_BIN" && "$("$UNAME_BIN" -s)" == "Darwin" ]]; then + BREW_BIN="${BREW_BIN:-$(which brew)}" + [[ ! -x "$BREW_BIN" && -x "/usr/local/bin/brew" ]] && BREW_BIN="/usr/local/bin/brew" + + if [[ -d "$("$BREW_BIN" --prefix)" ]]; then + export path=( + "$("$BREW_BIN" --prefix)/opt/coreutils/libexec/gnubin" + "$("$BREW_BIN" --prefix)/opt/findutils/libexec/gnubin" + "${path[@]}" + ) + fi +fi + +# Load dotly core for your current BASH +# PR Note about this: $SHELL sometimes see zsh under certain circumstances in macOS +CURRENT_SHELL="unknown" +if [[ -n "${BASH_VERSION:-}" ]]; then + CURRENT_SHELL="bash" +elif [[ -n "${ZSH_VERSION:-}" ]]; then + CURRENT_SHELL="zsh" +fi + +if [[ "$CURRENT_SHELL" != "unknown" && -f "$DOTLY_PATH/shell/${CURRENT_SHELL}/init.sh" ]]; then + #shellcheck source=/dev/null + . "$DOTLY_PATH/shell/${CURRENT_SHELL}/init.sh" +else + echo -e "\033[0;31m\033[1mDOTLY Could not be loaded: Initializer not found for \`${CURRENT_SHELL}\`\033[0m" +fi + +# Aliases +#shellcheck source=/dev/null +{ [[ -f "$DOTFILES_PATH/shell/aliases.sh" ]] && . "$DOTFILES_PATH/shell/aliases.sh"; } || true + +# Functions +#shellcheck source=/dev/null +{ [[ -f "$DOTFILES_PATH/shell/functions.sh" ]] && . "$DOTFILES_PATH/shell/functions.sh"; } || true + +#shellcheck source=/dev/null +[[ -f "$HOME/.cargo/env" ]] && . "$HOME/.cargo/env" + + +# Auto Init scripts at the end +init_scripts_path="$DOTFILES_PATH/shell/init-scripts.enabled" +if [[ ${DOTLY_INIT_SCRIPTS:-true} == true ]] && [[ -d "$init_scripts_path" ]]; then + find "$DOTFILES_PATH/shell/init-scripts.enabled" -mindepth 1 -maxdepth 1 -type f,l -print0 2>/dev/null | xargs -0 -I _ realpath --quiet --logical _ | while read -r init_script; do + [[ -z "$init_script" ]] && continue + #shellcheck source=/dev/null + { [[ -f "$init_script" ]] && . "$init_script"; } || echo -e "\033[0;31m$init_script could not be loaded\033[0m" + done +fi +unset init_script init_scripts_path diff --git a/shell/init-scripts/autoupdate b/shell/init-scripts/autoupdate new file mode 100755 index 000000000..ae80f3307 --- /dev/null +++ b/shell/init-scripts/autoupdate @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +"$DOTLY_PATH/bin/dot" self async-update \ No newline at end of file diff --git a/shell/zsh/init.sh b/shell/zsh/init.sh new file mode 100755 index 000000000..6bd6e73fb --- /dev/null +++ b/shell/zsh/init.sh @@ -0,0 +1,58 @@ +#shellcheck disable=SC2148 +reverse-search() { + local selected num + setopt localoptions noglobsubst noposixbuiltins pipefail HIST_FIND_NO_DUPS 2> /dev/null + + #shellcheck disable=SC2207 + selected=( $(fc -rl 1 | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" fzf) ) + local ret=$? + if [ -n "${selected[*]:-}" ]; then + num=${selected[1]} + if [ -n "$num" ]; then + zle vi-fetch-history -n $num + fi + fi + zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init + return $ret +} + +# ZSH Ops +setopt HIST_IGNORE_ALL_DUPS +setopt HIST_FCNTL_LOCK +setopt +o nomatch +# setopt autopushd + +# Start zim +#shellcheck source=/dev/null +. "$ZIM_HOME/init.zsh" + +# Async mode for autocompletion +# shellcheck disable=SC2034 +ZSH_AUTOSUGGEST_USE_ASYNC=true +# shellcheck disable=SC2034 +ZSH_HIGHLIGHT_MAXLENGTH=300 + +fpath=( + "$DOTFILES_PATH/shell/zsh/themes" + "$DOTFILES_PATH/shell/zsh/autocompletions" + "$DOTLY_PATH/shell/zsh/themes" + "$DOTLY_PATH/shell/zsh/completions" + "${fpath[@]}" +) + +# Brew ZSH Completions +if type brew &>/dev/null; then + fpath+=("$(brew --prefix)/share/zsh/site-functions") +fi + +autoload -Uz promptinit && promptinit +prompt "${DOTLY_THEME:-codely}" + +#shellcheck source=/dev/null +. "$DOTLY_PATH/shell/zsh/bindings/dot.zsh" +#shellcheck source=/dev/null +. "$DOTLY_PATH/shell/zsh/bindings/reverse_search.zsh" +#shellcheck source=/dev/null +. "$DOTFILES_PATH/shell/zsh/key-bindings.zsh" diff --git a/shell/zsh/themes/prompt_codely_setup b/shell/zsh/themes/prompt_codely_setup index 0bb0410c9..7220fd2c5 100644 --- a/shell/zsh/themes/prompt_codely_setup +++ b/shell/zsh/themes/prompt_codely_setup @@ -42,8 +42,16 @@ prompt_codely_precmd() { (( ${+functions[git-info]} )) && git-info } +prompt_dotly_autoupdate() { + if [ -f "$DOTFILES_PATH/.dotly_update_available" ]; then + print -n "📥 | " + fi +} + prompt_codely_setup() { - local prompt_codely_status="%(?:%F{green}{%F{$status_icon_color}$status_icon_ok%F{green}}:%F{red}{%F{$status_icon_color}$status_icon_ko%F{red}})" + local prompt_codely_status + + prompt_codely_status="%(?:%F{green}{%F{$status_icon_color}$status_icon_ok%F{green}}:%F{red}{%F{$status_icon_color}$status_icon_ko%F{red}})" autoload -Uz add-zsh-hook && add-zsh-hook precmd prompt_codely_precmd @@ -56,9 +64,9 @@ prompt_codely_setup() { zstyle ':zim:git-info:keys' format "prompt" " %F{cyan}%b%c %C%D" if [ "$CODELY_THEME_MINIMAL" = true ]; then - PS1="${prompt_codely_status} " + PS1="\$(prompt_dotly_autoupdate)${prompt_codely_status} " else - PS1="${prompt_codely_status} \$(prompt_codely_pwd)\$(prompt_codely_git)%f " + PS1="\$(prompt_dotly_autoupdate)${prompt_codely_status} \$(prompt_codely_pwd)\$(prompt_codely_git)%f " fi if [ "$CODELY_THEME_PROMPT_IN_NEW_LINE" = true ]; then diff --git a/symlinks/core-feature.yaml b/symlinks/core-feature.yaml new file mode 100644 index 000000000..7d66113e8 --- /dev/null +++ b/symlinks/core-feature.yaml @@ -0,0 +1,10 @@ +- defaults: + link: + create: true + force: true + +- create: + - $DOTLY_PATH/shell/init-scripts + - $DOTFILES_PATH/shell/init-scripts + - $DOTFILES_PATH/shell/init-scripts.enabled + diff --git a/symlinks/v2.0.0.yaml b/symlinks/v2.0.0.yaml new file mode 100644 index 000000000..2d05eab82 --- /dev/null +++ b/symlinks/v2.0.0.yaml @@ -0,0 +1,8 @@ +- defaults: + link: + create: true + +- create: + - $DOTLY_PATH/shell/init-scripts + - $DOTFILES_PATH/shell/init-scripts + - $DOTFILES_PATH/shell/init-scripts.enabled