prompt-choose.bash

#!/usr/bin/env bash

# Use like:
#  prompt_choose choice 
#      "# header" \
#      "'message" \
#      "choice_1" "shorthand" "choice 1 description" \
#      "choice_2" "shorthand 2" "choice 2 description" \
#  ;
#  echo "${choice}"
#
#  prompt_choose returns true if choice successful, false otherwise. 
#  Allows usage like:
#  prompt_choose cmd ... && $cmd;
#  To choose a command from the list & execute it OR do nothing
#
#  Shorthands are optional. If omitting, use empty string: "choice" "" "description"
function prompt_choose(){
    ## INIT variables
    local -n output=$1
    output=""
    declare -a functions;
    # To make it a base-1 array
    functions+=("")
    declare -A shorthands;

    entryCount=0
    shorthandCount=0

    status=$open
    stOpen=0
    stFunction=1
    stShorthand=2

    print_prompt_chooser "${@}"

    answer="$prompt_answer"
    if [[ -n "$answer" ]];then
        derive_single_answer
    fi

    if [[ -z "$answer" ]];then
        perform_prompt || return 1;

        derive_single_answer || return 1;
    fi

    # set output & return true
    output="${answerKey}"
    return 0;
}

## for prompt_choose
function perform_prompt(){
    # prompt for choice
    msg
    msg_instruct "(q-quit)"
    $has_shorthand \
        && prompt "Type the [shorthand]\n or choose (1-${entryCount}):" answer \
        || prompt "Choose (1-${entryCount}):" answer
    prompt_exited "$answer" -p && return 1;

    return 0
}

## for prompt_choose
## This is the function for which the MANY passed arguments are used
function print_prompt_chooser(){
    # Print entries & headers
    for str in "${@:2}"; do
        charOne=${str:0:1}
        if [[ $status -eq $stOpen && ${charOne} == "#" ]]; then
            # display header
            msg
            header "${str:1}"
        elif [[ $status -eq $stOpen && ${charOne} == "'" ]]; then
            # display header
            msg "  ${str:1}"
        elif [[ $status -eq $stOpen ]]; then
            # store functions
            entryCount=$(($entryCount + 1))
            functions+=("${str}")
            status=$stFunction
        elif [[ $status -eq $stFunction ]]; then
            # store shorthands
            if [[ -z "$str" ]];then
                has_shorthand=false
            else
                has_shorthand=true
                prevShorthand="$str"
                shorthandCount+=1;
                shorthands["$str"]="$entryCount"
            fi
            status=$stShorthand
        elif [[ $status -eq $stShorthand ]]; then
            # display entry
            shStr=""
            if $has_shorthand;then
                d=$(($entryCount + 1))
                shStr="$(color_li $d)[$prevShorthand]${cOff} "
            fi
            msg "  $(color_li $entryCount)${entryCount}.${cOff} ${shStr}${str}"
            status=$stOpen
        fi
    done


    has_shorthand=false;
    if [[ $shorthandCount -gt 0 ]];then
        has_shorthand=true
    fi
}


## for prompt_choose
function derive_single_answer(){
    # Determine function from answer
    answerIndex="${shorthands[${answer}]}"
    if [[ -z "$answerIndex" ]]; then
        answerIndex="$answer"
    fi
    answerKey="${functions["${answerIndex}"]}"

    # Display wrong answer message
    if [[ -z "${answerKey}" ]];then
        shMsg=""
        if $has_shorthand;then
            shMsg="\n   OR a [shorthand] (without the brackets)"
        fi
        msg_mistake "A number 1-${entryCount}${shMsg} is required.\nYou entered " \
            "${answer}" \
        ;
        return 1;
    fi;

    return 0;

}

# Same usage as prompt_choose 
function prompt_choose_multi(){
    ## INIT variables
    local -n answerList="$1"

    ##### THESE declares & initial values (below) are a copy+paste from prompt_choose() (single)
    declare -a functions;
    # To make it a base-1 array
    functions+=("")
    declare -A shorthands;

    entryCount=0
    shorthandCount=0

    status=$open
    stOpen=0
    stFunction=1
    stShorthand=2
    ##### END COPY+PASTE



    print_prompt_chooser "${@}"
    
    # prompt for choice
    msg
    msg_instruct "(q-quit)"
    # $has_shorthand \
        # && prompt "range (1-3) and/or separate items(1,shorthand). Use [shorthand] or #." multiAnswer \
        # || prompt "range (1-3) and/or separate items(1,4))" multiAnswer
    # prompt_exited "$multiAnswer" -p && return 1;
    $has_shorthand \
        && prompt "(1-3,shorthand-9)" multiAnswer \
        || prompt "(1-3,shorthand-9)" multiAnswer
    prompt_exited "$multiAnswer" -p && return 1;


    derive_multi_answer

}

# Called from prompt_choose_multi
function derive_multi_answer(){
    range_start=""
    range_end=""
    buffer=""
    for (( i=0; i<${#multiAnswer}; i++ )); do
        char="${multiAnswer:$i:1}"
        ## shorthands cannot contain '-', so this will be a problem for some file names
        if [[ "$char" == "-" ]];then
            # Current buffer starts a selection range
            range_start="$buffer"
            buffer=""
            continue
        elif [[ "$char" == "," ]];then
            if [[ -n "$range_start" ]];then
                # We've already matched a '-'
                range_end="$buffer"
                buffer=""
                get_current_range all_answers
                answerList+=("${all_answers[@]}")
                range_start=""
                range_end=""
            elif [[ -n "$buffer" ]];then
                # We have NOT matched a '-'
                range_start="$buffer"
                range_end="$range_start"
                get_current_range all_answers
                answerList+=("${all_answers[@]}")
                buffer=""
                range_start=""
                range_end=""
            else
                echo "improper syntax"
            fi

            continue
        fi
        buffer+="$char"
    done

    if [[ -z "$range_start" ]];then
        ## The loop has ended & we didn't hit any stopchars [-,]
        range_start="$buffer"
    fi
    range_end="$buffer"
    buffer=""
    get_current_range all_answers
    answerList+=("${all_answers[@]}")
    
}


## parse a user's answer from prompt_multi_choose
# Handles 1-3,6,7,9-12
# doesn't check for duplicates
# can use shorthands for range_start-range_end,and_indices
function get_current_range(){
    local -n current_range="$1"
    current_range=()

    ### copy vars from parent prompt_choose_multi() function 
        ## functions needs renamed in the prompt_choose() 
    local -n -r indices="functions"
    local -n -r keys="shorthands"
    ### Prevent leakage
    local i
    local start
    local end
    local startindex
    local endIndex

    # Pulling from parent scope
    start="$range_start"
    end="$range_end"

    # Check if answer is a string key & find int index associated with that key
    startIndex="${keys["${start}"]}"
    if [[ -z "$startIndex" ]];then
        if [[ -z "${indices["${start}"]}" ]];then
            msg_notice "${start} not found"
            return;
        else
            startIndex="$start"
        fi
    fi
    # Repeat check for end index
    endIndex="${keys["${end}"]}"
    if [[ -z "$endIndex" ]];then
        if [[ -z "${indices["${end}"]}" ]];then
            msg_notice "${end} not found"
            return;
        else
            endIndex="$end"
        fi
    fi

    # Loop over values & add them to the range
    i=$startIndex
    while [[ $i -le $endIndex ]]; do
        current_range+=("${indices["$i"]}")   
        i=$((i+1))
    done

}

function choose_files_recursively(){
    
    ## to avoid naming conflicts AND have descendence into recursive calls
    recurseDirRoot="${recurseDirRoot:-"${1}"}"
    dir="${1}"
    dir="${dir%%/}"

    # echo "Root: ${recurseDirRoot}"
    # echo "Search in: ${dir}"


    if [[ ! -d "$dir" ]];then
        msg_notice "${dir} is not a directory"
        return
    fi

    files=()
    dirs=()
    shopt -s dotglob
    for file in "$dir/"*; do
        echo "${file}"
        if [[ -d "$file" ]];then
            dirs+=("$file")
        else
            files+=("$file")
        fi
    done

    # return

### This seems to have a comprehensive sorting function
## https://stackoverflow.com/questions/7442417/how-to-sort-an-array-in-bash
### But I'm not going to integrate that now.

    relDir="${dir##"$recurseDirRoot"}/"
    menu=("# Choose Files and Dirs")
    menu+=("'${cInstruct}${relDir}${cOff}")
    for dir in "${dirs[@]}";do
        name="$(basename "$dir")"
        menu+=("$dir" "$name/" "")
    done
    for file in "${files[@]}";do
        name="$(basename "$file")"
        menu+=("$file" "$name" "")
    done

    pathList=()

    msg_header "${dir}"
    prompt_choose_multi pathList "${menu[@]}"

    i=0
    srcFiles=()
    destFiles=()
    projectDir="$(project_dir)"
    msg
    msg_instruct "Enter destination,${cOff} relative to project root\n${cCommand}(s-skip file)${cOff}"
    local path
    for path in "${pathList[@]}";do
        dir="$(basename "$(dirname "$path")")"
        base="$(basename "$path")"
        if [[ -f "$path" ]]; then

            fileList+=("$path")
            # dirList+=("${projectDir}/${dest}")
        elif [[ -d "$path" ]];then
            echo "Try ${path}"
            choose_files_recursively "$path"
        fi
    done
}