diff --git a/bin/ansi b/bin/ansi new file mode 100755 index 0000000..242d955 --- /dev/null +++ b/bin/ansi @@ -0,0 +1,675 @@ +#!/usr/bin/env bash +# +# ANSI code generator +# +# ยฉ Copyright 2015 Tyler Akins +# Licensed under the MIT license with an additional non-advertising clause +# See http://github.com/fidian/ansi + +ansi::addCode() { + local N + + if [[ "$1" == *=* ]]; then + N="${1#*=}" + N="${N//,/;}" + else + N="" + fi + + OUTPUT="$OUTPUT$CSI$N$2" +} + +ansi::addColor() { + OUTPUT="$OUTPUT$CSI${1}m" + + if [ ! -z "$2" ]; then + SUFFIX="$CSI${2}m$SUFFIX" + fi +} + +ansi::colorTable() { + local FNB_LOWER FNB_UPPER PADDED + + FNB_LOWER="$(ansi::colorize 2 22 f)n$(ansi::colorize 1 22 b)" + FNB_UPPER="$(ansi::colorize 2 22 F)N$(ansi::colorize 1 22 B)" + printf 'bold %s ' "$(ansi::colorize 1 22 Sample)" + printf 'faint %s ' "$(ansi::colorize 2 22 Sample)" + printf 'italic %s\n' "$(ansi::colorize 3 23 Sample)" + printf 'underline %s ' "$(ansi::colorize 4 24 Sample)" + printf 'blink %s ' "$(ansi::colorize 5 25 Sample)" + printf 'inverse %s\n' "$(ansi::colorize 7 27 Sample)" + printf 'invisible %s\n' "$(ansi::colorize 8 28 Sample)" + printf 'strike %s ' "$(ansi::colorize 9 29 Sample)" + printf 'fraktur %s ' "$(ansi::colorize 20 23 Sample)" + printf 'double-underline%s\n' "$(ansi::colorize 21 24 Sample)" + printf 'frame %s ' "$(ansi::colorize 51 54 Sample)" + printf 'encircle %s ' "$(ansi::colorize 52 54 Sample)" + printf 'overline%s\n' "$(ansi::colorize 53 55 Sample)" + printf '\n' + printf ' black red green yellow blue magenta cyan white\n' + for BG in 40:black 41:red 42:green 43:yellow 44:blue 45:magenta 46:cyan 47:white; do + PADDED="bg-${BG:3} " + PADDED="${PADDED:0:13}" + printf '%s' "$PADDED" + for FG in 30 31 32 33 34 35 36 37; do + printf '%s%s;%sm' "$CSI" "${BG:0:2}" "${FG}" + printf '%s' "$FNB_LOWER" + printf '%s%sm' "$CSI" "$(( FG + 60 ))" + printf '%s' "$FNB_UPPER" + printf '%s0m ' "${CSI}" + done + printf '\n' + printf ' +intense ' + for FG in 30 31 32 33 34 35 36 37; do + printf '%s%s;%sm' "$CSI" "$(( ${BG:0:2} + 60 ))" "${FG}" + printf '%s' "$FNB_LOWER" + printf '%s%sm' "$CSI" "$(( FG + 60 ))" + printf '%s' "$FNB_UPPER" + printf '%s0m ' "${CSI}" + done + printf '\n' + done + printf '\n' + printf 'Legend:\n' + printf ' Normal color: f = faint, n = normal, b = bold.\n' + printf ' Intense color: F = faint, N = normal, B = bold.\n' +} + +ansi::colorize() { + printf '%s%sm%s%s%sm' "$CSI" "$1" "$3" "$CSI" "$2" +} + +ansi::isAnsiSupported() { + # Idea: CSI c + # Response = CSI ? 6 [234] ; 2 2 c + # The "22" means ANSI color + printf "can't tell yet\n" +} + +ansi::report() { + local BUFF C + + REPORT="" + printf "%s%s" "$CSI" "$1" + read -r -N ${#2} -s -t 1 BUFF + + if [ "$BUFF" != "$2" ]; then + return 1 + fi + + read -r -N ${#3} -s -t 1 BUFF + + while [ "$BUFF" != "$3" ]; do + REPORT="$REPORT${BUFF:0:1}" + read -r -N 1 -s -t 1 C || exit 1 + BUFF="${BUFF:1}$C" + done +} + +ansi::showHelp() { + cat < [shell-args...]] + Activate a desk. Extra arguments are passed onto shell. If called with + no arguments, look for a Deskfile in the current directory. If not a + recognized desk, try as a path to directory containing a Deskfile. + $PROGRAM run + Run a command within a desk's environment then exit. Think '\$SHELL -c'. + $PROGRAM edit [desk-name] + Edit (or create) a deskfile with the name specified, otherwise + edit the active deskfile. + $PROGRAM help + Show this text. + $PROGRAM version + Show version information. + +Since desk spawns a shell, to deactivate and "pop" out a desk, you +simply need to exit or otherwise end the current shell process. +_EOF +} + +cmd_init() { + if [ -d "$PREFIX" ]; then + echo "Desk dir already exists at ${PREFIX}" + exit 1 + fi + read -p "Where do you want to store your deskfiles? (default: ${PREFIX}): " \ + NEW_PREFIX + [ -z "${NEW_PREFIX}" ] && NEW_PREFIX="$PREFIX" + + if [ ! -d "${NEW_PREFIX}" ]; then + echo "${NEW_PREFIX} doesn't exist, attempting to create." + mkdir -p "$NEW_PREFIX/desks" + fi + + local SHELLTYPE=$(get_running_shell) + + case "${SHELLTYPE}" in + bash) local SHELLRC="${HOME}/.bashrc" ;; + fish) local SHELLRC="${HOME}/.config/fish/config.fish" ;; + zsh) local SHELLRC="${HOME}/.zshrc" ;; + esac + + read -p "Where's your shell rc file? (default: ${SHELLRC}): " \ + USER_SHELLRC + [ -z "${USER_SHELLRC}" ] && USER_SHELLRC="$SHELLRC" + if [ ! -f "$USER_SHELLRC" ]; then + echo "${USER_SHELLRC} doesn't exist" + exit 1 + fi + + printf "\n# Hook for desk activation\n" >> "$USER_SHELLRC" + + # Since the hook is appended to the rc file, its exit status becomes + # the exit status of `source $USER_SHELLRC` which typically + # indicates if something went wrong. If $DESK_ENV is void, `test` + # sets exit status to 1. That, however, is part of desk's normal + # operation, so we clear exit status after that. + if [ "$SHELLTYPE" == "fish" ]; then + echo "test -n \"\$DESK_ENV\"; and . \"\$DESK_ENV\"; or true" >> "$USER_SHELLRC" + else + echo "[ -n \"\$DESK_ENV\" ] && source \"\$DESK_ENV\" || true" >> "$USER_SHELLRC" + fi + + echo "Done. Start adding desks to ${NEW_PREFIX}/desks!" +} + + +cmd_go() { + # TODESK ($1) may either be + # + # - the name of a desk in $DESKS/ + # - a path to a Deskfile + # - a directory containing a Deskfile + # - empty -> `./Deskfile` + # + local TODESK="$1" + local DESKEXT=$(get_deskfile_extension) + local DESKPATH="$(find "${DESKS}/" -name "${TODESK}${DESKEXT}" 2>/dev/null)" + + local POSSIBLE_DESKFILE_DIR="${TODESK%$DESKFILE_NAME}" + if [ -z "$POSSIBLE_DESKFILE_DIR" ]; then + POSSIBLE_DESKFILE_DIR="." + fi + + # If nothing could be found in $DESKS/, check to see if this is a path to + # a Deskfile. + if [[ -z "$DESKPATH" && -d "$POSSIBLE_DESKFILE_DIR" ]]; then + if [ ! -f "${POSSIBLE_DESKFILE_DIR}/${DESKFILE_NAME}" ]; then + echo "No Deskfile found in '${POSSIBLE_DESKFILE_DIR}'" + exit 1 + fi + + local REALPATH=$( cd $POSSIBLE_DESKFILE_DIR && pwd ) + DESKPATH="${REALPATH}/${DESKFILE_NAME}" + TODESK=$(basename "$REALPATH") + fi + + # Shift desk name so we can forward on all arguments to the shell. + shift; + + if [ -z "$DESKPATH" ]; then + echo "Desk $TODESK (${TODESK}${DESKEXT}) not found in $DESKS" + exit 1 + else + local SHELL_EXEC="$(get_running_shell)" + DESK_NAME="${TODESK}" DESK_ENV="${DESKPATH}" exec "${SHELL_EXEC}" "$@" + fi +} + + +cmd_run() { + local TODESK="$1" + shift; + cmd_go "$TODESK" -ic "$@" +} + + +# Usage: desk list [options] +# Description: List all desks along with a description +# --only-names: List only the names of the desks +# --no-format: Use ' - ' to separate names from descriptions +cmd_list() { + if [ ! -d "${DESKS}/" ]; then + echo "No desk dir! Run 'desk init'." + exit 1 + fi + local SHOW_DESCRIPTIONS DESKEXT AUTO_ALIGN name desc len longest out + + while [[ $# -gt 0 ]]; do + case "$1" in + --only-names) SHOW_DESCRIPTIONS=false && AUTO_ALIGN=false ;; + --no-format) AUTO_ALIGN=false ;; + esac + shift + done + + DESKEXT=$(get_deskfile_extension) + out="" + longest=0 + + while read -d '' -r f; do + name=$(basename "${f/${DESKEXT}//}") + if [[ "$SHOW_DESCRIPTIONS" = false ]]; then + out+="$name"$'\n' + else + desc=$(echo_description "$f") + out+="$name - $desc"$'\n' + len=${#name} + (( len > longest )) && longest=$len + fi + done < <(find "${DESKS}/" -name "*${DESKEXT}" -print0) + if [[ "$AUTO_ALIGN" != false ]]; then + print_aligned "$out" "$longest" + else + printf "%s" "$out" + fi +} + +# Usage: desk [options] +# Description: List the current desk and any associated aliases. If no desk is being used, display available desks +# --no-format: Use ' - ' to separate alias/export/function names from their descriptions +cmd_current() { + if [ -z "$DESK_ENV" ]; then + printf "No desk activated.\n\n%s" "$(cmd_list)" + exit 1 + fi + + local DESKPATH=$DESK_ENV + local CALLABLES=$(get_callables "$DESKPATH") + local AUTO_ALIGN len longest out + + while [[ $# -gt 0 ]]; do + case "$1" in + --no-format) AUTO_ALIGN=false ;; + esac + shift + done + printf "%s - %s\n" "$DESK_NAME" "$(echo_description "$DESKPATH")" + + if [[ -n "$CALLABLES" ]]; then + + longest=0 + out=$'\n' + for NAME in $CALLABLES; do + # Last clause in the grep regexp accounts for fish functions. + len=$((${#NAME} + 2)) + (( len > longest )) && longest=$len + local DOCLINE=$( + grep -B 1 -E \ + "^(alias ${NAME}=|export ${NAME}=|(function )?${NAME}( )?\()|function $NAME" "$DESKPATH" \ + | grep "#") + + if [ -z "$DOCLINE" ]; then + out+=" ${NAME}"$'\n' + else + out+=" ${NAME} - ${DOCLINE##\# }"$'\n' + fi + done + + if [[ "$AUTO_ALIGN" != false ]]; then + print_aligned "$out" "$longest" + else + printf "%s" "$out" + fi + fi +} + +cmd_edit() { + if [ $# -eq 0 ]; then + if [ "$DESK_NAME" == "" ]; then + echo "No desk activated." + exit 3 + fi + local DESKNAME_TO_EDIT="$DESK_NAME" + elif [ $# -eq 1 ]; then + local DESKNAME_TO_EDIT="$1" + else + echo "Usage: ${PROGRAM} edit [desk-name]" + exit 1 + fi + + local DESKEXT=$(get_deskfile_extension) + local EDIT_PATH="${DESKS}/${DESKNAME_TO_EDIT}${DESKEXT}" + + ${EDITOR:-vi} "$EDIT_PATH" +} + +## Utilities + +FNAME_CHARS='[a-zA-Z0-9_-]' + +# Echo the description of a desk. $1 is the deskfile. +echo_description() { + local descline=$(grep -E "#\s+Description" "$1") + echo "${descline##*Description: }" +} + +# Echo a list of aliases, exports, and functions for a given desk +get_callables() { + local DESKPATH=$1 + grep -E "^(alias |export |(function )?${FNAME_CHARS}+ ?\()|function $NAME" "$DESKPATH" \ + | sed 's/alias \([^= ]*\)=.*/\1/' \ + | sed 's/export \([^= ]*\)=.*/\1/' \ + | sed -E "s/(function )?(${FNAME_CHARS}+) ?\(\).*/\2/" \ + | sed -E "s/function (${FNAME_CHARS}+).*/\1/" +} + +get_running_shell() { + # Echo the name of the parent shell via procfs, if we have it available. + # Otherwise, try to use ps with bash's parent pid. + if [ -e /proc ]; then + # Get cmdline procfile of the process running this script. + local CMDLINE_FILE="/proc/$(grep PPid /proc/$$/status | cut -f2)/cmdline" + + if [ -f "$CMDLINE_FILE" ]; then + # Strip out any verion that may be attached to the shell executable. + # Strip leading dash for login shells. + local CMDLINE_SHELL=$(sed -r -e 's/\x0.*//' -e 's/^-//' "$CMDLINE_FILE") + fi + else + # Strip leading dash for login shells. + # If the parent process is a login shell, guess bash. + local CMDLINE_SHELL=$(ps -o args -p $PPID | tail -1 | sed -e 's/login/bash/' -e 's/^-//') + fi + + if [ ! -z "$CMDLINE_SHELL" ]; then + basename "$CMDLINE_SHELL" + exit + fi + + # Fall back to $SHELL otherwise. + basename "$SHELL" + exit +} + +# Echo the extension of recognized deskfiles (.fish for fish) +get_deskfile_extension() { + if [ "$(get_running_shell)" == "fish" ]; then + echo '.fish' + else + echo '.sh' + fi +} + +# Echo first argument as key/value pairs aligned into two columns; second argument is the longest key +print_aligned() { + local out=$1 longest=$2 + printf "%s" "$out" | awk -v padding="$longest" -F' - ' '{ + printf "%-*s %s\n", padding, $1, substr($0, index($0, " - ")+2, length($0)) + }' +} + + +PROGRAM="${0##*/}" + +case "$1" in + init) shift; cmd_init "$@" ;; + help|--help) shift; cmd_usage "$@" ;; + version|--version) shift; cmd_version "$@" ;; + ls|list) shift; cmd_list "$@" ;; + go|.) shift; cmd_go "$@" ;; + run) shift; cmd_run "$@" ;; + edit) shift; cmd_edit "$@" ;; + *) cmd_current "$@" ;; +esac +exit 0 + diff --git a/bin/is b/bin/is new file mode 100755 index 0000000..8ddd6a5 --- /dev/null +++ b/bin/is @@ -0,0 +1,128 @@ +#!/bin/bash +# +# Copyright (c) 2016 Jรณzef Sokoล‚owski +# Distributed under the MIT License +# +# For most current version checkout repository: +# https://github.com/qzb/is.sh +# + +is() { + if [ "$1" == "--help" ]; then + cat << EOF +Conditions: + is equal VALUE_A VALUE_B + is matching REGEXP VALUE + is substring VALUE_A VALUE_B + is empty VALUE + is number VALUE + is gt NUMBER_A NUMBER_B + is lt NUMBER_A NUMBER_B + is ge NUMBER_A NUMBER_B + is le NUMBER_A NUMBER_B + is file PATH + is dir PATH + is link PATH + is existing PATH + is readable PATH + is writeable PATH + is executable PATH + is available COMMAND + is older PATH_A PATH_B + is newer PATH_A PATH_B + is true VALUE + is false VALUE + +Negation: + is not equal VALUE_A VALUE_B +EOF + exit + fi + + if [ "$1" == "--version" ]; then + echo "is.sh 1.1.0" + exit + fi + + local condition="$1" + local value_a="$2" + local value_b="$3" + + if [ "$condition" == "not" ]; then + shift 1 + ! is "${@}" + return $? + fi + + if [ "$condition" == "a" ] || [ "$condition" == "an" ] || [ "$condition" == "the" ]; then + shift 1 + is "${@}" + return $? + fi + + case "$condition" in + file) + [ -f "$value_a" ]; return $?;; + dir|directory) + [ -d "$value_a" ]; return $?;; + link|symlink) + [ -L "$value_a" ]; return $?;; + existent|existing|exist|exists) + [ -e "$value_a" ]; return $?;; + readable) + [ -r "$value_a" ]; return $?;; + writeable) + [ -w "$value_a" ]; return $?;; + executable) + [ -x "$value_a" ]; return $?;; + available|installed) + which "$value_a"; return $?;; + empty) + [ -z "$value_a" ]; return $?;; + number) + echo "$value_a" | grep -E '^[0-9]+(\.[0-9]+)?$'; return $?;; + older) + [ "$value_a" -ot "$value_b" ]; return $?;; + newer) + [ "$value_a" -nt "$value_b" ]; return $?;; + gt) + is not a number "$value_a" && return 1; + is not a number "$value_b" && return 1; + awk "BEGIN {exit $value_a > $value_b ? 0 : 1}"; return $?;; + lt) + is not a number "$value_a" && return 1; + is not a number "$value_b" && return 1; + awk "BEGIN {exit $value_a < $value_b ? 0 : 1}"; return $?;; + ge) + is not a number "$value_a" && return 1; + is not a number "$value_b" && return 1; + awk "BEGIN {exit $value_a >= $value_b ? 0 : 1}"; return $?;; + le) + is not a number "$value_a" && return 1; + is not a number "$value_b" && return 1; + awk "BEGIN {exit $value_a <= $value_b ? 0 : 1}"; return $?;; + eq|equal) + [ "$value_a" = "$value_b" ] && return 0; + is not a number "$value_a" && return 1; + is not a number "$value_b" && return 1; + awk "BEGIN {exit $value_a == $value_b ? 0 : 1}"; return $?;; + match|matching) + echo "$value_b" | grep -xE "$value_a"; return $?;; + substr|substring) + echo "$value_b" | grep -F "$value_a"; return $?;; + true) + [ "$value_a" == true ] || [ "$value_a" == 0 ]; return $?;; + false) + [ "$value_a" != true ] && [ "$value_a" != 0 ]; return $?;; + esac > /dev/null + + return 1 +} + +if is not equal "${BASH_SOURCE[0]}" "$0"; then + export -f is +else + is "${@}" + exit $? +fi + diff --git a/bin/lj b/bin/lj new file mode 100755 index 0000000..5da2112 --- /dev/null +++ b/bin/lj @@ -0,0 +1,156 @@ +#!/usr/bin/env zsh + +# Create a mapping of log levels to their names +typeset -A _log_levels +_log_levels=( + 'emergency' 0 + 'alert' 1 + 'critical' 2 + 'error' 3 + 'warning' 4 + 'notice' 5 + 'info' 6 + 'debug' 7 +) + +### +# Output usage information and exit +### +function _lumberjack_usage() { + echo "\033[0;33mUsage:\033[0;m" + echo " lj [options] [] " + echo + echo "\033[0;33mOptions:\033[0;m" + echo " -h, --help Output help text and exit" + echo " -v, --version Output version information and exit" + echo " -f, --file Set the logfile and exit" + echo " -l, --level Set the log level and exit" + echo + echo "\033[0;33mLevels:\033[0;m" + echo " emergency" + echo " alert" + echo " critical" + echo " error" + echo " warning" + echo " notice" + echo " info" + echo " debug" +} + +### +# Output the message to the logfile +### +function _lumberjack_message() { + local level="$1" file="$2" logtype="$3" msg="${(@)@:4}" + + # If the file string is empty, output an error message + if [[ -z $file ]]; then + echo "\033[0;31mNo logfile has been set for this process. Use \`lumberjack --file /path/to/file\` to set it\033[0;m" + exit 1 + fi + + # If the level is not set, assume 5 (notice) + if [[ -z $level ]]; then + level=5 + fi + + case $logtype in + # If a valid logtype is passed + emergency|alert|critical|error|warning|notice|info|debug ) + # We do nothing here + ;; + # In all other cases + * ) + # Second argument was not a log level, so manually set it to notice + # and include the first parameter in the message + logtype='notice' + msg="${(@)@:3}" + ;; + esac + + if [[ $_log_levels[$logtype] > $level ]]; then + # The message being recorded is for a higher log level than the one + # currently being recorded, so gracefully exit + exit 0 + fi + + # Output the message to the logfile + echo "[$(echo $logtype | tr '[a-z]' '[A-Z]')] [$(date '+%Y-%m-%d %H:%M:%S')] $msg" >> $file +} + +### +# The main lumberjack process +### +function _lumberjack() { + local help version logfile loglevel dir statefile state + + # Create the state directory if it doesn't exist + dir="${ZDOTDIR:-$HOME}/.lumberjack" + if [[ ! -d $dir ]]; then + mkdir -p $dir + fi + + # If a statefile already exists, load the level and file + statefile="$dir/$PPID" + if [[ -f $statefile ]]; then + state=$(cat $statefile) + level="$state[1]" + file="${(@)state:2}" + fi + + # Parse CLI options + zparseopts -D h=help -help=help \ + v=version -version=version \ + f:=logfile -file:=logfile \ + l:=loglevel -level:=loglevel + + # If the help option is passed, output usage information and exit + if [[ -n $help ]]; then + _lumberjack_usage + exit 0 + fi + + # If the version option is passed, output the version and exit + if [[ -n $version ]]; then + echo "0.1.1" + exit 0 + fi + + # If the logfile option is passed, set the current logfile + # for the parent process ID + if [[ -n $logfile ]]; then + shift logfile + file=$logfile + + # Create the log file if it doesn't exist + if [[ ! -f $file ]]; then + touch $file + fi + fi + + # If the loglevel option is passed, set the current loglevel + # for the parent process ID + if [[ -n $loglevel ]]; then + shift loglevel + level=$_log_levels[$loglevel] + fi + + if [[ -z $level ]]; then + level=5 + fi + + # Check if we're setting options rather than logging + if [[ -n $logfile || -n $loglevel ]]; then + # Store the state + echo "$level $file" >! $statefile + + # Exit gracefully + exit 0 + fi + + # Log the message + _lumberjack_message "$level" "$file" "$@" +} + +_lumberjack "$@" + diff --git a/bin/mo b/bin/mo new file mode 100755 index 0000000..3fd65c2 --- /dev/null +++ b/bin/mo @@ -0,0 +1,986 @@ +#!/usr/bin/env bash +# +#/ Mo is a mustache template rendering software written in bash. It inserts +#/ environment variables into templates. +#/ +#/ Simply put, mo will change {{VARIABLE}} into the value of that +#/ environment variable. You can use {{#VARIABLE}}content{{/VARIABLE}} to +#/ conditionally display content or iterate over the values of an array. +#/ +#/ Learn more about mustache templates at https://mustache.github.io/ +#/ +#/ Simple usage: +#/ +#/ mo [--false] [--help] [--source=FILE] filenames... +#/ +#/ --fail-not-set - Fail upon expansion of an unset variable. +#/ --false - Treat the string "false" as empty for conditionals. +#/ --help - This message. +#/ --source=FILE - Load FILE into the environment before processing templates. +# +# Mo is under a MIT style licence with an additional non-advertising clause. +# See LICENSE.md for the full text. +# +# This is open source! Please feel free to contribute. +# +# https://github.com/tests-always-included/mo + + +# Public: Template parser function. Writes templates to stdout. +# +# $0 - Name of the mo file, used for getting the help message. +# --fail-not-set - Fail upon expansion of an unset variable. Default behavior +# is to silently ignore and expand into empty string. +# --false - Treat "false" as an empty value. You may set the +# MO_FALSE_IS_EMPTY environment variable instead to a non-empty +# value to enable this behavior. +# --help - Display a help message. +# --source=FILE - Source a file into the environment before processint +# template files. +# -- - Used to indicate the end of options. You may optionally +# use this when filenames may start with two hyphens. +# $@ - Filenames to parse. +# +# Mo uses the following environment variables: +# +# MO_FAIL_ON_UNSET - When set to a non-empty value, expansion of an unset +# env variable will be aborted with an error. +# MO_FALSE_IS_EMPTY - When set to a non-empty value, the string "false" +# will be treated as an empty value for the purposes +# of conditionals. +# MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate +# a help message. +# +# Returns nothing. +mo() ( + # This function executes in a subshell so IFS is reset. + # Namespace this variable so we don't conflict with desired values. + local moContent f2source files doubleHyphens + + IFS=$' \n\t' + files=() + doubleHyphens=false + + if [[ $# -gt 0 ]]; then + for arg in "$@"; do + if $doubleHyphens; then + # After we encounter two hyphens together, all the rest + # of the arguments are files. + files=("${files[@]}" "$arg") + else + case "$arg" in + -h|--h|--he|--hel|--help|-\?) + moUsage "$0" + exit 0 + ;; + + --fail-not-set) + # shellcheck disable=SC2030 + MO_FAIL_ON_UNSET=true + ;; + + --false) + # shellcheck disable=SC2030 + MO_FALSE_IS_EMPTY=true + ;; + + --source=*) + f2source="${arg#--source=}" + + if [[ -f "$f2source" ]]; then + # shellcheck disable=SC1090 + . "$f2source" + else + echo "No such file: $f2source" >&2 + exit 1 + fi + ;; + + --) + # Set a flag indicating we've encountered double hyphens + doubleHyphens=true + ;; + + *) + # Every arg that is not a flag or a option should be a file + files=(${files[@]+"${files[@]}"} "$arg") + ;; + esac + fi + done + fi + + moGetContent moContent "${files[@]}" || return 1 + moParse "$moContent" "" true +) + + +# Internal: Scan content until the right end tag is found. Creates an array +# with the following members: +# +# [0] = Content before end tag +# [1] = End tag (complete tag) +# [2] = Content after end tag +# +# Everything using this function uses the "standalone tags" logic. +# +# $1 - Name of variable for the array +# $2 - Content +# $3 - Name of end tag +# $4 - If -z, do standalone tag processing before finishing +# +# Returns nothing. +moFindEndTag() { + local content remaining scanned standaloneBytes tag + + #: Find open tags + scanned="" + moSplit content "$2" '{{' '}}' + + while [[ "${#content[@]}" -gt 1 ]]; do + moTrimWhitespace tag "${content[1]}" + + #: Restore content[1] before we start using it + content[1]='{{'"${content[1]}"'}}' + + case $tag in + '#'* | '^'*) + #: Start another block + scanned="${scanned}${content[0]}${content[1]}" + moTrimWhitespace tag "${tag:1}" + moFindEndTag content "${content[2]}" "$tag" "loop" + scanned="${scanned}${content[0]}${content[1]}" + remaining=${content[2]} + ;; + + '/'*) + #: End a block - could be ours + moTrimWhitespace tag "${tag:1}" + scanned="$scanned${content[0]}" + + if [[ "$tag" == "$3" ]]; then + #: Found our end tag + if [[ -z "${4-}" ]] && moIsStandalone standaloneBytes "$scanned" "${content[2]}" true; then + #: This is also a standalone tag - clean up whitespace + #: and move those whitespace bytes to the "tag" element + standaloneBytes=( $standaloneBytes ) + content[1]="${scanned:${standaloneBytes[0]}}${content[1]}${content[2]:0:${standaloneBytes[1]}}" + scanned="${scanned:0:${standaloneBytes[0]}}" + content[2]="${content[2]:${standaloneBytes[1]}}" + fi + + local "$1" && moIndirectArray "$1" "$scanned" "${content[1]}" "${content[2]}" + return 0 + fi + + scanned="$scanned${content[1]}" + remaining=${content[2]} + ;; + + *) + #: Ignore all other tags + scanned="${scanned}${content[0]}${content[1]}" + remaining=${content[2]} + ;; + esac + + moSplit content "$remaining" '{{' '}}' + done + + #: Did not find our closing tag + scanned="$scanned${content[0]}" + local "$1" && moIndirectArray "$1" "${scanned}" "" "" +} + + +# Internal: Find the first index of a substring. If not found, sets the +# index to -1. +# +# $1 - Destination variable for the index +# $2 - Haystack +# $3 - Needle +# +# Returns nothing. +moFindString() { + local pos string + + string=${2%%$3*} + [[ "$string" == "$2" ]] && pos=-1 || pos=${#string} + local "$1" && moIndirect "$1" "$pos" +} + + +# Internal: Generate a dotted name based on current context and target name. +# +# $1 - Target variable to store results +# $2 - Context name +# $3 - Desired variable name +# +# Returns nothing. +moFullTagName() { + if [[ -z "${2-}" ]] || [[ "$2" == *.* ]]; then + local "$1" && moIndirect "$1" "$3" + else + local "$1" && moIndirect "$1" "${2}.${3}" + fi +} + + +# Internal: Fetches the content to parse into a variable. Can be a list of +# partials for files or the content from stdin. +# +# $1 - Variable name to assign this content back as +# $2-@ - File names (optional) +# +# Returns nothing. +moGetContent() { + local content filename target + + target=$1 + shift + if [[ "${#@}" -gt 0 ]]; then + content="" + + for filename in "$@"; do + #: This is so relative paths work from inside template files + content="$content"'{{>'"$filename"'}}' + done + else + moLoadFile content /dev/stdin || return 1 + fi + + local "$target" && moIndirect "$target" "$content" +} + + +# Internal: Indent a string, placing the indent at the beginning of every +# line that has any content. +# +# $1 - Name of destination variable to get an array of lines +# $2 - The indent string +# $3 - The string to reindent +# +# Returns nothing. +moIndentLines() { + local content fragment len posN posR result trimmed + + result="" + + #: Remove the period from the end of the string. + len=$((${#3} - 1)) + content=${3:0:$len} + + if [[ -z "${2-}" ]]; then + local "$1" && moIndirect "$1" "$content" + + return 0 + fi + + moFindString posN "$content" $'\n' + moFindString posR "$content" $'\r' + + while [[ "$posN" -gt -1 ]] || [[ "$posR" -gt -1 ]]; do + if [[ "$posN" -gt -1 ]]; then + fragment="${content:0:$posN + 1}" + content=${content:$posN + 1} + else + fragment="${content:0:$posR + 1}" + content=${content:$posR + 1} + fi + + moTrimChars trimmed "$fragment" false true " " $'\t' $'\n' $'\r' + + if [[ -n "$trimmed" ]]; then + fragment="$2$fragment" + fi + + result="$result$fragment" + + moFindString posN "$content" $'\n' + moFindString posR "$content" $'\r' + + # If the content ends in a newline, do not indent. + if [[ "$posN" -eq ${#content} ]]; then + # Special clause for \r\n + if [[ "$posR" -eq "$((posN - 1))" ]]; then + posR=-1 + fi + + posN=-1 + fi + + if [[ "$posR" -eq ${#content} ]]; then + posR=-1 + fi + done + + moTrimChars trimmed "$content" false true " " $'\t' + + if [[ -n "$trimmed" ]]; then + content="$2$content" + fi + + result="$result$content" + + local "$1" && moIndirect "$1" "$result" +} + + +# Internal: Send a variable up to the parent of the caller of this function. +# +# $1 - Variable name +# $2 - Value +# +# Examples +# +# callFunc () { +# local "$1" && moIndirect "$1" "the value" +# } +# callFunc dest +# echo "$dest" # writes "the value" +# +# Returns nothing. +moIndirect() { + unset -v "$1" + printf -v "$1" '%s' "$2" +} + + +# Internal: Send an array as a variable up to caller of a function +# +# $1 - Variable name +# $2-@ - Array elements +# +# Examples +# +# callFunc () { +# local myArray=(one two three) +# local "$1" && moIndirectArray "$1" "${myArray[@]}" +# } +# callFunc dest +# echo "${dest[@]}" # writes "one two three" +# +# Returns nothing. +moIndirectArray() { + unset -v "$1" + + # IFS must be set to a string containing space or unset in order for + # the array slicing to work regardless of the current IFS setting on + # bash 3. This is detailed further at + # https://github.com/fidian/gg-core/pull/7 + eval "$(printf "IFS= %s=(\"\${@:2}\") IFS=%q" "$1" "$IFS")" +} + + +# Internal: Determine if a given environment variable exists and if it is +# an array. +# +# $1 - Name of environment variable +# +# Be extremely careful. Even if strict mode is enabled, it is not honored +# in newer versions of Bash. Any errors that crop up here will not be +# caught automatically. +# +# Examples +# +# var=(abc) +# if moIsArray var; then +# echo "This is an array" +# echo "Make sure you don't accidentally use \$var" +# fi +# +# Returns 0 if the name is not empty, 1 otherwise. +moIsArray() { + # Namespace this variable so we don't conflict with what we're testing. + local moTestResult + + moTestResult=$(declare -p "$1" 2>/dev/null) || return 1 + [[ "${moTestResult:0:10}" == "declare -a" ]] && return 0 + [[ "${moTestResult:0:10}" == "declare -A" ]] && return 0 + + return 1 +} + + +# Internal: Determine if the given name is a defined function. +# +# $1 - Function name to check +# +# Be extremely careful. Even if strict mode is enabled, it is not honored +# in newer versions of Bash. Any errors that crop up here will not be +# caught automatically. +# +# Examples +# +# moo () { +# echo "This is a function" +# } +# if moIsFunction moo; then +# echo "moo is a defined function" +# fi +# +# Returns 0 if the name is a function, 1 otherwise. +moIsFunction() { + local functionList functionName + + functionList=$(declare -F) + functionList=( ${functionList//declare -f /} ) + + for functionName in "${functionList[@]}"; do + if [[ "$functionName" == "$1" ]]; then + return 0 + fi + done + + return 1 +} + + +# Internal: Determine if the tag is a standalone tag based on whitespace +# before and after the tag. +# +# Passes back a string containing two numbers in the format "BEFORE AFTER" +# like "27 10". It indicates the number of bytes remaining in the "before" +# string (27) and the number of bytes to trim in the "after" string (10). +# Useful for string manipulation: +# +# $1 - Variable to set for passing data back +# $2 - Content before the tag +# $3 - Content after the tag +# $4 - true/false: is this the beginning of the content? +# +# Examples +# +# moIsStandalone RESULT "$before" "$after" false || return 0 +# RESULT_ARRAY=( $RESULT ) +# echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}" +# +# Returns nothing. +moIsStandalone() { + local afterTrimmed beforeTrimmed char + + moTrimChars beforeTrimmed "$2" false true " " $'\t' + moTrimChars afterTrimmed "$3" true false " " $'\t' + char=$((${#beforeTrimmed} - 1)) + char=${beforeTrimmed:$char} + + # If the content before didn't end in a newline + if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]]; then + # and there was content or this didn't start the file + if [[ -n "$char" ]] || ! $4; then + # then this is not a standalone tag. + return 1 + fi + fi + + char=${afterTrimmed:0:1} + + # If the content after doesn't start with a newline and it is something + if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]] && [[ -n "$char" ]]; then + # then this is not a standalone tag. + return 2 + fi + + if [[ "$char" == $'\r' ]] && [[ "${afterTrimmed:1:1}" == $'\n' ]]; then + char="$char"$'\n' + fi + + local "$1" && moIndirect "$1" "$((${#beforeTrimmed})) $((${#3} + ${#char} - ${#afterTrimmed}))" +} + + +# Internal: Join / implode an array +# +# $1 - Variable name to receive the joined content +# $2 - Joiner +# $3-$* - Elements to join +# +# Returns nothing. +moJoin() { + local joiner part result target + + target=$1 + joiner=$2 + result=$3 + shift 3 + + for part in "$@"; do + result="$result$joiner$part" + done + + local "$target" && moIndirect "$target" "$result" +} + + +# Internal: Read a file into a variable. +# +# $1 - Variable name to receive the file's content +# $2 - Filename to load +# +# Returns nothing. +moLoadFile() { + local content len + + # The subshell removes any trailing newlines. We forcibly add + # a dot to the content to preserve all newlines. + # As a future optimization, it would be worth considering removing + # cat and replacing this with a read loop. + + content=$(cat -- "$2" && echo '.') || return 1 + len=$((${#content} - 1)) + content=${content:0:$len} # Remove last dot + + local "$1" && moIndirect "$1" "$content" +} + + +# Internal: Process a chunk of content some number of times. Writes output +# to stdout. +# +# $1 - Content to parse repeatedly +# $2 - Tag prefix (context name) +# $3-@ - Names to insert into the parsed content +# +# Returns nothing. +moLoop() { + local content context contextBase + + content=$1 + contextBase=$2 + shift 2 + + while [[ "${#@}" -gt 0 ]]; do + moFullTagName context "$contextBase" "$1" + moParse "$content" "$context" false + shift + done +} + + +# Internal: Parse a block of text, writing the result to stdout. +# +# $1 - Block of text to change +# $2 - Current name (the variable NAME for what {{.}} means) +# $3 - true when no content before this, false otherwise +# +# Returns nothing. +moParse() { + # Keep naming variables mo* here to not overwrite needed variables + # used in the string replacements + local moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag + + moCurrent=$2 + moIsBeginning=$3 + + # Find open tags + moSplit moContent "$1" '{{' '}}' + + while [[ "${#moContent[@]}" -gt 1 ]]; do + moTrimWhitespace moTag "${moContent[1]}" + moNextIsBeginning=false + + case $moTag in + '#'*) + # Loop, if/then, or pass content through function + # Sets context + moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" + moTrimWhitespace moTag "${moTag:1}" + moFindEndTag moBlock "$moContent" "$moTag" + moFullTagName moTag "$moCurrent" "$moTag" + + if moTest "$moTag"; then + # Show / loop / pass through function + if moIsFunction "$moTag"; then + #: Consider piping the output to moGetContent + #: so the lambda does not execute in a subshell? + moContent=$($moTag "${moBlock[0]}") + moParse "$moContent" "$moCurrent" false + moContent="${moBlock[2]}" + elif moIsArray "$moTag"; then + eval "moLoop \"\${moBlock[0]}\" \"$moTag\" \"\${!${moTag}[@]}\"" + else + moParse "${moBlock[0]}" "$moCurrent" false + fi + fi + + moContent="${moBlock[2]}" + ;; + + '>'*) + # Load partial - get name of file relative to cwd + moPartial moContent "${moContent[@]}" "$moIsBeginning" "$moCurrent" + moNextIsBeginning=${moContent[1]} + moContent=${moContent[0]} + ;; + + '/'*) + # Closing tag - If hit in this loop, we simply ignore + # Matching tags are found in moFindEndTag + moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" + ;; + + '^'*) + # Display section if named thing does not exist + moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" + moTrimWhitespace moTag "${moTag:1}" + moFindEndTag moBlock "$moContent" "$moTag" + moFullTagName moTag "$moCurrent" "$moTag" + + if ! moTest "$moTag"; then + moParse "${moBlock[0]}" "$moCurrent" false "$moCurrent" + fi + + moContent="${moBlock[2]}" + ;; + + '!'*) + # Comment - ignore the tag content entirely + # Trim spaces/tabs before the comment + moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" + ;; + + .) + # Current content (environment variable or function) + moStandaloneDenied moContent "${moContent[@]}" + moShow "$moCurrent" "$moCurrent" + ;; + + '=') + # Change delimiters + # Any two non-whitespace sequences separated by whitespace. + # This tag is ignored. + moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning" + ;; + + '{'*) + # Unescaped - split on }}} not }} + moStandaloneDenied moContent "${moContent[@]}" + moContent="${moTag:1}"'}}'"$moContent" + moSplit moContent "$moContent" '}}}' + moTrimWhitespace moTag "${moContent[0]}" + moFullTagName moTag "$moCurrent" "$moTag" + moContent=${moContent[1]} + + # Now show the value + moShow "$moTag" "$moCurrent" + ;; + + '&'*) + # Unescaped + moStandaloneDenied moContent "${moContent[@]}" + moTrimWhitespace moTag "${moTag:1}" + moFullTagName moTag "$moCurrent" "$moTag" + moShow "$moTag" "$moCurrent" + ;; + + *) + # Normal environment variable or function call + moStandaloneDenied moContent "${moContent[@]}" + moFullTagName moTag "$moCurrent" "$moTag" + moShow "$moTag" "$moCurrent" + ;; + esac + + moIsBeginning=$moNextIsBeginning + moSplit moContent "$moContent" '{{' '}}' + done + + echo -n "${moContent[0]}" +} + + +# Internal: Process a partial. +# +# Indentation should be applied to the entire partial. +# +# This sends back the "is beginning" flag because the newline after a +# standalone partial is consumed. That newline is very important in the middle +# of content. We send back this flag to reset the processing loop's +# `moIsBeginning` variable, so the software thinks we are back at the +# beginning of a file and standalone processing continues to work. +# +# Prefix all variables. +# +# $1 - Name of destination variable. Element [0] is the content, [1] is the +# true/false flag indicating if we are at the beginning of content. +# $2 - Content before the tag that was not yet written +# $3 - Tag content +# $4 - Content after the tag +# $5 - true/false: is this the beginning of the content? +# $6 - Current context name +# +# Returns nothing. +moPartial() { + # Namespace variables here to prevent conflicts. + local moContent moFilename moIndent moIsBeginning moPartial moStandalone moUnindented + + if moIsStandalone moStandalone "$2" "$4" "$5"; then + moStandalone=( $moStandalone ) + echo -n "${2:0:${moStandalone[0]}}" + moIndent=${2:${moStandalone[0]}} + moContent=${4:${moStandalone[1]}} + moIsBeginning=true + else + moIndent="" + echo -n "$2" + moContent=$4 + moIsBeginning=$5 + fi + + moTrimWhitespace moFilename "${3:1}" + + # Execute in subshell to preserve current cwd and environment + ( + # It would be nice to remove `dirname` and use a function instead, + # but that's difficult when you're only given filenames. + cd "$(dirname -- "$moFilename")" || exit 1 + moUnindented="$( + moLoadFile moPartial "${moFilename##*/}" || exit 1 + moParse "${moPartial}" "$6" true + + # Fix bash handling of subshells and keep trailing whitespace. + # This is removed in moIndentLines. + echo -n "." + )" || exit 1 + moIndentLines moPartial "$moIndent" "$moUnindented" + echo -n "$moPartial" + ) || exit 1 + + # If this is a standalone tag, the trailing newline after the tag is + # removed and the contents of the partial are added, which typically + # contain a newline. We need to send a signal back to the processing + # loop that the moIsBeginning flag needs to be turned on again. + # + # [0] is the content, [1] is that flag. + local "$1" && moIndirectArray "$1" "$moContent" "$moIsBeginning" +} + + +# Internal: Show an environment variable or the output of a function to +# stdout. +# +# Limit/prefix any variables used. +# +# $1 - Name of environment variable or function +# $2 - Current context +# +# Returns nothing. +moShow() { + # Namespace these variables + local moJoined moNameParts + + if moIsFunction "$1"; then + CONTENT=$($1 "") + moParse "$CONTENT" "$2" false + return 0 + fi + + moSplit moNameParts "$1" "." + + if [[ -z "${moNameParts[1]}" ]]; then + if moIsArray "$1"; then + eval moJoin moJoined "," "\${$1[@]}" + echo -n "$moJoined" + else + # shellcheck disable=SC2031 + if [[ -z "$MO_FAIL_ON_UNSET" ]] || moTestVarSet "$1"; then + echo -n "${!1}" + else + echo "Env variable not set: $1" >&2 + exit 1 + fi + fi + else + # Further subindexes are disallowed + eval "echo -n \"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\"" + fi +} + + +# Internal: Split a larger string into an array. +# +# $1 - Destination variable +# $2 - String to split +# $3 - Starting delimiter +# $4 - Ending delimiter (optional) +# +# Returns nothing. +moSplit() { + local pos result + + result=( "$2" ) + moFindString pos "${result[0]}" "$3" + + if [[ "$pos" -ne -1 ]]; then + # The first delimiter was found + result[1]=${result[0]:$pos + ${#3}} + result[0]=${result[0]:0:$pos} + + if [[ -n "${4-}" ]]; then + moFindString pos "${result[1]}" "$4" + + if [[ "$pos" -ne -1 ]]; then + # The second delimiter was found + result[2]="${result[1]:$pos + ${#4}}" + result[1]="${result[1]:0:$pos}" + fi + fi + fi + + local "$1" && moIndirectArray "$1" "${result[@]}" +} + + +# Internal: Handle the content for a standalone tag. This means removing +# whitespace (not newlines) before a tag and whitespace and a newline after +# a tag. That is, assuming, that the line is otherwise empty. +# +# $1 - Name of destination "content" variable. +# $2 - Content before the tag that was not yet written +# $3 - Tag content (not used) +# $4 - Content after the tag +# $5 - true/false: is this the beginning of the content? +# +# Returns nothing. +moStandaloneAllowed() { + local bytes + + if moIsStandalone bytes "$2" "$4" "$5"; then + bytes=( $bytes ) + echo -n "${2:0:${bytes[0]}}" + local "$1" && moIndirect "$1" "${4:${bytes[1]}}" + else + echo -n "$2" + local "$1" && moIndirect "$1" "$4" + fi +} + + +# Internal: Handle the content for a tag that is never "standalone". No +# adjustments are made for newlines and whitespace. +# +# $1 - Name of destination "content" variable. +# $2 - Content before the tag that was not yet written +# $3 - Tag content (not used) +# $4 - Content after the tag +# +# Returns nothing. +moStandaloneDenied() { + echo -n "$2" + local "$1" && moIndirect "$1" "$4" +} + + +# Internal: Determines if the named thing is a function or if it is a +# non-empty environment variable. When MO_FALSE_IS_EMPTY is set to a +# non-empty value, then "false" is also treated is an empty value. +# +# Do not use variables without prefixes here if possible as this needs to +# check if any name exists in the environment +# +# $1 - Name of environment variable or function +# $2 - Current value (our context) +# MO_FALSE_IS_EMPTY - When set to a non-empty value, this will say the +# string value "false" is empty. +# +# Returns 0 if the name is not empty, 1 otherwise. When MO_FALSE_IS_EMPTY +# is set, this returns 1 if the name is "false". +moTest() { + # Test for functions + moIsFunction "$1" && return 0 + + if moIsArray "$1"; then + # Arrays must have at least 1 element + eval "[[ \"\${#${1}[@]}\" -gt 0 ]]" && return 0 + else + # If MO_FALSE_IS_EMPTY is set, then return 1 if the value of + # the variable is "false". + # shellcheck disable=SC2031 + [[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${!1-}" == "false" ]] && return 1 + + # Environment variables must not be empty + [[ -n "${!1-}" ]] && return 0 + fi + + return 1 +} + +# Internal: Determine if a variable is assigned, even if it is assigned an empty +# value. +# +# $1 - Variable name to check. +# +# Returns true (0) if the variable is set, 1 if the variable is unset. +moTestVarSet() { + [[ "${!1-a}" == "${!1-b}" ]] +} + + +# Internal: Trim the leading whitespace only. +# +# $1 - Name of destination variable +# $2 - The string +# $3 - true/false - trim front? +# $4 - true/false - trim end? +# $5-@ - Characters to trim +# +# Returns nothing. +moTrimChars() { + local back current front last target varName + + target=$1 + current=$2 + front=$3 + back=$4 + last="" + shift 4 # Remove target, string, trim front flag, trim end flag + + while [[ "$current" != "$last" ]]; do + last=$current + + for varName in "$@"; do + $front && current="${current/#$varName}" + $back && current="${current/%$varName}" + done + done + + local "$target" && moIndirect "$target" "$current" +} + + +# Internal: Trim leading and trailing whitespace from a string. +# +# $1 - Name of variable to store trimmed string +# $2 - The string +# +# Returns nothing. +moTrimWhitespace() { + local result + + moTrimChars result "$2" true true $'\r' $'\n' $'\t' " " + local "$1" && moIndirect "$1" "$result" +} + + +# Internal: Displays the usage for mo. Pulls this from the file that +# contained the `mo` function. Can only work when the right filename +# comes is the one argument, and that only happens when `mo` is called +# with `$0` set to this file. +# +# $1 - Filename that has the help message +# +# Returns nothing. +moUsage() { + grep '^#/' "${MO_ORIGINAL_COMMAND}" | cut -c 4- +} + + +# Save the original command's path for usage later +MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}" + +# If sourced, load all functions. +# If executed, perform the actions as expected. +if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then + mo "$@" +fi + diff --git a/bin/shml b/bin/shml new file mode 100755 index 0000000..2e3a807 --- /dev/null +++ b/bin/shml @@ -0,0 +1,490 @@ +#!/usr/bin/env bash + +#SHML:START +#************************************************# +# SHML - Shell Markup Language Framework +# v1.0.3 +# (MIT) +# by Justin Dorfman - @jdorfman +# && Joshua Mervine - @mervinej +# +# https://maxcdn.github.io/shml/ +#************************************************# + +# Foreground (Text) +## +fgcolor() { + local __end='\033[39m' + local __color=$__end # end by default + case "$1" in + end|off|reset) __color=$__end;; + black|000000) __color='\033[30m';; + red|F00BAF) __color='\033[31m';; + green|00CD00) __color='\033[32m';; + yellow|CDCD00) __color='\033[33m';; + blue|0286fe) __color='\033[34m';; + magenta|e100cc) __color='\033[35m';; + cyan|00d3cf) __color='\033[36m';; + gray|e4e4e4) __color='\033[90m';; + darkgray|4c4c4c) __color='\033[91m';; + lightgreen|00fe00) __color='\033[92m';; + lightyellow|f8fe00) __color='\033[93m';; + lightblue|3a80b5) __color='\033[94m';; + lightmagenta|fe00fe) __color='\033[95m';; + lightcyan|00fefe) __color='\033[96m';; + white|ffffff) __color='\033[97m';; + esac + if test "$2"; then + echo -en "$__color$2$__end" + else + echo -en "$__color" + fi +} + +# Backwards Compatibility +color() { + fgcolor "$@" +} + +# Aliases +fgc() { + fgcolor "$@" +} + +c() { + fgcolor "$@" +} + +# Background +## +bgcolor() { + local __end='\033[49m' + local __color=$__end # end by default + case "$1" in + end|off|reset) __color=$__end;; + black|000000) __color='\033[40m';; + red|F00BAF) __color='\033[41m';; + green|00CD00) __color='\033[42m';; + yellow|CDCD00) __color='\033[43m';; + blue|0286fe) __color='\033[44m';; + magenta|e100cc) __color='\033[45m';; + cyan|00d3cf) __color='\033[46m';; + gray|e4e4e4) __color='\033[47m';; + darkgray|4c4c4c) __color='\033[100m';; + lightred) __color='\033[101m';; + lightgreen|00fe00) __color='\033[102m';; + lightyellow|f8fe00) __color='\033[103m';; + lightblue|3a80b5) __color='\033[104m';; + lightmagenta|fe00fe) __color='\033[105m';; + lightcyan|00fefe) __color='\033[106m';; + white|fffff) __color='\033[107m';; + esac + + if test "$2"; then + echo -en "$__color$2$__end" + else + echo -en "$__color" + fi +} + +#Backwards Compatibility +background() { + bgcolor "$@" +} + +#Aliases +bgc() { + bgcolor "$@" +} + +bg() { + bgcolor "$@" +} + +## Color Bar +color-bar() { + if test "$2"; then + for i in "$@"; do + echo -en "$(background "$i" " ")" + done; echo + else + for i in {16..21}{21..16}; do + echo -en "\033[48;5;${i}m \033[0m" + done; echo + fi +} + +#Alises +cb() { + color-bar "$@" +} + +bar() { + color-bar "$@" +} + +## Attributes +## +attribute() { + local __end='\033[0m' + local __attr=$__end # end by default + case "$1" in + end|off|reset) __attr=$__end;; + bold) __attr='\033[1m';; + dim) __attr='\033[2m';; + underline) __attr='\033[4m';; + blink) __attr='\033[5m';; + invert) __attr='\033[7m';; + hidden) __attr='\033[8m';; + esac + if test "$2"; then + echo -en "$__attr$2$__end" + else + echo -en "$__attr" + fi +} +a() { + attribute "$@" +} + +## Elements +br() { + echo -e "\n\r" +} + +tab() { + echo -e "\t" +} + +indent() { + local __len=4 + if test "$1"; then + if [[ $1 =~ $re ]] ; then + __len=$1 + fi + fi + while [ $__len -gt 0 ]; do + echo -n " " + __len=$(( $__len - 1 )) + done +} +i() { + indent "$@" +} + +hr() { + local __len=60 + local __char='-' + if ! test "$2"; then + re='^[0-9]+$' + if [[ $1 =~ $re ]] ; then + __len=$1 + elif test "$1"; then + __char=$1 + fi + else + __len=$2 + __char=$1 + fi + while [ $__len -gt 0 ]; do + echo -n "$__char" + __len=$(( $__len - 1 )) + done +} + +# Icons +## + +icon() { + local i=''; + case "$1" in + check|checkmark) i='\xE2\x9C\x93';; + X|x|xmark) i='\xE2\x9C\x98';; + '<3'|heart) i='\xE2\x9D\xA4';; + sun) i='\xE2\x98\x80';; + '*'|star) i='\xE2\x98\x85';; + darkstar) i='\xE2\x98\x86';; + umbrella) i='\xE2\x98\x82';; + flag) i='\xE2\x9A\x91';; + snow|snowflake) i='\xE2\x9D\x84';; + music) i='\xE2\x99\xAB';; + scissors) i='\xE2\x9C\x82';; + tm|trademark) i='\xE2\x84\xA2';; + copyright) i='\xC2\xA9';; + apple) i='\xEF\xA3\xBF';; + skull|bones) i='\xE2\x98\xA0';; + ':-)'|':)'|smile|face) i='\xE2\x98\xBA';; + *) + entity $1; return 0;; + esac + echo -ne "$i"; +} +emoji() { + local i="" + case "$1" in + + 1F603|smiley|'=)'|':-)'|':)') i='๐Ÿ˜ƒ';; + 1F607|innocent|halo) i='๐Ÿ˜‡';; + 1F602|joy|lol|laughing) i='๐Ÿ˜‚';; + 1F61B|tongue|'=p'|'=P') i='๐Ÿ˜›';; + 1F60A|blush|'^^'|blushing) i='๐Ÿ˜Š';; + 1F61F|worried|sadface|sad) i='๐Ÿ˜Ÿ';; + 1F622|cry|crying|tear) i='๐Ÿ˜ข';; + 1F621|rage|redface) i='๐Ÿ˜ก';; + 1F44B|wave|hello|goodbye) i='๐Ÿ‘‹';; + 1F44C|ok_hand|perfect|okay|nice) i='๐Ÿ‘Œ';; + 1F44D|thumbsup|+1|like) i='๐Ÿ‘';; + 1F44E|thumbsdown|-1|no|dislike) i='๐Ÿ‘Ž';; + 1F63A|smiley_cat|happycat) i='๐Ÿ˜บ';; + 1F431|cat|kitten|:3|kitty) i='๐Ÿฑ';; + 1F436|dog|puppy) i='๐Ÿถ';; + 1F41D|bee|honeybee|bumblebee) i='๐Ÿ';; + 1F437|pig|pighead) i='๐Ÿท';; + 1F435|monkey_face|monkey) i='๐Ÿต';; + 1F42E|cow|happycow) i='๐Ÿฎ';; + 1F43C|panda_face|panda|shpanda) i='๐Ÿผ';; + 1F363|sushi|raw|sashimi) i='๐Ÿฃ';; + 1F3E0|home|house) i='๐Ÿ ';; + 1F453|eyeglasses|bifocals) i='๐Ÿ‘“';; + 1F6AC|smoking|smoke|cigarette) i='๐Ÿšฌ';; + 1F525|fire|flame|hot|snapstreak) i='๐Ÿ”ฅ';; + 1F4A9|hankey|poop|shit) i='๐Ÿ’ฉ';; + 1F37A|beer|homebrew|brew) i='๐Ÿบ';; + 1F36A|cookie|biscuit|chocolate) i='๐Ÿช';; + 1F512|lock|padlock|secure) i='๐Ÿ”’';; + 1F513|unlock|openpadlock) i='๐Ÿ”“';; + 2B50|star|yellowstar) i='โญ';; + 1F0CF|black_joker|joker|wild) i='๐Ÿƒ';; + 2705|white_check_mark|check) i='โœ…';; + 274C|x|cross|xmark) i='โŒ';; + 1F6BD|toilet|restroom|loo) i='๐Ÿšฝ';; + 1F514|bell|ringer|ring) i='๐Ÿ””';; + 1F50E|mag_right|search|magnify) i='๐Ÿ”Ž';; + 1F3AF|dart|bullseye|darts) i='๐ŸŽฏ';; + 1F4B5|dollar|cash|cream) i='๐Ÿ’ต';; + 1F4AD|thought_balloon|thinking) i='๐Ÿ’ญ';; + 1F340|four_leaf_clover|luck) i='๐Ÿ€';; + + *) + #entity $1; return 0;; + esac + echo -ne "$i" +} + +function e { + emoji "$@" +} + +#SHML:END + + +# Usage / Examples +## +if [[ "$(basename -- "$0")" = "shml.sh" ]]; then +I=2 +echo -e " + +$(a bold 'SHML Usage / Help') +$(hr '=') + +$(a bold 'Section 0: Sourcing') +$(hr '-') + +$(i $I)When installed in path: +$(i $I) source \$(which shml.sh) + +$(i $I)When installed locally: +$(i $I) source ./shml.sh + +$(a bold 'Section 1: Foreground') +$(hr '-') + +$(i $I)\$(color red \"foo bar\") +$(i $I)$(color red "foo bar") + +$(i $I)\$(color blue \"foo bar\") +$(i $I)$(color blue "foo bar") + +$(i $I)\$(fgcolor green) +$(i $I) >>foo bar<< +$(i $I) >>bah boo<< +$(i $I)\$(fgcolor end) +$(i $I)$(fgcolor green) +$(i $I)>>foo bar<< +$(i $I)>>bah boo<< +$(i $I)$(fgcolor end) + +$(i $I)Short Hand: $(a underline 'c') + +$(i $I)\$(c red 'foo') + +$(i $I)Argument list: + +$(i $I)black, red, green, yellow, blue, magenta, cyan, gray, +$(i $I)white, darkgray, lightgreen, lightyellow, lightblue, +$(i $I)lightmagenta, lightcyan + +$(i $I)Termination: end, off, reset + +$(i $I)Default (no arg): end + + +$(a bold 'Section 2: Background') +$(hr '-') + +$(i $I)\$(bgcolor red \"foo bar\") +$(i $I)$(background red "foo bar") + +$(i $I)\$(background blue \"foo bar\") +$(i $I)$(background blue "foo bar") + +$(i $I)\$(background green) +$(i $I)$(i $I)>>foo bar<< +$(i $I)$(i $I)>>bah boo<< +$(i $I)\$(background end) +$(background green) +$(i $I)>>foo bar<< +$(i $I)>>bah boo<< +$(background end) + +$(i $I)Short Hand: $(a underline 'bg') + +$(i $I)\$(bg red 'foo') + +$(i $I)Argument list: + +$(i $I)black, red, green, yellow, blue, magenta, cyan, gray, +$(i $I)white, darkgray, lightred, lightgreen, lightyellow, +$(i $I)lightblue, lightmagenta, lightcyan + +$(i $I)Termination: end, off, reset + +$(i $I)Default (no arg): end + + +$(a bold 'Section 3: Attributes') +$(hr '-') + +$(i $I)$(a bold "Attributes only work on vt100 compatible terminals.") + +$(i $I)> Note: +$(i $I)> $(a underline 'attribute end') turns off everything, +$(i $I)> including foreground and background color. + +$(i $I)\$(attribute bold \"foo bar\") +$(i $I)$(attribute bold "foo bar") + +$(i $I)\$(attribute underline \"foo bar\") +$(i $I)$(attribute underline "foo bar") + +$(i $I)\$(attribute blink \"foo bar\") +$(i $I)$(attribute blink "foo bar") + +$(i $I)\$(attribute invert \"foo bar\") +$(i $I)$(attribute invert "foo bar") + +$(i $I)\$(attribute dim) +$(i $I)$(i $I)>>foo bar<< +$(i $I)$(i $I)>>bah boo<< +$(i $I)\$(attribute end) +$(i $I)$(attribute dim) +$(i $I)$(i $I)>>foo bar<< +$(i $I)$(i $I)>>bah boo<< +$(i $I)$(attribute end) + +$(i $I)Short Hand: $(a underline 'a') + +$(i $I)\$(a bold 'foo') + +$(i $I)Argument list: + +$(i $I)bold, dim, underline, blink, invert, hidden + +$(i $I)Termination: end, off, reset + +$(i $I)Default (no arg): end + + +$(a bold 'Section 4: Elements') +$(hr '-') + +$(i $I)foo\$(br)\$(tab)bar +$(i $I)foo$(br)$(tab)bar +$(i $I) +$(i $I)foo\$(br)\$(indent)bar\$(br)\$(indent 6)boo +$(i $I)foo$(br)$(indent)bar$(br)$(indent 6)boo +$(i $I) +$(i $I)> Note: short hand for $(a underline 'indent') is $(a underline 'i') +$(i $I) +$(i $I)\$(hr) +$(i $I)$(hr) +$(i $I) +$(i $I)\$(hr 50) +$(i $I)$(hr 50) +$(i $I) +$(i $I)\$(hr '~' 40) +$(i $I)$(hr '~' 40) +$(i $I) +$(i $I)\$(hr '#' 30) +$(i $I)$(hr '#' 30) + + +$(a bold 'Section 5: Icons') +$(hr '-') + +$(i $I)Icons +$(i $I)$(hr '-' 10) + +$(i $I)\$(icon check) \$(icon '<3') \$(icon '*') \$(icon ':)') + +$(i $I)$(icon check) $(icon '<3') $(icon '*') $(icon 'smile') + +$(i $I)Argument list: + +$(i $I)check|checkmark, X|x|xmark, <3|heart, sun, *|star, +$(i $I)darkstar, umbrella, flag, snow|snowflake, music, +$(i $I)scissors, tm|trademark, copyright, apple, +$(i $I):-)|:)|smile|face + + +$(a bold 'Section 6: Emojis') +$(hr '-') + +$(i $I)Couldn't peep it with a pair of \$(emoji bifocals) +$(i $I)Couldn't peep it with a pair of $(emoji bifocals) +$(i $I) +$(i $I)I'm no \$(emoji joker) play me as a \$(emoji joker) +$(i $I)I'm no $(emoji joker) play me as a $(emoji joker) +$(i $I) +$(i $I)\$(emoji bee) on you like a \$(emoji house) on \$(emoji fire), \$(emoji smoke) ya +$(i $I)$(emoji bee) on you like a $(emoji house) on $(emoji fire), $(emoji smoke) ya +$(i $I) +$(i $I)$(a bold 'Each Emoji has 1 or more alias') +$(i $I) +$(i $I)\$(emoji smiley) \$(emoji 1F603) \$(emoji '=)') \$(emoji ':-)') \$(emoji ':)') +$(i $I)$(emoji smiley) $(emoji 1F603) $(emoji '=)') $(emoji ':-)') $(emoji ':)') + +$(a bold 'Section 7: Color Bar') +$(hr '-') + +$(i $I)\$(color-bar) +$(i $I)$(color-bar) +$(i $I) +$(i $I)\$(color-bar red green yellow blue magenta \\ +$(i $I)$(i 15)cyan lightgray darkgray lightred \\ +$(i $I)$(i 15)lightgreen lightyellow lightblue \\ +$(i $I)$(i 15)lightmagenta lightcyan) +$(i $I)$(color-bar red green yellow blue magenta \ + cyan lightgray darkgray lightred \ + lightgreen lightyellow lightblue \ + lightmagenta lightcyan) + +$(i $I)Short Hand: $(a underline 'bar') +$(i $I) +$(i $I)\$(bar black yellow black yellow black yellow) +$(i $I)$(bar black yellow black yellow black yellow) + +" | less -r +fi + +# vim: ft=sh: diff --git a/config/zsh/zshrc b/config/zsh/zshrc index 23de4ab..ba94149 100644 --- a/config/zsh/zshrc +++ b/config/zsh/zshrc @@ -14,3 +14,7 @@ export EDITOR='nano' fpath+=(${DOTF_LIB}/completions/src ${DOTF_LIB}/local) for file in ${ZSHRCD}/*.zsh; do source $file; done + +# Prompt +autoload -U promptinit && promptinit +prompt filthy diff --git a/config/zsh/zshrc.d/00-ohmy.zsh b/config/zsh/zshrc.d/00-ohmy.zsh index b73edfe..6e24d3a 100644 --- a/config/zsh/zshrc.d/00-ohmy.zsh +++ b/config/zsh/zshrc.d/00-ohmy.zsh @@ -1,7 +1,7 @@ export ZSH="${DOTF_LIB}/ohmyzsh" # Settings -ZSH_THEME="mortalscumbag" +#ZSH_THEME="mortalscumbag" DISABLE_AUTO_UPDATE="true" source ${ZSH}/oh-my-zsh.sh diff --git a/lib/local/_desk b/lib/local/_desk new file mode 100644 index 0000000..2aeac44 --- /dev/null +++ b/lib/local/_desk @@ -0,0 +1,34 @@ +#compdef desk +#autoload + +_all_desks() { + desks=($(desk list --only-names)) +} + +local expl +local -a desks + +local -a _subcommands +_subcommands=( + 'help:Print a help message.' + 'init:Initialize your desk configuration.' + 'list:List available desks' + 'ls:List available desks' + 'edit:Edit or create a desk, defaults to current desk' + 'go:Activate a desk' + '.:Activate a desk' + 'run:Run a command within a desk environment' + 'version:Show the desk version.' +) + +if (( CURRENT == 2 )); then + _describe -t commands 'desk subcommand' _subcommands + return +fi + +case "$words[2]" in + go|.|edit|run) + _all_desks + _wanted desks expl 'desks' compadd -a desks ;; +esac + diff --git a/lib/local/_lj b/lib/local/_lj new file mode 100644 index 0000000..20a9fe6 --- /dev/null +++ b/lib/local/_lj @@ -0,0 +1,61 @@ +#compdef lj + +### +# List of log levels +### +_levels=( + 'emergency' + 'alert' + 'critical' + 'error' + 'warning' + 'notice' + 'info' + 'debug' +) + +### +# Describe log levels +### +function _lumberjack_log_levels() { + _describe -t levels 'levels' _levels "$@" +} + +### +# Lumberjack completion +### +function _lumberjack() { + typeset -A opt_args + local context state line curcontext="$curcontext" + + # Set option arguments + _arguments -A \ + '(-h --help)'{-h,--help}'[show help text and exit]' \ + '(-v --version)'{-v,--version}'[show version information and exit]' \ + '(-f --file)'{-f,--file}'[set log file and exit]' \ + '(-l --level)'{-l,--level}'[set log level and exit]' \ + + # Set log level arguments + _arguments \ + '1: :_lumberjack_log_levels' \ + '*::arg:->args' + + # Complete option arguments + case "$state" in + args ) + case "$words[1]" in + --file|-f ) + _arguments \ + '1:file:_files' + ;; + --level|-l ) + _arguments \ + '1:level:_lumberjack_log_levels' + ;; + esac + ;; + esac +} + +_lumberjack "$@" + diff --git a/lib/local/prompt_filthy_setup b/lib/local/prompt_filthy_setup new file mode 100644 index 0000000..4b793f6 --- /dev/null +++ b/lib/local/prompt_filthy_setup @@ -0,0 +1,312 @@ +#!/usr/bin/env zsh + +# Filthy +# by James Dinsdale +# https://github.com/molovo/filthy +# MIT License + +# Largely based on Pure by Sindre Sorhus + +# For my own and others sanity +# git: +# %b => current branch +# %a => current action (rebase/merge) +# prompt: +# %F => color dict +# %f => reset color +# %~ => current path +# %* => time +# %n => username +# %m => shortname host +# %(?..) => prompt conditional - %(condition.true.false) + +prompt_filthy_nice_exit_code() { + local exit_status="${1:-$(print -P %?)}"; + # nothing to do here + [[ ${FILTHY_SHOW_EXIT_CODE:=0} != 1 || -z $exit_status || $exit_status == 0 ]] && return; + + local sig_name; + + # is this a signal name (error code = signal + 128) ? + case $exit_status in + 129) sig_name=HUP ;; + 130) sig_name=INT ;; + 131) sig_name=QUIT ;; + 132) sig_name=ILL ;; + 134) sig_name=ABRT ;; + 136) sig_name=FPE ;; + 137) sig_name=KILL ;; + 139) sig_name=SEGV ;; + 141) sig_name=PIPE ;; + 143) sig_name=TERM ;; + esac + + # usual exit codes + case $exit_status in + -1) sig_name=FATAL ;; + 1) sig_name=WARN ;; # Miscellaneous errors, such as "divide by zero" + 2) sig_name=BUILTINMISUSE ;; # misuse of shell builtins (pretty rare) + 126) sig_name=CCANNOTINVOKE ;; # cannot invoke requested command (ex : source script_with_syntax_error) + 127) sig_name=CNOTFOUND ;; # command not found (ex : source script_not_existing) + esac + + # assuming we are on an x86 system here + # this MIGHT get annoying since those are in a range of exit codes + # programs sometimes use.... we'll see. + case $exit_status in + 19) sig_name=STOP ;; + 20) sig_name=TSTP ;; + 21) sig_name=TTIN ;; + 22) sig_name=TTOU ;; + esac + + echo "$ZSH_PROMPT_EXIT_SIGNAL_PREFIX${exit_status}:${sig_name:-$exit_status}$ZSH_PROMPT_EXIT_SIGNAL_SUFFIX "; +} + +# turns seconds into human readable time +# 165392 => 1d 21h 56m 32s +prompt_filthy_human_time() { + local tmp=$(( $1 / 1000 )) + local days=$(( tmp / 60 / 60 / 24 )) + local hours=$(( tmp / 60 / 60 % 24 )) + local minutes=$(( tmp / 60 % 60 )) + local seconds=$(( tmp % 60 )) + (( $days > 0 )) && print -n "${days}d " + (( $hours > 0 )) && print -n "${hours}h " + (( $minutes > 0 )) && print -n "${minutes}m " + (( $seconds > 5 )) && print -n "${seconds}s" + (( $tmp <= 5 )) && print -n "${1}ms" +} + +# displays the exec time of the last command if set threshold was exceeded +prompt_filthy_cmd_exec_time() { + local stop=$((EPOCHREALTIME*1000)) + local start=${cmd_timestamp:-$stop} + integer elapsed=$stop-$start + (($elapsed > ${FILTHY_CMD_MAX_EXEC_TIME:=500})) && prompt_filthy_human_time $elapsed +} + +prompt_filthy_preexec() { + cmd_timestamp=$((EPOCHREALTIME*1000)) + + # shows the current dir and executed command in the title when a process is active + print -Pn "\e]0;" + echo -nE "$PWD:t: $2" + print -Pn "\a" +} + +# string length ignoring ansi escapes +prompt_filthy_string_length() { + print ${#${(S%%)1//(\%([KF1]|)\{*\}|\%[Bbkf])}} +} + +prompt_filthy_precmd() { + local prompt_filthy_preprompt git_root current_path branch repo_status + + # Ensure prompt starts on a new line + prompt_filthy_preprompt="\n" + + # Print connection info + prompt_filthy_preprompt+="$(prompt_filthy_connection_info)" + + # check if we're in a git repo, and show git info if we are + if command git rev-parse --is-inside-work-tree &>/dev/null; then + # Print the name of the repository + git_root=$(git rev-parse --show-toplevel) + prompt_filthy_preprompt+="%B%F{yellow}$(basename ${git_root})%b%f" + + # Print the current_path relative to the git root + current_path=$(git rev-parse --show-prefix) + prompt_filthy_preprompt+=" %F{blue}${${current_path%/}:-"/"}%f" + else + # We're not in a repository, so just print the current path + prompt_filthy_preprompt+="%F{blue}%~%f" + fi + + # Print everything so far in the title + # print -Pn '\e]0;${prompt_filthy_preprompt}\a' + + # Echo command exec time + prompt_filthy_preprompt+=" %F{yellow}$(prompt_filthy_cmd_exec_time)%f" + + if [[ -f "${ZDOTDIR:-$HOME}/.promptmsg" ]]; then + # Echo any stored messages after the pre-prompt + prompt_filthy_preprompt+=" $(cat ${ZDOTDIR:-$HOME}/.promptmsg)" + fi + + # We've already added any messages to our prompt, so let's reset them + cat /dev/null >! "${ZDOTDIR:-$HOME}/.promptmsg" + + print -P $prompt_filthy_preprompt + + # reset value since `preexec` isn't always triggered + unset cmd_timestamp +} + +prompt_filthy_rprompt() { + # check if we're in a git repo, and show git info if we are + if command git rev-parse --is-inside-work-tree &>/dev/null; then + # Print the repository status + branch=$(prompt_filthy_git_branch) + repo_status=$(prompt_filthy_git_repo_status) + ci_status=$(prompt_filthy_ci_status) + fi + + if builtin type zvm >/dev/null 2>&1; then + zvm_version=" $(zvm current --quiet)" + fi + + print "${branch}${repo_status}${ci_status}%F{yellow}${zvm_version}%f" +} + +prompt_filthy_ci_status() { + local state git_dir_local state_file + + [[ $FILTHY_SHOW_CI_STATUS -eq 0 ]] && return + + builtin type hub >/dev/null 2>&1 || return + + git_dir_local="$(git rev-parse --git-dir)" + state_file="${git_dir_local}/ci-status" + + function _retrieve_ci_status() { + # Delay the asynchronous process, otherwise the status file + # will be empty when we read it + sleep 1 + + state=$(hub ci-status 2>&1) + cat /dev/null >! "${state_file}" 2>/dev/null + case $state in + success ) + print '%F{green}โ—%f' >> "${state_file}" + ;; + pending ) + print '%F{yellow}โ—‹%f' >> "${state_file}" + ;; + failure ) + print '%F{red}โ—%f' >> "${state_file}" + ;; + error ) + print '%F{red}โ€ผ%f' >> "${state_file}" + ;; + 'no status' ) + print '%F{242}โ—‹%f' >> "${state_file}" + ;; + esac + } + + _retrieve_ci_status >/dev/null 2>&1 &! + + state=$(cat "${state_file}" 2>/dev/null) + + [[ -n $state ]] && print " $state" +} + +prompt_filthy_git_repo_status() { + # Do a fetch asynchronously + git fetch > /dev/null 2>&1 &! + + local clean + local rtn="" + local count + local up + local down + + dirty="$(git diff --ignore-submodules=all HEAD 2>/dev/null)" + [[ $dirty != "" ]] && rtn+=" %F{242}โ€ฆ%f" + + staged="$(git diff --staged HEAD 2>/dev/null)" + [[ $staged != "" ]] && rtn+=" %F{242}*%f" + + # check if there is an upstream configured for this branch + # exit if there isn't, as we can't check for remote changes + if command git rev-parse --abbrev-ref @'{u}' &>/dev/null; then + # if there is, check git left and right arrow_status + count="$(command git rev-list --left-right --count HEAD...@'{u}' 2>/dev/null)" + + # Get the push and pull counts + up="$count[(w)1]" + down="$count[(w)2]" + + # Check if either push or pull is needed + [[ $up > 0 || $down > 0 ]] && rtn+=" " + + # Push is needed, show up arrow + [[ $up > 0 ]] && rtn+="%F{yellow}โ‡ก%f" + + # Pull is needed, show down arrow + [[ $down > 0 ]] && rtn+="%F{yellow}โ‡ฃ%f" + fi + + print $rtn +} + +prompt_filthy_git_branch() { + # get the current git status + local branch git_dir_local rtn + + branch=$(git status --short --branch -uno --ignore-submodules=all | head -1 | awk '{print $2}' 2>/dev/null) + git_dir_local=$(git rev-parse --git-dir) + + # remove reference to any remote tracking branch + branch=${branch%...*} + + # check if HEAD is detached + if [[ -d "${git_dir_local}/rebase-merge" ]]; then + branch=$(git status | head -5 | tail -1 | awk '{print $6}') + rtn="%F{red}rebasing interactively%f%F{242} โ†’ ${branch//([[:space:]]|\')/}%f" + elif [[ -d "${git_dir_local}/rebase-apply" ]]; then + branch=$(git status | head -2 | tail -1 | awk '{print $6}') + rtn="%F{red}rebasing%f%F{242} โ†’ ${branch//([[:space:]]|\')/}%f" + elif [[ -f "${git_dir_local}/MERGE_HEAD" ]]; then + branch=$(git status | head -1 | awk '{print $3}') + rtn="%F{red}merging%f%F{242} โ†’ ${branch//([[:space:]]|\')/}%f" + elif [[ "$branch" = "HEAD" ]]; then + commit=$(git status HEAD -uno --ignore-submodules=all | head -1 | awk '{print $4}' 2>/dev/null) + + if [[ "$commit" = "on" ]]; then + rtn="%F{yellow}no branch%f" + else + rtn="%F{242}detached@%f" + rtn+="%F{yellow}" + rtn+="$commit" + rtn+="%f" + fi + else + rtn="%F{242}$branch%f" + fi + + print "$rtn" +} + +prompt_filthy_connection_info() { + # show username@host if logged in through SSH + if [[ "x$SSH_CONNECTION" != "x" ]]; then + echo '%(!.%B%F{red}%n%f%b.%F{242}%n%f)%F{242}@%m%f ' + else + echo '%(!.%B%F{red}%n%f%b%F{242}@%m%f .)' + fi +} + +prompt_filthy_setup() { + # prevent percentage showing up + # if output doesn't end with a newline + export PROMPT_EOL_MARK='' + + prompt_opts=(cr subst percent) + + zmodload zsh/datetime + autoload -Uz add-zsh-hook + # autoload -Uz git-info + + add-zsh-hook precmd prompt_filthy_precmd + add-zsh-hook preexec prompt_filthy_preexec + + # prompt turns red if the previous command didn't exit with 0 + PROMPT='%(?.%F{green}.%F{red}$(prompt_filthy_nice_exit_code))โฏ%f ' + + RPROMPT='$(prompt_filthy_rprompt)' +} + +prompt_filthy_setup "$@" +