Skip to content

Instantly share code, notes, and snippets.

@UndeadDemidov
Forked from jacksluong/cdp.md
Created September 4, 2024 07:18
Show Gist options
  • Save UndeadDemidov/8dfcd27d05fc4a75563ca86939b6ff8f to your computer and use it in GitHub Desktop.
Save UndeadDemidov/8dfcd27d05fc4a75563ca86939b6ff8f to your computer and use it in GitHub Desktop.

Revisions

  1. @jacksluong jacksluong revised this gist Dec 23, 2023. 2 changed files with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    # cdp: Change Directory Pro
    ![recording](https://gist.github.com/jacksluong/744ee3e30f6fc05a5563353e6db28aca/raw/3d79923e200ab5b133730cf2d9f82efbadbd4631/recording.gif)
    ![recording](https://gist.github.com/assets/27376487/0819e597-43e7-4742-afb9-5eab9cd19f74)
    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily. Compatible with bash and zsh.
    Binary file removed recording.gif
    Binary file not shown.
  2. @jacksluong jacksluong revised this gist Dec 23, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    # cdp: Change Directory Pro
    (recording)[recording.gif]
    ![recording](https://gist.github.com/jacksluong/744ee3e30f6fc05a5563353e6db28aca/raw/3d79923e200ab5b133730cf2d9f82efbadbd4631/recording.gif)
    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily. Compatible with bash and zsh.
  3. @jacksluong jacksluong revised this gist Dec 23, 2023. 3 changed files with 1 addition and 0 deletions.
    Binary file removed _recording.gif
    Binary file not shown.
    1 change: 1 addition & 0 deletions cdp.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,5 @@
    # cdp: Change Directory Pro
    (recording)[recording.gif]
    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily. Compatible with bash and zsh.
    Binary file added recording.gif
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  4. @jacksluong jacksluong revised this gist Dec 23, 2023. 3 changed files with 257 additions and 112 deletions.
    Binary file added _recording.gif
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    20 changes: 9 additions & 11 deletions cdp.md
    Original file line number Diff line number Diff line change
    @@ -1,25 +1,23 @@
    # cdp: Change Directory Pro
    ![recording](https://user-images.githubusercontent.com/27376487/188341254-75583362-0614-4475-a6dd-309b2592d1ea.gif)

    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily.
    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily. Compatible with bash and zsh.

    ## Controls
    - `cdp` to activate
    - ↑/↓ to change the selected subdirectory
    - ← to move into the parent subdirectory (disallowed when in $HOME)
    - → to make the selected subdirectory the active one (i.e. `cd` into it)
    - ↵ (return) to exit navigation in the new active directory
    - ⎵ (space) to make $HOME the active directory
    - [0-9] to make active the subdirectory is listed next to that number
    - Ctrl+C to cancel navigation, staying in the initial directory
    - Any characters (except backslash) to search for a specific substring

    ## Installation
    Just copy the code below into your .zshrc! (Alternatively, place it in a different file and source that file in .zshrc to keep it from getting bloated.)

    Note 1: Only works for Zsh (not Bash).

    Note 2: If the formatting of the code below looks ugly for whatever reason, reload the page.
    Download `cdp.sh` and place it wherever you want. You can source this file in your `.{zsh|bash}rc`, or you can directly copy the contents in. The following commands can help with that (replace `.zshrc` with `.bashrc` as appropriate):

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
    ```zsh
    # source file from .zshrc
    echo "source <path-to-file>" >> ~/.zshrc
    # copy into .zshrc
    tail -n +3 <path-to-file> >> ~/.zshrc
    ```
    349 changes: 248 additions & 101 deletions cdp.sh
    Original file line number Diff line number Diff line change
    @@ -1,102 +1,249 @@
    #!/bin/bash

    # `cdp` to activate an interactive way to navigate directories
    function cdp {
    # helpers for terminal print control, key input, and other things
    ESC=$( printf "\033" )
    cursor_blink_on() { printf "${ESC}[?25h"; }
    cursor_blink_off() { printf "${ESC}[?25l"; }
    cursor_to() { printf "${ESC}[$1;${2:-1}H"; }
    print_option() { printf " $1 $(tput el)"; }
    print_selected() { printf "${ESC}[7m $1 ${ESC}[27m$(tput el)"; }
    get_cursor_row() { IFS=';' read -sdR $'?\E[6n' ROW COL; echo ${ROW#*\[}; }

    key_input() { read -sk1 'key?'
    case $key in
    $'\n') echo enter;;
    ' ') echo space;;
    0) echo 10;;
    1|2|3|4|5|6|7|8|9) echo $key;;
    "${ESC}")
    read -sk2 key
    if [[ $key = "[A" ]]; then echo up
    elif [[ $key = "[B" ]]; then echo down
    elif [[ $key = "[C" ]]; then echo right
    elif [[ $key = "[D" ]]; then echo left
    fi;;
    esac }
    min() { printf "%s\n" "$@" | sort -g | head -n1 }
    max() { printf "%s\n" "$@" | sort -gr | head -n1 }


    # initialize persistent variables
    MAX_SIZE=10
    local prev_dir=''
    local orig_dir=$PWD

    while true; do
    # determine the options
    local subdirs=$((ls -1d */) 2> /dev/null)
    local opts=("$(tput smul)Change directory to $(tput bold)$(pwd)$(tput sgr0)$(tput el)")
    [[ ! -z $subdirs ]] && opts+=("${(f)subdirs}")
    opts+=('../')
    local num_opts=${#opts[@]}
    # initialize interaction area and screen positions of first and last row (used for overwriting)
    # note: indices are all 1-indexed with row 1 being the line displaying the working directory
    local num_opt_rows=$(min $(tput lines) $MAX_SIZE $((num_opts - 1)))
    for _ in {1..$num_opt_rows}; do echo; done
    local last_row=$(get_cursor_row)
    local first_row=$((last_row - num_opt_rows))
    cursor_to $first_row
    echo " $opts[1]"

    # ensure cursor and input echoing back on upon a ctrl+c during read
    trap "cd $orig_dir; cursor_blink_on; stty echo; printf '\n'; return" 2
    cursor_blink_off

    # jump to previous dir option (if left arrow key was pressed)
    local selected=2
    for i in {2..$#opts}; do [[ $opts[$i] = $prev_dir ]] && selected=$i; done
    local inp=''

    local top_row=$(max $((selected - num_opt_rows + 2)) 2)
    while true; do
    # determine "scroll" position
    if [[ $selected -lt $top_row ]]; then top_row=$selected
    elif [[ $selected -ge $((top_row + num_opt_rows)) ]]; then top_row=$((selected - num_opt_rows + 1)); fi

    # print options by overwriting the last lines
    for opt_index in {$top_row..$((top_row + num_opt_rows - 1))}; do
    cursor_to $((first_row + opt_index - top_row + 1))
    printf " [%i] " $(((opt_index - top_row + 1) % 10))
    if [[ $opt_index -eq $selected ]]; then print_selected $opts[$((opt_index))]
    else print_option $opts[$((opt_index))]; fi
    done

    # user key control
    inp=$(key_input)
    case $inp in
    left) [[ $PWD != $HOME ]] && break;;
    up) ((selected--)); [[ $selected -lt 2 ]] && selected=$num_opts;;
    down) ((selected++)); [[ $selected -gt $num_opts ]] && selected=2;;
    right|enter|space|1|2|3|4|5|6|7|8|9|10) break;;
    esac
    done

    # reset interaction area
    cursor_to $first_row
    for row_offset in {1..$num_opt_rows}; do cursor_to $((first_row + row_offset)); echo -n $(tput el); done
    cursor_to $first_row

    # cd accordingly
    case $inp in
    right) cd $opts[$selected]; prev_dir='';;
    enter) break;;
    space) cd; prev_dir='';;
    left) prev_dir=$(printf '%s/' "${PWD##*/}"); cd ..;;
    *) local dest=$opts[$((top_row + inp - 1))]
    [[ $dest == '../' ]] && prev_dir=$(printf '%s/' "${PWD##*/}")
    cd $dest;;
    esac
    done
    cursor_blink_on
    }
    # note: for consistency, treat all arrays as 0-indexed
    cdp() {
    local MAX_VOPTS=8 MAX_DIR_WIDTH=60 SEARCH_ICON=""
    local SEARCH_PREFIX=" $(tput bold)${SEARCH_ICON}$(tput sgr0)$(tput el) "

    local is_bash=$([[ -n $BASH ]] && echo true || echo false)
    local is_zsh=$([[ -n $ZSH_NAME ]] && echo true || echo false)

    # helper functions
    translate_input() {
    # args: input
    case $1 in
    $'\n'|'') echo enter;;
    $'\177'|$'\b') echo backspace;;
    "[A") echo up;;
    "[B") echo down;;
    "[C") echo right;;
    "[D") echo left;;
    *) echo "$1";;
    esac
    }
    zsh_key_input() {
    read -sk1 key
    [[ $key = $'\e' ]] && read -sk2 -t 0.1 key
    translate_input "$key"
    }
    bash_key_input() {
    IFS='' read -rsn1 key
    [[ $key = $'\e' ]] && read -rsn2 -t 1 key
    translate_input "$key"
    }

    index_array() {
    # args: index, array
    local i=$1
    shift 1
    echo "${@:$((i+1)):1}" # only way for array indexing to work for both bash and zsh
    # ${@:0:1} will return the function name
    }
    index_of() {
    # args: element, array
    local e=$1
    shift 1

    local i=0
    for s in "$@"; do
    if [[ $s = "$e" ]]; then
    echo $i
    return
    fi
    ((i++))
    done
    echo -1
    }

    cursor_to_first_option() {
    tput rc; tput cud1; tput cud1
    }
    print_option() {
    echo " $1 $(tput el)"
    }
    print_selected() {
    echo " $(tput setab 7)$(tput setaf 0) $1 $(tput sgr0)$(tput el)"
    }

    render_options() {
    # precondition: cursor is at the first line of the options
    # args: selected index (of visible options), all visible options (as array)
    local selected=$1
    shift 1
    local options=("$@")

    local i=0
    for s in "${options[@]}"; do
    if [[ $i -eq $selected ]]; then print_selected "$s"; else print_option "$s"; fi
    ((i++))
    done
    }

    render_search_string() {
    # args: search string
    # postcondition: cursor is at the first line of the options
    tput rc; tput cud1; tput cuf 4
    echo "$(tput setaf 3)${search_str}$(tput el)$(tput sgr0)_"
    }

    render_heading() {
    # args: none
    tput rc
    local pwd_str=$(pwd)
    local lim_width=$(($(tput cols) - 20 - 5)) # 20 for "Change directory to ", 5 for buffer
    [[ lim_width -gt MAX_DIR_WIDTH ]] && lim_width=$MAX_DIR_WIDTH
    [[ ${#pwd_str} -gt $lim_width ]] && pwd_str="...${pwd_str:$((${#pwd_str} - lim_width + 3))}"
    echo "$(tput smul)Change directory to $(tput bold)${pwd_str}$(tput sgr0)$(tput el)"
    }

    # initialize variables
    local search_str='' prev_dir='' orig_dir=$PWD
    local num_vopts=$(($(tput lines) - 2 - 1)) # 2 fixed lines, 1 for buffer
    [[ $num_vopts -gt $MAX_VOPTS ]] && num_vopts=$MAX_VOPTS

    local regex_chars='$^.?+*(){}[]/'
    escape_regex() {
    # args: string
    local str=$1 c=''
    for ((i=0; i<${#regex_chars}; i++)); do
    c=${regex_chars:$i:1}
    str=${str//"$c"/\\$c}
    done
    echo "$str"
    }

    # initialize interface
    tput civis
    stty -echo
    tpuc sc && render_heading
    echo -e "$SEARCH_PREFIX"
    for ((i=0; i<num_vopts; i++)); do echo; done
    tput cuu $((num_vopts + 2))
    tput sc

    # expected output:
    # - directory line (fixed line)
    # - search string line (fixed line)
    # - options (opts, each on their own line)
    # - blank lines to fill up the rest of the screen as needed

    # cleanup functions
    cleanup_base() {
    tput rc
    tput ed
    tput cnorm
    stty echo
    trap - INT
    }

    cleanup_exit() {
    cleanup_base
    echo "Working directory changed: $(tput bold)$(pwd)$(tput el)$(tput sgr0)"
    }

    cleanup_interrupt() {
    cleanup_base
    cd $orig_dir
    echo "Restored working directory: $(tput bold)$(pwd)$(tput el)$(tput sgr0)"
    return
    }

    trap 'cleanup_interrupt; return' INT

    # main loop
    # opts: options, fopts: filtered options, vopts: visible options
    while true; do
    # determine the options
    local fopts=() opts=()
    local subdirs=$( (ls -F | grep /$ | sort -f) )
    if [[ -n $subdirs ]]; then
    [[ $is_bash = true ]] && IFS=$'\n' read -r -d '' -a opts <<< "$subdirs"
    [[ $is_zsh = true ]] && opts+=("${(f)subdirs}")
    fi
    opts+=('../')

    # jump to previous dir option (if left arrow key was pressed)
    local sel=0
    local prev_dir_index=$(index_of "$prev_dir" "${opts[@]}")
    [[ $prev_dir_index -ne -1 ]] && sel=$prev_dir_index;
    local inp=''

    local first_vopt=$((sel - num_vopts + 1))
    [[ $first_vopt -lt 0 ]] && first_vopt=0
    local do_rerender_opts=true did_update_search=true

    render_heading

    # loop while still in the same directory
    while true; do
    if [[ $did_update_search = true ]]; then
    render_search_string "$search_str"
    # filter options by search string
    fopts=()
    if [[ -z $search_str ]]; then
    fopts=("${opts[@]}")
    else
    for opt in "${opts[@]}"; do
    local opt_l="" regex=""
    if [[ $is_bash = true ]]; then
    opt_l=$(echo "$opt" | tr '[:upper:]' '[:lower:]')
    regex=$(echo "$search_str" | tr '[:upper:]' '[:lower:]')
    else
    opt_l=${opt:l}
    regex=${search_str:l}
    fi
    regex=$(escape_regex "$regex")
    [[ "$opt_l" =~ $regex ]] && fopts+=("$opt")
    done
    fi
    [[ "${#fopts[@]}" -eq 0 ]] && fopts+=('../')
    local num_fopts=${#fopts[@]}
    else
    cursor_to_first_option
    fi
    did_update_search=false

    if [[ $do_rerender_opts = true ]]; then
    # determine "scroll" position
    if [[ $sel -lt $first_vopt ]]; then first_vopt=$sel
    elif [[ $sel -ge $((first_vopt + num_vopts)) ]]; then first_vopt=$((sel - num_vopts + 1)); fi

    # print options
    vopts=( "${fopts[@]:$first_vopt:$num_vopts}" )
    render_options $((sel - first_vopt)) "${vopts[@]}"
    tput ed
    fi
    do_rerender_opts=true

    # user key control
    [[ $is_bash = true ]] && inp=$(bash_key_input) || inp=$(zsh_key_input)
    case $inp in
    left) [[ $PWD != "$HOME" ]] && break;;
    up) ((sel--)); [[ $sel -lt 0 ]] && sel=$((num_fopts - 1));;
    down) ((sel++)); [[ $sel -ge $num_fopts ]] && sel=0;;
    right|enter) break;;
    '\') do_rerender_opts=false;;
    backspace)
    search_str="${search_str%?}"
    sel=0
    did_update_search=true;;
    *)
    search_str+=$inp
    sel=0
    did_update_search=true;;
    esac
    done

    # cd accordingly
    case $inp in
    right) cd "$(index_array "$sel" "${fopts[@]}")"; prev_dir='';;
    left) prev_dir=$(printf '%s/' "${PWD##*/}"); cd ..;;
    enter) break;;
    esac

    search_str=''
    done

    cleanup_exit
    }
  5. @jacksluong jacksluong revised this gist Oct 6, 2023. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -16,9 +16,10 @@ How do you navigate in your terminal? Do you chain sequences of `cd` commands to
    - Ctrl+C to cancel navigation, staying in the initial directory

    ## Installation
    Just copy the code below into your .zshrc! (Alternatively, place it in a different file and source that file in .zshrc to keep those files from getting bloated.)
    Just copy the code below into your .zshrc! (Alternatively, place it in a different file and source that file in .zshrc to keep it from getting bloated.)

    Note 1: Only works for Zsh (not Bash).

    Note 2: If the formatting of the code below looks ugly for whatever reason, reload the page.

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
  6. @jacksluong jacksluong revised this gist Dec 27, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -18,7 +18,7 @@ How do you navigate in your terminal? Do you chain sequences of `cd` commands to
    ## Installation
    Just copy the code below into your .zshrc! (Alternatively, place it in a different file and source that file in .zshrc to keep those files from getting bloated.)

    Note 1: Not yet tested for Bash or PowerShell.
    Note 1: Only works for Zsh (not Bash).
    Note 2: If the formatting of the code below looks ugly for whatever reason, reload the page.

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
  7. @jacksluong jacksluong revised this gist Sep 7, 2022. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -18,6 +18,7 @@ How do you navigate in your terminal? Do you chain sequences of `cd` commands to
    ## Installation
    Just copy the code below into your .zshrc! (Alternatively, place it in a different file and source that file in .zshrc to keep those files from getting bloated.)

    Not yet tested for Bash or PowerShell.
    Note 1: Not yet tested for Bash or PowerShell.
    Note 2: If the formatting of the code below looks ugly for whatever reason, reload the page.

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
  8. @jacksluong jacksluong revised this gist Sep 5, 2022. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -16,6 +16,8 @@ How do you navigate in your terminal? Do you chain sequences of `cd` commands to
    - Ctrl+C to cancel navigation, staying in the initial directory

    ## Installation
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)! (Alternatively, place it in a different file and source that file in .zshrc/.profile to keep those files from getting bloated.)
    Just copy the code below into your .zshrc! (Alternatively, place it in a different file and source that file in .zshrc to keep those files from getting bloated.)

    Not yet tested for Bash or PowerShell.

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
  9. @jacksluong jacksluong revised this gist Sep 5, 2022. 2 changed files with 1 addition and 4 deletions.
    2 changes: 1 addition & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -16,6 +16,6 @@ How do you navigate in your terminal? Do you chain sequences of `cd` commands to
    - Ctrl+C to cancel navigation, staying in the initial directory

    ## Installation
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)!
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)! (Alternatively, place it in a different file and source that file in .zshrc/.profile to keep those files from getting bloated.)

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
    3 changes: 0 additions & 3 deletions cdp.sh
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,5 @@
    # `cdp` to activate an interactive way to navigate directories
    function cdp {
    # derived from: https://unix.stackexchange.com/a/415155 (converted to zsh)
    # tput usage: https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux/20983251#20983251

    # helpers for terminal print control, key input, and other things
    ESC=$( printf "\033" )
    cursor_blink_on() { printf "${ESC}[?25h"; }
  10. @jacksluong jacksluong revised this gist Sep 5, 2022. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion cdp.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    # cdp: Change Directory Pro
    ![recording](https://user-images.githubusercontent.com/27376487/188336663-8879a54d-4816-4b0c-b887-9c6f256dfa9e.gif)
    ![recording](https://user-images.githubusercontent.com/27376487/188341254-75583362-0614-4475-a6dd-309b2592d1ea.gif)

    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

  11. @jacksluong jacksluong revised this gist Sep 5, 2022. 1 changed file with 3 additions and 5 deletions.
    8 changes: 3 additions & 5 deletions cdp.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,6 @@
    # cdp: Change Directory Pro
    ![recording](https://user-images.githubusercontent.com/27376487/188336663-8879a54d-4816-4b0c-b887-9c6f256dfa9e.gif)

    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily.
    @@ -16,8 +18,4 @@ How do you navigate in your terminal? Do you chain sequences of `cd` commands to
    ## Installation
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)!

    ## Demo
    ![recording](https://user-images.githubusercontent.com/27376487/188336663-8879a54d-4816-4b0c-b887-9c6f256dfa9e.gif)

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)!
    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
  12. @jacksluong jacksluong created this gist Sep 5, 2022.
    23 changes: 23 additions & 0 deletions cdp.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    # cdp: Change Directory Pro
    How do you navigate in your terminal? Do you chain sequences of `cd` commands together, or copy a file path and paste it? Sometimes to a destination directory that's not adjacent to your current one?

    `cdp` might be the solution for you. It's a command that activates an interactive way to navigate directories smoothly and easily.

    ## Controls
    - `cdp` to activate
    - ↑/↓ to change the selected subdirectory
    - ← to move into the parent subdirectory (disallowed when in $HOME)
    - → to make the selected subdirectory the active one (i.e. `cd` into it)
    - ↵ (return) to exit navigation in the new active directory
    - ⎵ (space) to make $HOME the active directory
    - [0-9] to make active the subdirectory is listed next to that number
    - Ctrl+C to cancel navigation, staying in the initial directory

    ## Installation
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)!

    ## Demo
    ![recording](https://user-images.githubusercontent.com/27376487/188336663-8879a54d-4816-4b0c-b887-9c6f256dfa9e.gif)

    ###### Based on https://unix.stackexchange.com/questions/146570/arrow-key-enter-menu
    Just copy the code below into your .zshrc (Zsh) or .profile (Bash)!
    105 changes: 105 additions & 0 deletions cdp.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    # `cdp` to activate an interactive way to navigate directories
    function cdp {
    # derived from: https://unix.stackexchange.com/a/415155 (converted to zsh)
    # tput usage: https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux/20983251#20983251

    # helpers for terminal print control, key input, and other things
    ESC=$( printf "\033" )
    cursor_blink_on() { printf "${ESC}[?25h"; }
    cursor_blink_off() { printf "${ESC}[?25l"; }
    cursor_to() { printf "${ESC}[$1;${2:-1}H"; }
    print_option() { printf " $1 $(tput el)"; }
    print_selected() { printf "${ESC}[7m $1 ${ESC}[27m$(tput el)"; }
    get_cursor_row() { IFS=';' read -sdR $'?\E[6n' ROW COL; echo ${ROW#*\[}; }

    key_input() { read -sk1 'key?'
    case $key in
    $'\n') echo enter;;
    ' ') echo space;;
    0) echo 10;;
    1|2|3|4|5|6|7|8|9) echo $key;;
    "${ESC}")
    read -sk2 key
    if [[ $key = "[A" ]]; then echo up
    elif [[ $key = "[B" ]]; then echo down
    elif [[ $key = "[C" ]]; then echo right
    elif [[ $key = "[D" ]]; then echo left
    fi;;
    esac }
    min() { printf "%s\n" "$@" | sort -g | head -n1 }
    max() { printf "%s\n" "$@" | sort -gr | head -n1 }


    # initialize persistent variables
    MAX_SIZE=10
    local prev_dir=''
    local orig_dir=$PWD

    while true; do
    # determine the options
    local subdirs=$((ls -1d */) 2> /dev/null)
    local opts=("$(tput smul)Change directory to $(tput bold)$(pwd)$(tput sgr0)$(tput el)")
    [[ ! -z $subdirs ]] && opts+=("${(f)subdirs}")
    opts+=('../')
    local num_opts=${#opts[@]}
    # initialize interaction area and screen positions of first and last row (used for overwriting)
    # note: indices are all 1-indexed with row 1 being the line displaying the working directory
    local num_opt_rows=$(min $(tput lines) $MAX_SIZE $((num_opts - 1)))
    for _ in {1..$num_opt_rows}; do echo; done
    local last_row=$(get_cursor_row)
    local first_row=$((last_row - num_opt_rows))
    cursor_to $first_row
    echo " $opts[1]"

    # ensure cursor and input echoing back on upon a ctrl+c during read
    trap "cd $orig_dir; cursor_blink_on; stty echo; printf '\n'; return" 2
    cursor_blink_off

    # jump to previous dir option (if left arrow key was pressed)
    local selected=2
    for i in {2..$#opts}; do [[ $opts[$i] = $prev_dir ]] && selected=$i; done
    local inp=''

    local top_row=$(max $((selected - num_opt_rows + 2)) 2)
    while true; do
    # determine "scroll" position
    if [[ $selected -lt $top_row ]]; then top_row=$selected
    elif [[ $selected -ge $((top_row + num_opt_rows)) ]]; then top_row=$((selected - num_opt_rows + 1)); fi

    # print options by overwriting the last lines
    for opt_index in {$top_row..$((top_row + num_opt_rows - 1))}; do
    cursor_to $((first_row + opt_index - top_row + 1))
    printf " [%i] " $(((opt_index - top_row + 1) % 10))
    if [[ $opt_index -eq $selected ]]; then print_selected $opts[$((opt_index))]
    else print_option $opts[$((opt_index))]; fi
    done

    # user key control
    inp=$(key_input)
    case $inp in
    left) [[ $PWD != $HOME ]] && break;;
    up) ((selected--)); [[ $selected -lt 2 ]] && selected=$num_opts;;
    down) ((selected++)); [[ $selected -gt $num_opts ]] && selected=2;;
    right|enter|space|1|2|3|4|5|6|7|8|9|10) break;;
    esac
    done

    # reset interaction area
    cursor_to $first_row
    for row_offset in {1..$num_opt_rows}; do cursor_to $((first_row + row_offset)); echo -n $(tput el); done
    cursor_to $first_row

    # cd accordingly
    case $inp in
    right) cd $opts[$selected]; prev_dir='';;
    enter) break;;
    space) cd; prev_dir='';;
    left) prev_dir=$(printf '%s/' "${PWD##*/}"); cd ..;;
    *) local dest=$opts[$((top_row + inp - 1))]
    [[ $dest == '../' ]] && prev_dir=$(printf '%s/' "${PWD##*/}")
    cd $dest;;
    esac
    done
    cursor_blink_on
    }