wd.sh
17040 bytes
1#!/usr/bin/env zsh
2
3# WARP DIRECTORY
4# ==============
5# Jump to custom directories in terminal
6# because `cd` takes too long...
7#
8# @github.com/mfaerevaag/wd
9
10# version
11readonly WD_VERSION=0.10.1
12
13# colors
14readonly WD_BLUE="\033[96m"
15readonly WD_GREEN="\033[92m"
16readonly WD_YELLOW="\033[93m"
17readonly WD_RED="\033[91m"
18readonly WD_NOC="\033[m"
19
20## functions
21
22# helpers
23wd_yesorno()
24{
25 # variables
26 local question="${1}"
27 local prompt="${question} "
28 local yes_RETVAL="0"
29 local no_RETVAL="3"
30 local RETVAL=""
31 local answer=""
32
33 # read-eval loop
34 while true ; do
35 printf $prompt
36 read -r answer
37
38 case ${answer:=${default}} in
39 "Y"|"y"|"YES"|"yes"|"Yes" )
40 RETVAL=${yes_RETVAL} && \
41 break
42 ;;
43 "N"|"n"|"NO"|"no"|"No" )
44 RETVAL=${no_RETVAL} && \
45 break
46 ;;
47 * )
48 echo "Please provide a valid answer (y or n)"
49 ;;
50 esac
51 done
52
53 return ${RETVAL}
54}
55
56wd_print_msg()
57{
58 if [[ -z $wd_quiet_mode ]]
59 then
60 local color="${1:-$WD_BLUE}" # Default to blue if no color is provided
61 local msg="$2"
62
63 if [[ -z "$msg" ]]; then
64 print "${WD_RED}*${WD_NOC} Could not print message. Sorry!"
65 else
66 print " ${color}*${WD_NOC} ${msg}"
67 fi
68 fi
69}
70
71wd_print_usage()
72{
73 command cat <<- EOF
74Usage: wd [command] [point]
75
76Commands:
77 <point> Warps to the directory specified by the warp point
78 <point> <path> Warps to the directory specified by the warp point with path appended
79 add <point> Adds the current working directory to your warp points
80 add Adds the current working directory to your warp points with current directory's name
81 addcd <path> Adds a path to your warp points with the directory's name
82 addcd <path> <point> Adds a path to your warp points with a custom name
83 rm <point> Removes the given warp point
84 rm Removes the given warp point with current directory's name
85 show <point> Print path to given warp point
86 show Print warp points to current directory
87 list Print all stored warp points
88 ls <point> Show files from given warp point (ls)
89 open <point> Open the warp point in the default file explorer (open / xdg-open)
90 path <point> Show the path to given warp point (pwd)
91 clean Remove points warping to nonexistent directories (will prompt unless --force is used)
92
93 -v | --version Print version
94 -c | --config Specify config file (default ~/.warprc)
95 -q | --quiet Suppress all output
96 -f | --force Allows overwriting without warning (for add & clean)
97
98 help Show this extremely helpful text
99EOF
100}
101
102wd_exit_fail()
103{
104 local msg=$1
105
106 wd_print_msg "$WD_RED" "$msg"
107 WD_EXIT_CODE=1
108}
109
110wd_exit_warn()
111{
112 local msg=$1
113
114 wd_print_msg "$WD_YELLOW" "$msg"
115 WD_EXIT_CODE=1
116}
117
118wd_getdir()
119{
120 local name_arg=$1
121
122 point=$(wd_show "$name_arg")
123 dir=${point:28+$#name_arg+7}
124
125 if [[ -z $name_arg ]]; then
126 wd_exit_fail "You must enter a warp point"
127 break
128 elif [[ -z $dir ]]; then
129 wd_exit_fail "Unknown warp point '${name_arg}'"
130 break
131 fi
132}
133
134# core
135
136wd_warp()
137{
138 local point=$1
139 local sub=$2
140
141 if [[ $point =~ "^\.+$" ]]
142 then
143 if [[ $#1 < 2 ]]
144 then
145 wd_exit_warn "Warping to current directory?"
146 else
147 (( n = $#1 - 1 ))
148 cd -$n > /dev/null
149 WD_EXIT_CODE=$?
150 fi
151 elif [[ ${points[$point]} != "" ]]
152 then
153 if [[ $sub != "" ]]
154 then
155 cd ${points[$point]/#\~/$HOME}/$sub
156 WD_EXIT_CODE=$?
157 else
158 cd ${points[$point]/#\~/$HOME}
159 WD_EXIT_CODE=$?
160 fi
161 else
162 wd_exit_fail "Unknown warp point '${point}'"
163 fi
164}
165
166wd_add()
167{
168 local point=$1
169 local force=$2
170 cmdnames=(add rm show list ls path clean help)
171
172 if [[ $point == "" ]]
173 then
174 point=$(basename "$PWD")
175 fi
176
177 if [ ! -w "$wd_config_file" ]; then
178 wd_exit_fail "\'$wd_config_file\' is not writeable."
179 return
180 fi
181
182 if [[ $point =~ "^[\.]+$" ]]
183 then
184 wd_exit_fail "Warp point cannot be just dots"
185 elif [[ $point =~ "[[:space:]]+" ]]
186 then
187 wd_exit_fail "Warp point should not contain whitespace"
188 elif [[ $point =~ : ]] || [[ $point =~ / ]]
189 then
190 wd_exit_fail "Warp point contains illegal character (:/)"
191 elif (($cmdnames[(Ie)$point]))
192 then
193 wd_exit_fail "Warp point name cannot be a wd command (see wd -h for a full list)"
194 elif [[ ${points[$point]} == "" ]] || [ ! -z "$force" ]
195 then
196 wd_remove "$point" > /dev/null
197 printf "%q:%s\n" "${point}" "${PWD/#$HOME/~}" >> "$wd_config_file"
198 if (whence sort >/dev/null); then
199 local config_tmp=$(mktemp "${TMPDIR:-/tmp}/wd.XXXXXXXXXX")
200 # use 'cat' below to ensure we respect $wd_config_file as a symlink
201 command sort -o "${config_tmp}" "$wd_config_file" && command cat "${config_tmp}" >| "$wd_config_file" && command rm "${config_tmp}"
202 fi
203
204 wd_export_static_named_directories
205
206 wd_print_msg "$WD_GREEN" "Warp point added"
207
208 # override exit code in case wd_remove did not remove any points
209 # TODO: we should handle this kind of logic better
210 WD_EXIT_CODE=0
211 else
212 wd_exit_warn "Warp point '${point}' already exists. Use 'add --force' to overwrite."
213 fi
214}
215
216wd_addcd() {
217 local folder="$1"
218 local point=$2
219 local force=$3
220 local currentdir=$PWD
221
222 if [[ -z "$folder" ]]; then
223 wd_exit_fail "You must specify a path"
224 return
225 fi
226
227 if [[ ! -d "$folder" ]]; then
228 wd_exit_fail "The directory does not exist"
229 return
230 fi
231
232 cd "$folder" || return
233 wd_add "$point" "$force"
234 cd "$currentdir" || return
235}
236
237
238wd_remove()
239{
240 local point_list=$1
241
242 if [[ "$point_list" == "" ]]
243 then
244 point_list=$(basename "$PWD")
245 fi
246
247 if [ ! -w "$wd_config_file" ]; then
248 wd_exit_fail "\'$wd_config_file\' is not writeable."
249 return
250 fi
251
252 for point_name in $point_list ; do
253 if [[ ${points[$point_name]} != "" ]]
254 then
255 local config_tmp=$(mktemp "${TMPDIR:-/tmp}/wd.XXXXXXXXXX")
256 # Copy and delete in two steps in order to preserve symlinks
257 if sed -n "/^${point_name}:.*$/!p" "$wd_config_file" >| "$config_tmp" && command cp "$config_tmp" "$wd_config_file" && command rm "$config_tmp"
258 then
259 wd_print_msg "$WD_GREEN" "Warp point removed"
260 else
261 wd_exit_fail "Something bad happened! Sorry."
262 fi
263 else
264 wd_exit_fail "Warp point was not found"
265 fi
266 done
267}
268
269wd_browse() {
270 # Check if fzf is installed
271 if ! command -v fzf >/dev/null; then
272 wd_print_msg "$WD_RED" "This functionality requires fzf. Please install fzf first."
273 return 1
274 fi
275
276 # Ensure wd_config_file is properly set
277 if [[ -z $wd_config_file ]]; then
278 wd_config_file="${WD_CONFIG:-$HOME/.warprc}"
279 fi
280
281 # Check if config file exists
282 if [[ ! -f $wd_config_file ]]; then
283 wd_print_msg "$WD_RED" "Config file $wd_config_file does not exist. Please create it first."
284 return 1
285 fi
286
287 # Read entries from the config file
288 local entries=("${(@f)$(sed "s:${HOME}:~:g" "$wd_config_file" | awk -F ':' '{print $1 " -> " $2}')}")
289 if [[ -z $entries ]]; then
290 wd_print_msg "$WD_YELLOW" "You don't have any warp points to browse"
291 return 1
292 fi
293
294 # Temp file for remove operations
295 local script_path="${${(%):-%x}:h}"
296 local wd_remove_output=$(mktemp "${TMPDIR:-/tmp}/wd.XXXXXXXXXX")
297
298 # Create fzf bindings
299 entries=("All warp points:" "Press enter to select. Press delete to remove" "${entries[@]}")
300 local fzf_bind="delete:execute(echo {} | awk -F ' -> ' '{print \$1}' | xargs -I {} \"$script_path/wd.sh\" rm {} > \"$wd_remove_output\")+abort"
301
302 # Run fzf
303 local selected_entry=$(printf '%s\n' "${entries[@]}" | fzf --height 100% --reverse --header-lines=2 --bind="$fzf_bind")
304
305 # Handle selection
306 if [[ -e $wd_remove_output ]]; then
307 cat "$wd_remove_output"
308 rm -f "$wd_remove_output"
309 fi
310
311 if [[ -n $selected_entry ]]; then
312 local selected_point="${selected_entry%% ->*}"
313 selected_point=$(echo "$selected_point" | xargs)
314 wd $selected_point
315 fi
316}
317
318wd_browse_widget() {
319 # Ensure wd_config_file is properly set
320 if [[ -z $wd_config_file ]]; then
321 wd_config_file="${WD_CONFIG:-$HOME/.warprc}"
322 fi
323
324 # Check if config file exists
325 if [[ ! -f $wd_config_file ]]; then
326 wd_print_msg "$WD_RED" "Config file $wd_config_file does not exist. Please create it first."
327 return 1
328 fi
329
330 # Call wd_browse to handle the selection
331 wd_browse
332
333 # Restore the zsh buffer and cursor after running wd_browse
334 saved_buffer=$BUFFER
335 saved_cursor=$CURSOR
336 BUFFER=
337 zle redisplay
338 zle accept-line
339}
340
341wd_restore_buffer() {
342 if [[ -n $saved_buffer ]]; then
343 BUFFER=$saved_buffer
344 CURSOR=$saved_cursor
345 fi
346 saved_buffer=
347 saved_cursor=1
348}
349
350wd_list_all()
351{
352 wd_print_msg "$WD_BLUE" "All warp points:"
353
354 entries=$(sed "s:${HOME}:~:g" "$wd_config_file")
355
356 max_warp_point_length=0
357 while IFS= read -r line
358 do
359 arr=(${(s,:,)line})
360 key=${arr[1]}
361
362 length=${#key}
363 if [[ length -gt max_warp_point_length ]]
364 then
365 max_warp_point_length=$length
366 fi
367 done <<< "$entries"
368
369 while IFS= read -r line
370 do
371 if [[ $line != "" ]]
372 then
373 arr=(${(s,:,)line})
374 key=${arr[1]}
375 val=${line#"${arr[1]}:"}
376
377 if [[ -z $wd_quiet_mode ]]
378 then
379 printf "%${max_warp_point_length}s -> %s\n" "$key" "$val"
380 fi
381 fi
382 done <<< "$entries"
383}
384
385wd_ls()
386{
387 wd_getdir "$1"
388 ls "${dir/#\~/$HOME}"
389}
390
391wd_open()
392{
393 wd_getdir "$1"
394 if command -v open >/dev/null 2>&1; then
395 # MacOS, Ubuntu (alias)
396 open "${dir/#\~/$HOME}"
397 elif command -v xdg-open >/dev/null 2>&1; then
398 # Most Linux desktops
399 xdg-open "${dir/#\~/$HOME}"
400 else
401 echo "No known file opener found (need 'open' or 'xdg-open')." >&2
402 exit 1
403 fi
404}
405
406wd_path()
407{
408 wd_getdir "$1"
409 echo "$(echo "$dir" | sed "s:~:${HOME}:g")"
410}
411
412wd_show()
413{
414 local name_arg=$1
415 local show_pwd
416 # if there's an argument we look up the value
417 if [[ -n $name_arg ]]
418 then
419 if [[ -z $points[$name_arg] ]]
420 then
421 wd_print_msg "$WD_BLUE" "No warp point named $name_arg"
422 else
423 wd_print_msg "$WD_GREEN" "Warp point: ${WD_GREEN}$name_arg${WD_NOC} -> $points[$name_arg]"
424 fi
425 else
426 # hax to create a local empty array
427 local wd_matches
428 wd_matches=()
429 # do a reverse lookup to check whether PWD is in $points
430 show_pwd="${PWD/$HOME/~}"
431 if [[ ${points[(r)$show_pwd]} == "$show_pwd" ]]
432 then
433 for name in ${(k)points}
434 do
435 if [[ $points[$name] == "$show_pwd" ]]
436 then
437 wd_matches[$(($#wd_matches+1))]=$name
438 fi
439 done
440
441 wd_print_msg "$WD_BLUE" "$#wd_matches warp point(s) to current directory: ${WD_GREEN}$wd_matches${WD_NOC}"
442 else
443 wd_print_msg "$WD_YELLOW" "No warp point to $show_pwd"
444 fi
445 fi
446}
447
448wd_clean() {
449 local force=$1
450 local count=0
451 local wd_tmp=""
452
453 if [ ! -w "$wd_config_file" ]; then
454 wd_exit_fail "\'$wd_config_file\' is not writeable."
455 return
456 fi
457
458 while read -r line
459 do
460 if [[ $line != "" ]]
461 then
462 arr=(${(s,:,)line})
463 key=${arr[1]}
464 val=${arr[2]}
465
466 if [ -d "${val/#\~/$HOME}" ]
467 then
468 wd_tmp=$wd_tmp"\n"`echo "$line"`
469 else
470 wd_print_msg "$WD_YELLOW" "Nonexistent directory: ${key} -> ${val}"
471 count=$((count+1))
472 fi
473 fi
474 done < "$wd_config_file"
475
476 if [[ $count -eq 0 ]]
477 then
478 wd_print_msg "$WD_BLUE" "No warp points to clean, carry on!"
479 else
480 if [ ! -z "$force" ] || wd_yesorno "Removing ${count} warp points. Continue? (y/n)"
481 then
482 echo "$wd_tmp" >! "$wd_config_file"
483 wd_print_msg "$WD_GREEN" "Cleanup complete. ${count} warp point(s) removed"
484 else
485 wd_print_msg "$WD_BLUE" "Cleanup aborted"
486 fi
487 fi
488}
489
490wd_export_static_named_directories() {
491 if [[ ! -z $WD_EXPORT ]]
492 then
493 command grep '^[0-9a-zA-Z_-]\+:' "$wd_config_file" | sed -e "s,~,$HOME," -e 's/:/=/' | while read -r warpdir ; do
494 hash -d "$warpdir"
495 done
496 fi
497}
498
499WD_CONFIG=${WD_CONFIG:-$HOME/.warprc}
500local WD_QUIET=0
501local WD_EXIT_CODE=0
502
503# Parse 'meta' options first to avoid the need to have them before
504# other commands. The `-D` flag consumes recognized options so that
505# the actual command parsing won't be affected.
506
507zparseopts -D -E \
508 c:=wd_alt_config -config:=wd_alt_config \
509 q=wd_quiet_mode -quiet=wd_quiet_mode \
510 v=wd_print_version -version=wd_print_version \
511 f=wd_force_mode -force=wd_force_mode
512
513if [[ ! -z $wd_print_version ]]
514then
515 echo "wd version $WD_VERSION"
516fi
517
518# set the config file from variable or default
519typeset wd_config_file=${WD_CONFIG:-$HOME/.warprc}
520if [[ ! -z $wd_alt_config ]]
521then
522 # prefer the flag if provided
523 wd_config_file=$wd_alt_config[2]
524fi
525
526# check if config file exists
527if [ ! -e "$wd_config_file" ]
528then
529 # if not, create config file
530 touch "$wd_config_file"
531else
532 wd_export_static_named_directories
533fi
534
535# disable extendedglob for the complete wd execution time
536setopt | grep -q extendedglob
537wd_extglob_is_set=$?
538if (( wd_extglob_is_set == 0 )); then
539 setopt noextendedglob
540fi
541
542# load warp points
543typeset -A points
544while read -r line
545do
546 arr=(${(s,:,)line})
547 key=${arr[1]}
548 # join the rest, in case the path contains colons
549 val=${(j,:,)arr[2,-1]}
550
551 points[$key]=$val
552done < "$wd_config_file"
553
554# get opts
555args=$(getopt -o a:r:c:lhs -l add:,rm:,clean,list,ls:,open:,path:,help,show -- $*)
556
557# check if no arguments were given, and that version is not set
558if [[ ($? -ne 0 || $#* -eq 0) && -z $wd_print_version ]]
559then
560 wd_print_usage
561else
562 # parse rest of options
563 local wd_o
564 for wd_o
565 do
566 case "$wd_o"
567 in
568 "-a"|"--add"|"add")
569 wd_add "$2" "$wd_force_mode"
570 break
571 ;;
572 "-b"|"browse")
573 wd_browse
574 break
575 ;;
576 "-c"|"--addcd"|"addcd")
577 wd_addcd "$2" "$3" "$wd_force_mode"
578 break
579 ;;
580 "-e"|"export")
581 wd_export_static_named_directories
582 break
583 ;;
584 "-r"|"--remove"|"rm")
585 # Passes all the arguments as a single string separated by whitespace to wd_remove
586 wd_remove "${@:2}"
587 break
588 ;;
589 "-l"|"list")
590 wd_list_all
591 break
592 ;;
593 "-ls"|"ls")
594 wd_ls "$2"
595 break
596 ;;
597 "-o"|"--open"|"open")
598 wd_open "$2"
599 break
600 ;;
601 "-p"|"--path"|"path")
602 wd_path "$2"
603 break
604 ;;
605 "-h"|"--help"|"help")
606 wd_print_usage
607 break
608 ;;
609 "-s"|"--show"|"show")
610 wd_show "$2"
611 break
612 ;;
613 "-c"|"--clean"|"clean")
614 wd_clean "$wd_force_mode"
615 break
616 ;;
617 *)
618 wd_warp "$wd_o" "$2"
619 break
620 ;;
621 --)
622 break
623 ;;
624 esac
625 done
626fi
627
628## garbage collection
629# if not, next time warp will pick up variables from this run
630# remember, there's no sub shell
631
632if (( wd_extglob_is_set == 0 )); then
633 setopt extendedglob
634fi
635
636unset wd_extglob_is_set
637unset wd_warp
638unset wd_add
639unset wd_addcd
640unset wd_remove
641unset wd_show
642unset wd_list_all
643unset wd_print_msg
644unset wd_yesorno
645unset wd_print_usage
646unset wd_alt_config
647#unset wd_config_file do not unset this - breaks keybind
648unset wd_quiet_mode
649unset wd_print_version
650unset wd_force_mode
651unset wd_export_static_named_directories
652unset wd_o
653
654unset args
655unset points
656unset val &> /dev/null # fixes issue #1
657
658return $WD_EXIT_CODE