diff options
| author | Craig Jennings <c@cjennings.net> | 2025-05-21 22:01:35 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-05-21 22:01:35 -0500 |
| commit | b4463015b97912658d630377fafbf630f7588d1e (patch) | |
| tree | d04b66d992fe2ce88391889c21c5d8dc97acd0ef /dotfiles/system/.zsh/lib | |
| parent | 548154ea395356868e87980b149dfc0abdc84e17 (diff) | |
moving arch dotfiles into archsetup
Diffstat (limited to 'dotfiles/system/.zsh/lib')
| -rw-r--r-- | dotfiles/system/.zsh/lib/-ftb-colorize | 34 | ||||
| -rwxr-xr-x | dotfiles/system/.zsh/lib/-ftb-fzf | 102 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/-ftb-generate-complist | 113 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/-ftb-generate-header | 35 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/-ftb-generate-query | 36 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/ftb-switch-group | 38 | ||||
| -rwxr-xr-x | dotfiles/system/.zsh/lib/ftb-tmux-popup | 83 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/zsh-ls-colors/LICENSE | 21 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/zsh-ls-colors/README.md | 114 | ||||
| -rwxr-xr-x | dotfiles/system/.zsh/lib/zsh-ls-colors/demo | 65 | ||||
| -rw-r--r-- | dotfiles/system/.zsh/lib/zsh-ls-colors/ls-colors.zsh | 186 |
11 files changed, 827 insertions, 0 deletions
diff --git a/dotfiles/system/.zsh/lib/-ftb-colorize b/dotfiles/system/.zsh/lib/-ftb-colorize new file mode 100644 index 0000000..3b1909b --- /dev/null +++ b/dotfiles/system/.zsh/lib/-ftb-colorize @@ -0,0 +1,34 @@ +#!/hint/zsh +emulate -L zsh -o cbases -o octalzeroes + +local REPLY +local -a reply stat lstat + +# fzf-tab-lscolors::match-by $1 lstat follow +zstat -A lstat -L -- $1 +# follow symlink +(( lstat[3] & 0170000 )) && zstat -A stat -- $1 2>/dev/null + +fzf-tab-lscolors::from-mode "$1" "$lstat[3]" $stat[3] +# fall back to name +[[ -z $REPLY ]] && fzf-tab-lscolors::from-name $1 + +# If this is a symlink +if [[ -n $lstat[14] ]]; then + local sym_color=$REPLY + local rsv_color=$REPLY + local rsv=$lstat[14] + # If this is not a broken symlink + if [[ -e $rsv ]]; then + # fzf-tab-lscolors::match-by $rsv stat + zstat -A stat -- $rsv + fzf-tab-lscolors::from-mode $rsv $stat[3] + # fall back to name + [[ -z $REPLY ]] && fzf-tab-lscolors::from-name $rsv + rsv_color=$REPLY + fi + dpre=$'\033[0m\033['$sym_color'm' + dsuf+=$'\033[0m -> \033['$rsv_color'm'$rsv +else + dpre=$'\033[0m\033['$REPLY'm' +fi diff --git a/dotfiles/system/.zsh/lib/-ftb-fzf b/dotfiles/system/.zsh/lib/-ftb-fzf new file mode 100755 index 0000000..19adf04 --- /dev/null +++ b/dotfiles/system/.zsh/lib/-ftb-fzf @@ -0,0 +1,102 @@ +#!/hint/zsh + +local tmp_dir=${TMPPREFIX:-/tmp/zsh}-fzf-tab-$USER +[[ -d $tmp_dir ]] || command mkdir $tmp_dir + +local ftb_preview_init=" +zmodload zsh/mapfile +local -a _ftb_compcap=(\"\${(@f)mapfile[$tmp_dir/compcap.$$]}\") +local -a _ftb_groups=(\"\${(@f)mapfile[$tmp_dir/groups.$$]}\") +local bs=\$'\2' +# get descriptoin +export desc=\${\${\"\$(<{f})\"%\$'\0'*}#*\$'\0'} +# get ctxt for current completion +local -A ctxt=(\"\${(@0)\${_ftb_compcap[(r)\${(b)desc}\$bs*]#*\$bs}}\") +# get group +if (( \$+ctxt[group] )); then + export group=\$_ftb_groups[\$ctxt[group]] +fi +# get original word +export word=\${(Q)ctxt[word]} +# get real path if it is file +if (( \$+ctxt[realdir] )); then + export realpath=\${ctxt[realdir]}\$word +fi +" +local binds=tab:down,btab:up,change:top,ctrl-space:toggle +local fzf_command fzf_flags fzf_preview debug_command tmp switch_group fzf_pad +local ret=0 + +-ftb-zstyle -s fzf-command fzf_command || fzf_command=fzf +-ftb-zstyle -a fzf-bindings tmp && binds+=,${(j:,:)tmp} +-ftb-zstyle -a fzf-flags fzf_flags +-ftb-zstyle -s fzf-preview fzf_preview +-ftb-zstyle -a switch-group switch_group || switch_group=(F1 F2) +-ftb-zstyle -s fzf-pad fzf_pad || fzf_pad=2 + +-ftb-zstyle -a debug-command debug_command && { + ${(eX)debug_command} $fzf_flags + return +} + +print -rl -- $_ftb_compcap > $tmp_dir/compcap.$$ +print -rl -- $_ftb_groups > $tmp_dir/groups.$$ +print -r -- ${ftb_preview_init/{f}/\$1} > $tmp_dir/ftb_preview_init.$$ + +binds=${binds//{_FTB_INIT_}/. $tmp_dir/ftb_preview_init.$$ {f} $'\n'} + +local -i header_lines=$#_ftb_headers +local -i lines=$(( $#_ftb_compcap + fzf_pad + header_lines )) +local reload_command="$commands[zsh] -f $FZF_TAB_HOME/lib/ftb-switch-group $$ $header_lines $tmp_dir" + +# detect if we will use tmux popup +local use_tmux_popup=0 +if [[ $fzf_command == "ftb-tmux-popup" ]]; then + use_tmux_popup=1 +fi + +if (( ! use_tmux_popup )); then + # fzf will cause the current line to refresh, so move the cursor down. + echoti cud1 >/dev/tty + # reset cursor before call fzf + echoti cnorm >/dev/tty 2>/dev/null +fi + +cat > $tmp_dir/completions.$$ + +local dd='gdd' +if (( ${+commands[$dd]} == 0 )) ; then + dd='dd' +fi +if (( ${+commands[$dd]} == 0 )) ; then + dd='true' # nop if dd is not installed +fi + +_ftb_query="${_ftb_query}$(command "$dd" bs=1G count=1 status=none iflag=nonblock < /dev/tty 2>/dev/null)" || true + +$fzf_command \ + --ansi \ + --bind=$binds \ + --bind="${switch_group[1]}:reload($reload_command -1),${switch_group[2]}:reload($reload_command 1)" \ + --color=hl:$(( header_lines == 0 ? 188 : 255 )) \ + --cycle \ + --delimiter='\x00' \ + --expect=$continuous_trigger,$print_query,$accept_line \ + --header-lines=$header_lines \ + --height=${FZF_TMUX_HEIGHT:=$(( lines > LINES / 3 * 2 ? LINES / 3 * 2 : lines ))} \ + --layout=reverse \ + --multi \ + --nth=2,3 \ + --print-query \ + --query=$_ftb_query \ + --tiebreak=begin \ + ${fzf_preview:+--preview=$ftb_preview_init$fzf_preview} \ + $fzf_flags < $tmp_dir/completions.$$ || ret=$? + +if (( ! use_tmux_popup )); then + echoti civis >/dev/tty 2>/dev/null + echoti cuu1 >/dev/tty +fi + +command rm $tmp_dir/*.$$ 2>/dev/null +return $ret diff --git a/dotfiles/system/.zsh/lib/-ftb-generate-complist b/dotfiles/system/.zsh/lib/-ftb-generate-complist new file mode 100644 index 0000000..42dd033 --- /dev/null +++ b/dotfiles/system/.zsh/lib/-ftb-generate-complist @@ -0,0 +1,113 @@ +#!/hint/zsh + +local dsuf dpre k _v filepath first_word show_group default_color prefix bs=$'\b' +local -a list_colors group_colors tcandidates reply match mbegin mend +local -i same_word=1 colorful=0 +local -Ua duplicate_groups=() +local -A word_map=() + +(( $#_ftb_compcap == 0 )) && return + +-ftb-zstyle -s show-group show_group || show_group=full +-ftb-zstyle -s default-color default_color || default_color=$'\x1b[37m' +-ftb-zstyle -s prefix prefix || { + zstyle -m ':completion:*:descriptions' format '*' && prefix='·' +} +-ftb-zstyle -a group-colors group_colors || group_colors=($_ftb_group_colors) +zstyle -a ":completion:$_ftb_curcontext" list-colors list_colors + +# init colorize +if (( $+builtins[fzf-tab-candidates-generate] )); then + fzf-tab-candidates-generate -i list_colors +else + local -A namecolors=(${(@s:=:)${(@s.:.)list_colors}:#[[:alpha:]][[:alpha:]]=*}) + local -A modecolors=(${(@Ms:=:)${(@s.:.)list_colors}:#[[:alpha:]][[:alpha:]]=*}) + (( $#namecolors == 0 && $#modecolors == 0 )) && list_colors=() +fi + +if (( $#_ftb_groups == 1 )); then + -ftb-zstyle -m single-group prefix || prefix='' + -ftb-zstyle -m single-group color || group_colors=("$default_color") +fi + +if (( $+builtins[fzf-tab-candidates-generate] )); then + fzf-tab-candidates-generate +else + for k _v in "${(@ps:\2:)_ftb_compcap}"; do + local -A v=("${(@0)_v}") + [[ $v[word] == ${first_word:=$v[word]} ]] || same_word=0 + + # add character and color to describe the type of the files + dsuf='' dpre='' + if (( $+v[realdir] )); then + filepath=$v[realdir]${(Q)v[word]} + if [[ -d $filepath ]]; then + dsuf=/ + fi + # add color and resolve symlink if have list-colors + # detail: http://zsh.sourceforge.net/Doc/Release/Zsh-Modules.html#The-zsh_002fcomplist-Module + if (( $#list_colors )) && [[ -a $filepath || -L $filepath ]]; then + -ftb-colorize $filepath + colorful=1 + elif [[ -L $filepath ]]; then + dsuf=@ + fi + if [[ $options[list_types] == off ]]; then + dsuf='' + fi + fi + + # add color to description if they have group index + if (( $+v[group] )); then + local color=$group_colors[$v[group]] + # add a hidden group index at start of string to keep group order when sorting + # first group index is for builtin sort, sencond is for GNU sort + tcandidates+=$v[group]$'\b'$color$prefix$dpre$'\0'$v[group]$'\b'$k$'\0'$dsuf + else + tcandidates+=$default_color$dpre$'\0'$k$'\0'$dsuf + fi + + # check group with duplicate member + if [[ $show_group == brief ]]; then + if (( $+word_map[$v[word]] && $+v[group] )); then + duplicate_groups+=$v[group] # add this group + duplicate_groups+=$word_map[$v[word]] # add previous group + fi + word_map[$v[word]]=$v[group] + fi + done +fi + +(( same_word )) && tcandidates[2,-1]=() + +# sort and remove sort group or other index +zstyle -T ":completion:$_ftb_curcontext" sort +if (( $? != 1 )); then + if (( colorful )); then + # if enable list_colors, we should skip the first field + if [[ ${commands[sort]:A:t} != (|busybox*) ]]; then + # this is faster but doesn't work if `find` is from busybox + tcandidates=(${(f)"$(command sort -u -t '\0' -k 2 <<< ${(pj:\n:)tcandidates})"}) + else + # slower but portable + tcandidates=(${(@o)${(@)tcandidates:/(#b)([^$'\0']#)$'\0'(*)/$match[2]$'\0'$match[1]}}) + tcandidates=(${(@)tcandidates/(#b)(*)$'\0'([^$'\0']#)/$match[2]$'\0'$match[1]}) + fi + else + tcandidates=("${(@o)tcandidates}") + fi +fi +typeset -gUa _ftb_complist=("${(@)tcandidates//[0-9]#$bs}") + +# hide needless group +if (( $#_ftb_groups )); then + local i to_hide indexs=({1..$#_ftb_groups}) + case $show_group in + brief) to_hide=(${indexs:|duplicate_groups}) ;; + none) to_hide=($indexs) ;; + esac + for i in $to_hide; do + # NOTE: _ftb_groups is unique array + _ftb_groups[i]="__hide__$i" + done +fi diff --git a/dotfiles/system/.zsh/lib/-ftb-generate-header b/dotfiles/system/.zsh/lib/-ftb-generate-header new file mode 100644 index 0000000..a54fee1 --- /dev/null +++ b/dotfiles/system/.zsh/lib/-ftb-generate-header @@ -0,0 +1,35 @@ +#!/hint/zsh + +typeset -ga _ftb_headers=() +local i tmp group_colors +local -i mlen=0 len=0 + +if (( $#_ftb_groups == 1 )) && { ! -ftb-zstyle -m single-group "header" }; then + return +fi + +# calculate the max column width +for i in $_ftb_groups; do + (( $#i > mlen )) && mlen=$#i +done +mlen+=1 + +-ftb-zstyle -a group-colors group_colors || group_colors=($_ftb_group_colors) + +for (( i=1; i<=$#_ftb_groups; i++ )); do + [[ $_ftb_groups[i] == "__hide__"* ]] && continue + + if (( len + $#_ftb_groups[i] > COLUMNS - 5 )); then + _ftb_headers+=$tmp + tmp='' && len=0 + fi + if (( len + mlen > COLUMNS - 5 )); then + # the last column doesn't need padding + _ftb_headers+=$tmp$group_colors[i]$_ftb_groups[i]$'\033[00m' + tmp='' && len=0 + else + tmp+=$group_colors[i]${(r:$mlen:)_ftb_groups[i]}$'\033[00m' + len+=$mlen + fi +done +(( $#tmp )) && _ftb_headers+=$tmp diff --git a/dotfiles/system/.zsh/lib/-ftb-generate-query b/dotfiles/system/.zsh/lib/-ftb-generate-query new file mode 100644 index 0000000..87ebb75 --- /dev/null +++ b/dotfiles/system/.zsh/lib/-ftb-generate-query @@ -0,0 +1,36 @@ +#!/hint/zsh + +local key qtype tmp query_string +typeset -g _ftb_query= +-ftb-zstyle -a query-string query_string || query_string=(prefix input first) +for qtype in $query_string; do + if [[ $qtype == prefix ]]; then + # find the longest common prefix among descriptions + local -a keys=(${_ftb_compcap%$'\2'*}) + tmp=$keys[1] + local MATCH match mbegin mend prefix=(${(s::)tmp}) + for key in ${keys:1}; do + (( $#tmp )) || break + [[ $key == $tmp* ]] && continue + # interpose characters from the current common prefix and $key and see how + # many pairs of equal characters we get at the start of the resulting string + [[ ${(j::)${${(s::)key[1,$#tmp]}:^prefix}} =~ '^(((.)\3)*)' ]] + # truncate common prefix and maintain loop invariant: ${(s::)tmp} == $prefix + tmp[$#MATCH/2+1,-1]="" + prefix[$#MATCH/2+1,-1]=() + done + elif [[ $qtype == input ]]; then + local fv=${_ftb_compcap[1]#*$'\2'} + local -A v=("${(@0)fv}") + tmp=$v[PREFIX] + if (( $RBUFFER[(i)$v[SUFFIX]] != 1 )); then + tmp=${tmp/%$v[SUFFIX]} + fi + tmp=${${tmp#$v[hpre]}#$v[apre]} + fi + if (( $query_string[(I)longest] )); then + (( $#tmp > $#_ftb_query )) && _ftb_query=$tmp + elif [[ -n $tmp ]]; then + _ftb_query=$tmp && break + fi +done diff --git a/dotfiles/system/.zsh/lib/ftb-switch-group b/dotfiles/system/.zsh/lib/ftb-switch-group new file mode 100644 index 0000000..8d06956 --- /dev/null +++ b/dotfiles/system/.zsh/lib/ftb-switch-group @@ -0,0 +1,38 @@ +#!/hint/zsh +emulate -L zsh -o extended_glob + +zmodload zsh/mapfile + +# receive arguments +local pid=$1 header_lines=$2 tmp_dir=$3 offset=$@[-1] + +# read completion list +local -a list=(${(f)mapfile[$tmp_dir/completions.$pid]}) + +# get total group count +if (( $#list > 10000 )); then + local -Ua total=(${(f)"$(print -l ${list:$header_lines} | grep -a -oP '^\x1b\[[0-9;]*m')"}) +else + local -Ua total=(${(M)${list:$header_lines}#$'\x1b['[0-9;]#*m}) +fi + +# get current group index, start from 2 +local current=2 +if [[ -f $tmp_dir/current-group.$pid ]]; then + current=$(( $(<$tmp_dir/current-group.$pid) + offset )) +fi +(( current > $#total )) && current=1 +(( current == 0 )) && current=$#total +echo $current > $tmp_dir/current-group.$pid + +# print headers +if (( header_lines != 0 )); then + print -l ${list[1,header_lines]/${total[current]}/$'\x1b[1m'} +fi + +# print current group +if (( $#list > 10000 )); then + print -l ${list:$header_lines} | grep -a -F "${total[current]}" +else + print -l ${(M)${list:$header_lines}:#${total[current]}*} +fi diff --git a/dotfiles/system/.zsh/lib/ftb-tmux-popup b/dotfiles/system/.zsh/lib/ftb-tmux-popup new file mode 100755 index 0000000..7e74d3c --- /dev/null +++ b/dotfiles/system/.zsh/lib/ftb-tmux-popup @@ -0,0 +1,83 @@ +#!/hint/zsh +# Show results with tmux popup +# Example usage: +# zstyle ':fzf-tab:*' fzf-command ftb-tmux-popup +# zstyle ':fzf-tab:*' popup-pad 0 0 +# It can also be used as a standalone tool, like: +# ls | ftb-tmux-popup +emulate -L zsh -o extended_glob + +# import min +autoload -Uz zmathfunc +zmathfunc + +: ${tmp_dir:=${TMPPREFIX:-/tmp/zsh}-fzf-tab-$USER} + +# fallback to fzf if it is not running in tmux +if (( ! $+TMUX_PANE )); then + fzf $@ + return +fi + +local ret=0 + +local -a fzf_opts=($@) +fzf_opts=(${${fzf_opts/--height*}/--layout*}) + +# get position of cursor and size of window +local -a tmp=($(command tmux display-message -p "#{pane_top} #{cursor_y} #{pane_left} #{cursor_x} #{window_height} #{window_width}")) +local cursor_y=$((tmp[1] + tmp[2])) cursor_x=$((tmp[3] + tmp[4])) window_height=$tmp[5] window_width=$tmp[6] + +# if not called by fzf-tab +if (( ! $+IN_FZF_TAB )); then + [[ -d $tmp_dir ]] || mkdir -p $tmp_dir + cat > $tmp_dir/completions.$$ +fi + +local text REPLY comp_lines comp_length length popup_pad + +zstyle -a ":fzf-tab:$_ftb_curcontext" popup-pad popup_pad || popup_pad=(0 0) + +# get the size of content, note we should remove all ANSI color code +comp_lines=$(( ${#${(f)mapfile[$tmp_dir/completions.$$]}} + $popup_pad[2] )) +if (( comp_lines <= 500 )); then + comp_length=0 + for line in ${(f)mapfile[$tmp_dir/completions.$$]}; do + length=${(m)#${(S)line//$'\x1b['[0-9]#*m}} + (( length >= comp_length )) && comp_length=$length + done +else + # FIXME: can't get the correct width of CJK characters. + comp_length=$(command sed 's/\x1b\[[0-9;]*m//g;s/\x00//g' $tmp_dir/completions.$$ | command awk 'length > max_length { max_length = length; } END { print max_length }') +fi +comp_length=$(( comp_length + $popup_pad[1] )) + +local popup_height popup_y popup_width popup_x + +# calculate the popup height and y position +if (( cursor_y * 2 > window_height )); then + # show above the cursor + popup_height=$(( min(comp_lines + 4, cursor_y) )) + popup_y=$cursor_y +else + # show below the cursor + popup_height=$(( min(comp_lines + 4, window_height - cursor_y) )) + popup_y=$(( cursor_y + popup_height + 1 )) + fzf_opts+=(--layout=reverse) +fi + +# calculate the popup width and x position +popup_width=$(( min(comp_length + 5, window_width) )) +popup_x=$(( cursor_x + popup_width > window_width ? window_width - popup_width : cursor_x )) + +echo -E "$commands[fzf] ${(qq)fzf_opts[@]} < $tmp_dir/completions.$$ > $tmp_dir/result-$$" > $tmp_dir/fzf-$$ +{ + tmux popup -x $popup_x -y $popup_y \ + -w $popup_width -h $popup_height \ + -d $PWD -E ". $tmp_dir/fzf-$$" || ret=$? + echo -E "$(<$tmp_dir/result-$$)" +} always { + command rm $tmp_dir/*-$$ + (( $+IN_FZF_TAB )) || command rm $tmp_dir/completions.$$ +} +return $ret diff --git a/dotfiles/system/.zsh/lib/zsh-ls-colors/LICENSE b/dotfiles/system/.zsh/lib/zsh-ls-colors/LICENSE new file mode 100644 index 0000000..940b4c2 --- /dev/null +++ b/dotfiles/system/.zsh/lib/zsh-ls-colors/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Gamma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dotfiles/system/.zsh/lib/zsh-ls-colors/README.md b/dotfiles/system/.zsh/lib/zsh-ls-colors/README.md new file mode 100644 index 0000000..7736ce6 --- /dev/null +++ b/dotfiles/system/.zsh/lib/zsh-ls-colors/README.md @@ -0,0 +1,114 @@ +# zsh-ls-colors + + + +A zsh library to use `LS_COLORS` in scripts or other plugins. + +For a simple demo, see the `demo` script in this repo. + +For more advanced usage, +instructions are located at top of the source files for `from-mode` and `from-name`. +If a use case isn't adequately covered, +please open an issue! + +## Using zsh-ls-colors in a plugin + +You can use this as a submodule or a subtree. + +### submodule: + +```sh +# Add (only once) +git submodule add git://github.com/xPMo/zsh-ls-colors.git ls-colors +git commit -m 'Add ls-colors as submodule' + +# Update +cd ls-colors +git fetch +git checkout origin/master +cd .. +git commit ls-colors -m 'Update ls-colors to latest' +``` + +### Subtree: + +```sh +# Initial add +git subtree add --prefix=ls-colors/ --squash -m 'Add ls-colors as a subtree' \ + git://github.com/xPMo/zsh-ls-colors.git master + +# Update +git subtree pull --prefix=ls-colors/ --squash -m 'Update ls-colors to latest' \ + git://github.com/xPMo/zsh-ls-colors.git master + + +# Or, after adding a remote: +git remote add ls-colors git://github.com/xPMo/zsh-ls-colors.git + +# Initial add +git subtree add --prefix=ls-colors/ --squash -m 'Add ls-colors as a subtree' ls-colors master + +# Update +git subtree pull --prefix=ls-colors/ --squash -m 'Update ls-colors to latest' ls-colors master +``` + +### Function namespacing + +Since functions are a public namespace, +this plugin allows you to customize the preifix for your plugin: + +```zsh +# load functions as my-lscolors::{init,match-by,from-name,from-mode} +source ${0:h}/ls-colors/ls-colors.zsh my-lscolors +``` + +### Parameter namespacing + +While indirect parameter expansion exists with `${(P)var}`, +it doesn't play nicely with array parameters. + +There are multiple strategies to prevent unnecessary re-parsing: + +```zsh +# Call once when loading. +# Pollutes global namespace but prevents re-parsing +ls-color::init +``` + +```zsh +# Don't call init at all and only use ::match-by. +# Doesn't pollute global namespace but reparses LS_COLORS on every call +ls-color::match-by $file lstat +``` + +```zsh +# Initialize within a scope with local parameters. +# Best for not polluting global namespace when multiple filenames need to be parsed. +(){ + local -A namecolors modecolors + ls-color::init + + for arg; do + ... + done +} +``` + +```zsh +# Serialize: +typeset -g LS_COLORS_CACHE_FILE=$(mktemp) +(){ + local -A namecolors modecolors + ls-color::init + typeset -p modecolors namecolors >| $LS_COLORS_CACHE_FILE + zcompile $LS_COLORS_CACHE_FILE +} + +my-function(){ + local -A namecolors modecolors + source $LS_COLORS_CACHE_FILE + + ... +} +``` + diff --git a/dotfiles/system/.zsh/lib/zsh-ls-colors/demo b/dotfiles/system/.zsh/lib/zsh-ls-colors/demo new file mode 100755 index 0000000..a5e468d --- /dev/null +++ b/dotfiles/system/.zsh/lib/zsh-ls-colors/demo @@ -0,0 +1,65 @@ +#!/usr/bin/env zsh +# set $0 (ref: zdharma.org/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html#zero-handling) +0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" +0="${${(M)0:#/*}:-$PWD/$0}" + +# load library functions +source ls-colors.zsh '' + +# to name the functions with a different namespace +# call source with a different argument +#source my-plugin::ls + +# init (sets modecolors and namecolors) +# You have options. Either you can pollute global namespace: +ls-color::init +# Or you can have ::match-by re-parse colors on every call +: # (do nothing) +# Or if you have multiple calls, you can parse colors once for a scope: +(){ + local -A modecolors namecolors + ls-color::init + + for arg; do + ls-color::match-by $arg lstat + : do something else + done +} + + +# colors can also be added for other globs after init as well: +namecolors[*.md]='01' # bold markdown files + +# EXTENDED_GLOB is enabled when matching, so things like this are possible: +namecolors[(#i)(*/|)license(|.*)]='04' # underline LICENSE, or license.txt, or similar + +local file reply +# color each file in the argument list +for file; do + ls-color::match-by $file all + # point to symlink resolution if it exists + print '\e['$reply[1]'m'$file'\e[0m'${reply[2]:+' → \e['$reply[3]'m'$reply[2]'\e[0m'} +done + +# ======================= +# Alternate manual method: +for file; do + ls-color::match-by $file lstat follow + if [[ $reply[2] ]]; then + # This is a symlink + symlink_color=$reply[1] + # If broken, use link color for destination + resolved_color=$reply[1] + resolved=$reply[2] + if [[ -e $file ]]; then + # Not broken, update destination color + ls-color::match-by $file stat + resolved_color=$reply[1] + fi + print '\e['$symlink_color'm'$file'\e[0m → \e['$resolved_color'm'$resolved'\e[0m' + else + # This is not a symlink + print '\e['$reply[1]'m'$file'\e[0m' + fi +done + diff --git a/dotfiles/system/.zsh/lib/zsh-ls-colors/ls-colors.zsh b/dotfiles/system/.zsh/lib/zsh-ls-colors/ls-colors.zsh new file mode 100644 index 0000000..276a7bb --- /dev/null +++ b/dotfiles/system/.zsh/lib/zsh-ls-colors/ls-colors.zsh @@ -0,0 +1,186 @@ +#!/usr/bin/env zsh + +# set the prefix for all functions +local pfx=${1:-'ls-color'} + +# {{{ From mode +# Usage: +# $1: filename +# $2: The value of struct stat st_mode +# If empty, modecolors lookup will be skipped +# $3: (If symlink) The value of struct stat st_mode +# for the target of $1's symlink. If unset, +# interpret as a broken link. +# Sets REPLY to the console code +${pfx}::from-mode () { + + emulate -L zsh + setopt cbases octalzeroes extendedglob + + [[ -z $2 ]] && return 1 + + local -i reg=0 + local -a codes + + local -i st_mode=$(($2)) + # See man 7 inode for more info + # file type + case $(( st_mode & 0170000 )) in + $(( 0140000 )) ) codes=( $modecolors[so] ) ;; + $(( 0120000 )) ) # symlink, special handling + if ! (($+3)); then + REPLY=$modecolors[or] + elif [[ $modecolors[ln] = target ]]; then + "$0" "$1" "${@:3}" + else + REPLY=$modecolors[ln] + fi + return + ;; + $(( 0100000 )) ) codes=( ); reg=1 ;; # regular file + $(( 0060000 )) ) codes=( $modecolors[bd] ) ;; + $(( 0040000 )) ) codes=( $modecolors[di] ) ;; + $(( 0020000 )) ) codes=( $modecolors[cd] ) ;; + $(( 0010000 )) ) codes=( $modecolors[pi] ) ;; + esac + + # setuid/setgid/sticky/other-writable + (( st_mode & 04000 )) && codes+=( $modecolors[su] ) + (( st_mode & 02000 )) && codes+=( $modecolors[sg] ) + (( ! reg )) && case $(( st_mode & 01002 )) in + # sticky + $(( 01000 )) ) codes+=( $modecolors[st] ) ;; + # other-writable + $(( 00002 )) ) codes+=( $modecolors[ow] ) ;; + # other-writable and sticky + $(( 01002 )) ) codes+=( $modecolors[tw] ) ;; + esac + + # executable + if (( ! $#codes )); then + (( st_mode & 0111 )) && codes+=( $modecolors[ex] ) + fi + + # return nonzero if no matching code + [[ ${REPLY::=${(j:;:)codes}} ]] +} # }}} +# {{{ From name +# Usage: +# $1: filename +# +# Sets REPLY to the console code +${pfx}::from-name () { + + emulate -L zsh + setopt extendedglob + + # Return non-zero if no keys match + [[ ${REPLY::=$namecolors[(k)$1]} ]] +} # }}} +# {{{ Init +# WARNING: initializes namecolors and modecolors in global scope +${pfx}::init () { + emulate -L zsh + + # Use $1 if provided, otherwise use LS_COLORS + # Use LSCOLORS on BSD + local LS_COLORS=${1:-${LS_COLORS:-$LSCOLORS}} + + # read in LS_COLORS + typeset -gA namecolors=(${(@s:=:)${(@s.:.)LS_COLORS}:#[[:alpha:]][[:alpha:]]=*}) + typeset -gA modecolors=(${(@Ms:=:)${(@s.:.)LS_COLORS}:#[[:alpha:]][[:alpha:]]=*}) +} +# }}} +# {{{ Match by +# Usage: +# $1: filename +# Optional (must be $2): g[lobal]: Use existing stat | lstat in parent scope +# ${@:2}: Append to reply: +# - l[stat] : Look up using lstat (don't follow symlink), if empty match name +# - s[tat] : Look up using stat (do follow symlink), if empty match name +# - n[ame] : Only match name +# - f[ollow]: Get resolution path of symlink +# - L[stat] : Same as above but don't match name +# - S[tat] : Same as above but don't match name +# - a[ll] : If a broken symlink: lstat follow lstat +# : If a symlink : lstat follow stat +# : Otherwise : lstat +# - A[ll] : If a broken symlink: Lstat follow Lstat +# : If a symlink : Lstat follow Stat +# : Otherwise : Lstat +# +# or returns non-zero +${pfx}::match-by () { + emulate -L zsh + setopt extendedglob cbases octalzeroes + + local arg REPLY name=$1 pfx=${0%::match-by} + shift + + # init in local scope if not using global params + if ! [[ -v namecolors && -v modecolors ]]; then + local -A namecolors modecolors + ${pfx}::init + fi + + if [[ ${1:l} = (g|global) ]]; then + shift + else + local -a stat lstat + declare -ga reply=() + fi + + zmodload -F zsh/stat b:zstat + for arg; do + case ${arg[1]:l} in + n|name) + ${pfx}::from-name $name + reply+=("$REPLY") + ;; + l|lstat) + (($#lstat)) || zstat -A lstat -L $name || return 1 + if ((lstat[3] & 0170000 )); then + # follow symlink + (($#stat)) || zstat -A stat $name 2>/dev/null + fi + ${pfx}::from-mode "$name" "$lstat[3]" $stat[3] + if [[ $REPLY || ${2[1]} = L ]]; then + reply+=("$REPLY") + else # fall back to name + "$0" "$name" g n + fi + ;; + s|stat) + (($#stat)) || zstat -A stat $name || return 1 + ${pfx}::from-mode $name $stat[3] + reply+=("$REPLY") + if [[ $REPLY || ${arg[1]} = S ]]; then + reply+=("$REPLY") + else # fall back to name + "$0" "$name" g n + fi + ;; + f|follow) + (($#lstat)) || zstat -A lstat -L $name || return 1 + reply+=("$lstat[14]") + ;; + a|all) + # Match case + "$0" "$name" g ${${${arg[1]%a}:+L}:-l} + # won't append if empty + reply+=($lstat[14]) + # $stat[14] will be empty if not a symlink + if [[ $lstat[14] ]]; then + if [[ -e $name ]]; then + "$0" "$name" g ${${${arg[1]%a}:+S}:-s} + else + reply+=($reply[-2]) + fi + fi + ;; + *) return 2 ;; + esac + done +} +# }}} +# vim: set foldmethod=marker: |
