diff --git a/README.md b/README.md index 06c9ddd..1bb524b 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,25 @@ Commands `n`: Next page. `p`: Previous page. `b`: Move selection back to parent directory. (For `sd` only.) -`Enter`: Navigate to selected directory (For `sd` only.) - +`Enter`: Navigate to selected directory (For `sd` only.) + +Customization +---- +You can customize the selection keys by editing the config file at `~/.config/shunpo/config`: + +```bash +# Selection keys (exactly 10 characters, one per menu item) +SHUNPO_SELECTION_KEYS="asdfghjkl;" +``` + +**Rules:** +- Must be exactly 10 characters (one for each menu item). +- Cannot contain `n`, `p`, or `b` (reserved for navigation). +- No duplicate characters allowed. +- Invalid configs fall back to the default `0123456789`. + +**Note:** CLI arguments (e.g., `sg 3`, `sj 1`) always use numeric indices 0-9, regardless of your custom keys. + Uninstalling ---- Run `uninstall.sh` diff --git a/install.sh b/install.sh index db655d9..ef8f433 100755 --- a/install.sh +++ b/install.sh @@ -10,6 +10,10 @@ BASHRC="$HOME/.bashrc" # File containing command definitions. SHUNPO_CMD="$INSTALL_DIR/shunpo_cmd" +# Config file path. +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/shunpo" +CONFIG_FILE="$CONFIG_DIR/config" + setup() { mkdir -p "$INSTALL_DIR" mkdir -p "$SCRIPT_DIR" @@ -39,7 +43,7 @@ install() { cp src/* "$SCRIPT_DIR" # Add sourcing for shunpo_cmd (overwrite). - source_rc_line="source $SHUNPO_CMD" + source_rc_line="source \"$SHUNPO_CMD\"" temp_file=$(mktemp) sed '/^source.*\shunpo_cmd/d' "$BASHRC" >"$temp_file" mv "$temp_file" "$BASHRC" @@ -55,6 +59,19 @@ install() { echo "Added to BASHRC: $install_dir_line" add_commands + + # Create default config file if it doesn't exist. + if [ ! -f "$CONFIG_FILE" ]; then + mkdir -p "$CONFIG_DIR" + cat >"$CONFIG_FILE" < "$SHUNPO_INIT" + cat > "$SHUNPO_INIT" <<'INIT_EOF' +# Create default config file if it doesn't exist. +SHUNPO_CONFIG_DIR=''${XDG_CONFIG_HOME:-$HOME/.config}/shunpo +SHUNPO_CONFIG_FILE=$SHUNPO_CONFIG_DIR/config +if [ ! -f "$SHUNPO_CONFIG_FILE" ]; then + mkdir -p "$SHUNPO_CONFIG_DIR" + cat >"$SHUNPO_CONFIG_FILE" <<'CONFIG_EOF' +# Shunpo Configuration +# Selection keys (exactly 10 characters, one per menu item) +# Reserved keys that cannot be used: n, p, b +# Note: CLI arguments (e.g., sg 3, sj 1) always use numeric indices 0-9 +SHUNPO_SELECTION_KEYS="0123456789" +CONFIG_EOF +fi +unset SHUNPO_CONFIG_DIR SHUNPO_CONFIG_FILE +INIT_EOF + echo "source $SHUNPO_CMD" >> "$SHUNPO_INIT" chmod +x $SHUNPO_INIT # not necessary, but keep for auto-complete. ''; diff --git a/src/functions.sh b/src/functions.sh index 3b4189f..08dc233 100644 --- a/src/functions.sh +++ b/src/functions.sh @@ -1,5 +1,65 @@ #!/usr/bin/env bash +# Configuration Path. +SHUNPO_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/shunpo" +SHUNPO_CONFIG_FILE="$SHUNPO_CONFIG_DIR/config" + +# Default selection keys (can be overridden in config). +SHUNPO_SELECTION_KEYS="0123456789" + +# Load and validate user configuration. +function shunpo_load_config() { + if [ -f "$SHUNPO_CONFIG_FILE" ]; then + local user_keys + # Source the config to get SHUNPO_SELECTION_KEYS + source "$SHUNPO_CONFIG_FILE" + user_keys="$SHUNPO_SELECTION_KEYS" + + # Validate: exactly 10 characters + if [ ${#user_keys} -ne 10 ]; then + echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Warning: SHUNPO_SELECTION_KEYS must be exactly 10 characters. Using defaults.${SHUNPO_RESET}" + SHUNPO_SELECTION_KEYS="0123456789" + return + fi + + # Validate: no reserved keys (n, p, b) + if [[ $user_keys == *n* ]] || [[ $user_keys == *p* ]] || [[ $user_keys == *b* ]]; then + echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Warning: SHUNPO_SELECTION_KEYS cannot contain 'n', 'p', or 'b'. Using defaults.${SHUNPO_RESET}" + SHUNPO_SELECTION_KEYS="0123456789" + return + fi + + # Validate: no duplicate characters + local seen="" + for ((i = 0; i < ${#user_keys}; i++)); do + local char="${user_keys:i:1}" + if [[ $seen == *"$char"* ]]; then + echo -e "${SHUNPO_BOLD}${SHUNPO_ORANGE}Warning: SHUNPO_SELECTION_KEYS cannot contain duplicates. Using defaults.${SHUNPO_RESET}" + SHUNPO_SELECTION_KEYS="0123456789" + return + fi + seen+="$char" + done + + SHUNPO_SELECTION_KEYS="$user_keys" + fi +} + +# Get the index (0-9) for a given key, or return 1 if not found. +function shunpo_get_key_index() { + local key="$1" + for ((i = 0; i < ${#SHUNPO_SELECTION_KEYS}; i++)); do + if [[ ${SHUNPO_SELECTION_KEYS:i:1} == "$key" ]]; then + echo "$i" + return 0 + fi + done + return 1 +} + +# Load configuration on source. +shunpo_load_config + # Default Bookmarks Path. SHUNPO_BOOKMARKS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/shunpo/" if [ ! -d "$SHUNPO_BOOKMARKS_DIR" ]; then @@ -80,7 +140,7 @@ function shunpo_interact_bookmarks() { # Display bookmarks for the current page. for ((i = start_index; i < end_index; i++)); do - echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] ${bookmarks[i]}" + echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}${SHUNPO_SELECTION_KEYS:$((i - start_index)):1}${SHUNPO_RESET}] ${bookmarks[i]}" done if [ $last_page -gt 1 ]; then @@ -101,9 +161,9 @@ function shunpo_interact_bookmarks() { fi shunpo_clear_output - elif [[ $input =~ ^[0-9]+$ ]] && [ "$input" -ge 0 ] && [ "$input" -lt $max_per_page ]; then + elif key_index=$(shunpo_get_key_index "$input"); then # Process bookmark selection input. - shunpo_selected_bookmark_index=$((current_page * max_per_page + input)) + shunpo_selected_bookmark_index=$((current_page * max_per_page + key_index)) if [[ $shunpo_selected_bookmark_index -lt $total_bookmarks ]]; then shunpo_selected_dir="${bookmarks[$shunpo_selected_bookmark_index]}" shunpo_clear_output @@ -201,9 +261,9 @@ function shunpo_jump_to_parent_dir() { # Display the current page of parent directories. for ((i = start_index; i < end_index; i++)); do if [[ $i -eq $((end_index - 1)) && $current_page -eq $((last_page - 1)) ]]; then - echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] $(basename "${parent_dirs[i]}")" + echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}${SHUNPO_SELECTION_KEYS:$((i - start_index)):1}${SHUNPO_RESET}] $(basename "${parent_dirs[i]}")" else - echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] /$(basename "${parent_dirs[i]}")" + echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}${SHUNPO_SELECTION_KEYS:$((i - start_index)):1}${SHUNPO_RESET}] /$(basename "${parent_dirs[i]}")" fi done @@ -225,17 +285,16 @@ function shunpo_jump_to_parent_dir() { fi shunpo_clear_output - elif [[ $input =~ ^[0-9]+$ ]] && [ "$input" -ge 0 ] && [ "$input" -lt "$max_per_page" ]; then - selected_index=$((start_index + input)) - if [[ $selected_index -lt $total_parents ]]; then + elif key_index=$(shunpo_get_key_index "$input"); then + selected_index=$((start_index + key_index)) + if [[ $selected_index -ge 0 ]] && [[ $selected_index -lt $total_parents ]]; then shunpo_clear_output tput cnorm cd "${parent_dirs[$selected_index]}" || return 1 echo -e "${SHUNPO_GREEN}${SHUNPO_BOLD}Changed to:${SHUNPO_RESET} ${parent_dirs[$selected_index]}" return 0 - else - shunpo_clear_output fi + shunpo_clear_output else shunpo_clear_output tput cnorm @@ -283,7 +342,8 @@ function shunpo_jump_to_child_dir() { # Attempt to retrieve from cache. if cache_index=$(shunpo_is_cached "$selected_path"); then # Use cached value. - IFS='|' read -r -a child_dirs <<<"${cache_values[$cache_index]}" + local IFS='|' + read -r -a child_dirs <<<"${cache_values[$cache_index]}" else # Collect directories if not cached. child_dirs=() @@ -345,7 +405,7 @@ function shunpo_jump_to_child_dir() { else # Print child directories. for ((i = start_index; i < end_index; i++)); do - echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}$((i - start_index))${SHUNPO_RESET}] ${child_dirs[i]#$selected_path}" + echo -e "[${SHUNPO_BOLD}${SHUNPO_ORANGE}${SHUNPO_SELECTION_KEYS:$((i - start_index)):1}${SHUNPO_RESET}] ${child_dirs[i]#"$selected_path"}" done if [ $last_page -gt 1 ]; then @@ -391,8 +451,8 @@ function shunpo_jump_to_child_dir() { fi break - elif [[ $input =~ ^[0-9]+$ ]] && [[ $input -ge 0 ]] && [[ $input -lt $max_per_page ]]; then - selected_index=$((start_index + input)) + elif key_index=$(shunpo_get_key_index "$input"); then + selected_index=$((start_index + key_index)) if [[ $selected_index -lt $total_child_dirs ]]; then selected_path="${child_dirs[selected_index]}" is_start_dir=0 @@ -446,11 +506,22 @@ function shunpo_assert_bookmarks_exist() { function shunpo_cleanup() { # Clean up to avoid namespace pollution. + # + # Restore terminal state. + [ -t 0 ] && tput cnorm 2>/dev/null + trap - SIGINT + + # Clean up variables. unset SHUNPO_BOOKMARKS_FILE unset SHUNPO_BOOKMARKS_DIR - unset IFS + unset SHUNPO_CONFIG_DIR + unset SHUNPO_CONFIG_FILE + unset SHUNPO_SELECTION_KEYS + IFS=$' \t\n' # Restore IFS to default. unset shunpo_selected_dir unset shunpo_selected_bookmark_index + + # Clean up functions. unset -f shunpo_interact_bookmarks unset -f shunpo_add_space unset -f shunpo_clear_output @@ -459,8 +530,7 @@ function shunpo_cleanup() { unset -f shunpo_jump_to_child_dir unset -f shunpo_is_cached unset -f shunpo_handle_kill + unset -f shunpo_load_config + unset -f shunpo_get_key_index unset -f shunpo_cleanup - tput cnorm - stty echo - trap - SIGINT } diff --git a/tests/common.sh b/tests/common.sh index 5c026e8..f02fd6e 100755 --- a/tests/common.sh +++ b/tests/common.sh @@ -8,22 +8,24 @@ fi function setup_env { HOME=${SHUNPO_TEST_DIR}/home - mkdir -p $HOME + mkdir -p "$HOME" XDG_DATA_HOME=${SHUNPO_TEST_DIR}/home/.local/share - mkdir -p $XDG_DATA_HOME + mkdir -p "$XDG_DATA_HOME" + XDG_CONFIG_HOME=${SHUNPO_TEST_DIR}/home/.config + mkdir -p "$XDG_CONFIG_HOME" } function cleanup_env { - rm $SHUNPO_TEST_DIR/home/.bashrc - rm $SHUNPO_TEST_DIR/home/.bashrc$ + rm "$SHUNPO_TEST_DIR/home/.bashrc" + rm "$SHUNPO_TEST_DIR/home/.bashrc$" if [ -d "${SHUNPO_TEST_DIR}/home" ]; then - rmdir ${SHUNPO_TEST_DIR}/home/ + rmdir "${SHUNPO_TEST_DIR}/home/" fi if [ -d "${SHUNPO_TEST_DIR}" ]; then - find ${SHUNPO_TEST_DIR} -type d -empty -delete - rmdir $SHUNPO_TEST_DIR + find "${SHUNPO_TEST_DIR}" -type d -empty -delete + rmdir "$SHUNPO_TEST_DIR" fi } @@ -47,5 +49,5 @@ make_directories() { get_num_bookmarks() { SHUNPO_BOOKMARKS_FILE="${XDG_DATA_HOME:-$HOME/.local/share}/shunpo/.shunpo_bookmarks" - echo $(wc -l <$SHUNPO_BOOKMARKS_FILE | tr -d '[:space:]') + echo $(wc -l <"$SHUNPO_BOOKMARKS_FILE" | tr -d '[:space:]') } diff --git a/tests/test_config.bats b/tests/test_config.bats new file mode 100755 index 0000000..a20265d --- /dev/null +++ b/tests/test_config.bats @@ -0,0 +1,207 @@ +#!/usr/bin/env bats + +load common.sh +load bats/assert.sh +load bats/error.sh +load bats/lang.sh +load bats/output.sh + +setup() { + echo "Setting Up Test." + setup_env + printf '\n' | ./install.sh + working_dir=$(pwd) + source ${SHUNPO_TEST_DIR}/home/.bashrc +} + +teardown() { + echo "Shutting Down Test." + cd "$working_dir" + ./uninstall.sh +} + +@test "Test Config File Created During Install." { + # Check that config file was created. + assert [ -f "${XDG_CONFIG_HOME}/shunpo/config" ] +} + +@test "Test Config File Removed During Uninstall." { + # Verify config exists before uninstall. + assert [ -f "${XDG_CONFIG_HOME}/shunpo/config" ] + + # Run uninstall. + cd "$working_dir" + ./uninstall.sh + + # Verify config is removed. + refute [ -f "${XDG_CONFIG_HOME}/shunpo/config" ] + refute [ -d "${XDG_CONFIG_HOME}/shunpo" ] + + # Reinstall for teardown. + printf '\n' | ./install.sh + source ${SHUNPO_TEST_DIR}/home/.bashrc +} + +@test "Test Default Selection Keys." { + # Source functions to load config. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Check default keys. + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" +} + +@test "Test Custom Selection Keys." { + # Write custom config. + echo 'SHUNPO_SELECTION_KEYS="asdfghjkl;"' >"${XDG_CONFIG_HOME}/shunpo/config" + + # Source functions to load config. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Check custom keys loaded. + assert_equal "$SHUNPO_SELECTION_KEYS" "asdfghjkl;" + + # Restore default config. + cat >"${XDG_CONFIG_HOME}/shunpo/config" <"${XDG_CONFIG_HOME}/shunpo/config" + + # Source functions. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Test key lookup with custom keys. + result=$(shunpo_get_key_index "a") + assert_equal "$result" "0" + + result=$(shunpo_get_key_index "g") + assert_equal "$result" "4" + + result=$(shunpo_get_key_index ";") + assert_equal "$result" "9" + + # Test that old numeric keys no longer work. + run shunpo_get_key_index "1" + assert_failure + + # Restore default config. + cat >"${XDG_CONFIG_HOME}/shunpo/config" <"${XDG_CONFIG_HOME}/shunpo/config" + + # Source functions (should show warning and use defaults). + source ${SHUNPO_DIR}/scripts/colors.sh + run source ${SHUNPO_DIR}/scripts/functions.sh + + # Re-source to get the variable. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Should fall back to defaults. + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" + + # Restore default config. + cat >"${XDG_CONFIG_HOME}/shunpo/config" <"${XDG_CONFIG_HOME}/shunpo/config" + + # Source functions. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Should fall back to defaults. + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" + + # Test with 'p'. + echo 'SHUNPO_SELECTION_KEYS="abcdefghip"' >"${XDG_CONFIG_HOME}/shunpo/config" + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" + + # Test with 'b'. + echo 'SHUNPO_SELECTION_KEYS="abcdefghib"' >"${XDG_CONFIG_HOME}/shunpo/config" + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" + + # Restore default config. + cat >"${XDG_CONFIG_HOME}/shunpo/config" <"${XDG_CONFIG_HOME}/shunpo/config" + + # Source functions. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Should fall back to defaults. + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" + + # Restore default config. + cat >"${XDG_CONFIG_HOME}/shunpo/config" <"${XDG_CONFIG_HOME}/shunpo/config" + + # Source functions. + source ${SHUNPO_DIR}/scripts/colors.sh + source ${SHUNPO_DIR}/scripts/functions.sh + + # Should use defaults. + assert_equal "$SHUNPO_SELECTION_KEYS" "0123456789" + + # Restore default config. + cat >"${XDG_CONFIG_HOME}/shunpo/config" </dev/null && echo "Removed $SHUNPO_CONFIG_DIR" + fi + # Remove scripts and directories. if [ -z "$SHUNPO_DIR" ]; then echo "No Installation Found." @@ -38,7 +49,7 @@ uninstall() { cd .. rmdir "${SHUNPO_DIR}"/scripts cd .. - rmdir $SHUNPO_DIR + rmdir "$SHUNPO_DIR" echo "Removed $SHUNPO_DIR" unset SHUNPO_DIR fi