diff --git a/Dockerfile b/Dockerfile index 7203ef5..9a1def1 100755 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ WORKDIR /home/tester # Copy the script and tests COPY --chown=tester:tester bash-autosuggestions.sh /home/tester/.local/share/bash-autosuggestions/bash-autosuggestions.sh -COPY --chown=tester:tester test-automated.sh /home/tester/test-automated.sh # Seed bash history with realistic commands so suggestions appear immediately RUN mkdir -p /home/tester/.local/share/bash-autosuggestions && \ diff --git a/bash-autosuggestions.sh b/bash-autosuggestions.sh index 9564150..c37e86e 100644 --- a/bash-autosuggestions.sh +++ b/bash-autosuggestions.sh @@ -5,7 +5,7 @@ # # KEYBINDINGS: # → (Right Arrow) / End Accept full suggestion -# Alt+→ / Alt+F Accept next word +# Ctrl+→ / Ctrl+← / Alt+F Accept next word # ↑ (Up Arrow) Next suggestion (cycle) # ↓ (Down Arrow) Previous suggestion (cycle) # Ctrl+] Dismiss suggestion @@ -56,7 +56,7 @@ _bas_collect_candidates() { local line prev_line="" local contextual_matches=() local regular_matches=() - + # Read history in reverse (most recent first) while IFS='' read -r line; do # Strip leading spaces and history number @@ -70,7 +70,7 @@ _bas_collect_candidates() { eval "[[ \"\$line\" == $BASH_AUTOSUGGEST_HISTORY_IGNORE ]]" 2>/dev/null && continue fi seen[$line]=1 - + # For match_prev_cmd strategy: check if prev_line matches _bas_prev_cmd if [[ "$BASH_AUTOSUGGEST_STRATEGY" == "match_prev_cmd" && -n "$_bas_prev_cmd" ]]; then if [[ "$prev_line" == "$_bas_prev_cmd" ]]; then @@ -85,13 +85,13 @@ _bas_collect_candidates() { (( ${#_bas_candidates[@]} >= BASH_AUTOSUGGEST_MAX_CANDIDATES )) && break fi fi - + # Track previous line for context matching if [[ -n "$line" ]]; then prev_line="$line" fi done < <(HISTTIMEFORMAT='' builtin history | tac) - + # For match_prev_cmd: combine contextual matches first, then regular if [[ "$BASH_AUTOSUGGEST_STRATEGY" == "match_prev_cmd" ]]; then _bas_candidates=("${contextual_matches[@]}" "${regular_matches[@]}") @@ -190,14 +190,14 @@ _bas_show_current_candidate() { _bas_clear_ghost if (( _bas_cand_idx >= 0 && _bas_cand_idx < ${#_bas_candidates[@]} )); then local suffix="${_bas_candidates[$_bas_cand_idx]:${#_bas_typed}}" - + # Store for async display _bas_pending_ghost="$suffix" _bas_pending_counter="" if (( ${#_bas_candidates[@]} > 1 )); then _bas_pending_counter=" [$((_bas_cand_idx+1))/${#_bas_candidates[@]}]" fi - + _bas_show_ghost_after_redisplay fi } @@ -213,21 +213,21 @@ _bas_show_ghost_after_redisplay() { if [[ -n "$_bas_pending_ghost" ]]; then _bas_ghost="$_bas_pending_ghost" _bas_showing=1 - + # Launch background job and immediately disown to remove from job table { # Minimal delay to let readline finish cursor positioning read -t 0.001 < /dev/null 2>/dev/null || true - + # Print ghost and reposition cursor printf '\e[%sm%s\e[0m\e[2m%s\e[0m\e[%dD' \ "$BASH_AUTOSUGGEST_HIGHLIGHT" "$_bas_pending_ghost" "$_bas_pending_counter" \ "$((${#_bas_pending_ghost} + ${#_bas_pending_counter}))" > /dev/tty } &>/dev/null & - + # Remove job from job table immediately disown -a 2>/dev/null - + _bas_pending_ghost="" _bas_pending_counter="" fi @@ -236,39 +236,39 @@ _bas_show_ghost_after_redisplay() { _bas_self_insert_and_update() { local char="$1" _bas_clear_ghost - + # Insert character at cursor position local line="$READLINE_LINE" local pos="$READLINE_POINT" local before="${line:0:$pos}" local after="${line:$pos}" - + local new_line="${before}${char}${after}" local new_point=$((pos + 1)) - + # Only show suggestions if typing at end of line and enabled if [[ -z "$after" ]] && (( _bas_enabled )); then local typed="${before}${char}" _bas_typed="$typed" - + if [[ -n "$typed" && ${#typed} -ge $BASH_AUTOSUGGEST_MIN_LENGTH ]]; then _bas_collect_candidates "$typed" - + if (( ${#_bas_candidates[@]} > 0 )); then _bas_cand_idx=0 local suffix="${_bas_candidates[0]:${#typed}}" - + # Store the ghost to show it AFTER readline redraws _bas_pending_ghost="$suffix" _bas_pending_counter="" if (( ${#_bas_candidates[@]} > 1 )); then _bas_pending_counter=" [$((_bas_cand_idx+1))/${#_bas_candidates[@]}]" fi - + # Update readline variables - this causes a redisplay READLINE_LINE="$new_line" READLINE_POINT=$new_point - + # Schedule ghost display to happen after this handler returns # Use PROMPT_COMMAND to display on next prompt cycle # Store for display after readline finishes @@ -282,7 +282,7 @@ _bas_self_insert_and_update() { _bas_cand_idx=-1 _bas_typed="${before}${char}" fi - + # Update readline variables READLINE_LINE="$new_line" READLINE_POINT=$new_point @@ -292,29 +292,29 @@ _bas_backspace_handler() { # Delete character before cursor local line="$READLINE_LINE" local pos="$READLINE_POINT" - + if (( pos > 0 )); then local before="${line:0:$((pos-1))}" local after="${line:$pos}" - + READLINE_LINE="${before}${after}" READLINE_POINT=$((pos - 1)) - + # Clear state immediately - call _bas_clear_ghost to clear visible ghost _bas_clear_ghost - + # Only show suggestions if at end of line and enabled if [[ -z "$after" ]] && (( _bas_enabled )); then local typed="$before" _bas_typed="$typed" - + if [[ -n "$typed" && ${#typed} -ge $BASH_AUTOSUGGEST_MIN_LENGTH ]]; then _bas_collect_candidates "$typed" - + if (( ${#_bas_candidates[@]} > 0 )); then _bas_cand_idx=0 local suffix="${_bas_candidates[0]:${#typed}}" - + # Store for async display _bas_pending_ghost="$suffix" _bas_pending_counter="" @@ -362,32 +362,32 @@ _bas_delete_handler() { _bas_kill_word_handler() { _bas_clear_ghost - + # Kill word backward from cursor local line="$READLINE_LINE" local pos="$READLINE_POINT" local before="${line:0:$pos}" local after="${line:$pos}" - + # Remove trailing spaces then word before="${before%"${before##*[! ]}"}" before="${before%"${before##* }"}" - + READLINE_LINE="${before}${after}" READLINE_POINT=${#before} - + # Only show suggestions if at end of line and enabled if [[ -z "$after" ]] && (( _bas_enabled )); then local typed="$before" _bas_typed="$typed" - + if [[ -n "$typed" && ${#typed} -ge $BASH_AUTOSUGGEST_MIN_LENGTH ]]; then _bas_collect_candidates "$typed" - + if (( ${#_bas_candidates[@]} > 0 )); then _bas_cand_idx=0 local suffix="${_bas_candidates[0]:${#typed}}" - + # Store for async display _bas_pending_ghost="$suffix" _bas_pending_counter="" @@ -412,7 +412,7 @@ _bas_kill_word_handler() { _bas_kill_line_handler() { _bas_clear_ghost - + READLINE_LINE="" READLINE_POINT=0 _bas_typed="" @@ -428,7 +428,7 @@ _bas_cycle_next() { # Multiple suggestions available - cycle through them _bas_cand_idx=$(( (_bas_cand_idx + 1) % ${#_bas_candidates[@]} )) _bas_clear_ghost - + # Prepare ghost for display local suffix="${_bas_candidates[$_bas_cand_idx]:${#_bas_typed}}" _bas_pending_ghost="$suffix" @@ -436,7 +436,7 @@ _bas_cycle_next() { if (( ${#_bas_candidates[@]} > 1 )); then _bas_pending_counter=" [$((_bas_cand_idx+1))/${#_bas_candidates[@]}]" fi - + # Display after handler returns trap '_bas_show_ghost_after_redisplay; trap - RETURN' RETURN elif (( ${#_bas_candidates[@]} == 1 )); then @@ -460,7 +460,7 @@ _bas_cycle_prev() { # Multiple suggestions available - cycle through them _bas_cand_idx=$(( (_bas_cand_idx - 1 + ${#_bas_candidates[@]}) % ${#_bas_candidates[@]} )) _bas_clear_ghost - + # Prepare ghost for display local suffix="${_bas_candidates[$_bas_cand_idx]:${#_bas_typed}}" _bas_pending_ghost="$suffix" @@ -468,7 +468,7 @@ _bas_cycle_prev() { if (( ${#_bas_candidates[@]} > 1 )); then _bas_pending_counter=" [$((_bas_cand_idx+1))/${#_bas_candidates[@]}]" fi - + # Display after handler returns trap '_bas_show_ghost_after_redisplay; trap - RETURN' RETURN elif (( ${#_bas_candidates[@]} == 1 )); then @@ -522,10 +522,10 @@ _bas_accept_word() { local remaining="${full:${#_bas_typed}}" local word="" i=0 len=${#remaining} - while (( i < len )) && [[ "${remaining:$i:1}" == " " ]]; do + while (( i < len )) && [[ "${remaining:$i:1}" == " " || "${remaining:$i:1}" == "." ]]; do word+="${remaining:$i:1}"; (( i++ )) done - while (( i < len )) && [[ "${remaining:$i:1}" != " " ]]; do + while (( i < len )) && [[ "${remaining:$i:1}" != " " && "${remaining:$i:1}" != "." ]]; do word+="${remaining:$i:1}"; (( i++ )) done @@ -548,8 +548,8 @@ _bas_accept_word() { fi else local pos=$READLINE_POINT len=${#READLINE_LINE} - while (( pos < len )) && [[ "${READLINE_LINE:$pos:1}" != " " ]]; do (( pos++ )); done - while (( pos < len )) && [[ "${READLINE_LINE:$pos:1}" == " " ]]; do (( pos++ )); done + while (( pos < len )) && [[ "${READLINE_LINE:$pos:1}" != " " && "${READLINE_LINE:$pos:1}" != "." ]]; do (( pos++ )); done + while (( pos < len )) && [[ "${READLINE_LINE:$pos:1}" == " " || "${READLINE_LINE:$pos:1}" == "." ]]; do (( pos++ )); done READLINE_POINT=$pos fi } @@ -575,14 +575,14 @@ _bas_enter_handler() { _bas_candidates=() _bas_cand_idx=-1 fi - + # Save current command for match_prev_cmd strategy _bas_prev_cmd="$READLINE_LINE" # Unbind our handler and rebind to accept-line for THIS press bind '"\C-m": accept-line' bind '"\C-j": accept-line' - + # Inject Enter to trigger the now-bound accept-line # Use ANSI sequence to inject key into input buffer # \x1b[200~ starts bracketed paste, then send \r, then \x1b[201~ ends it @@ -600,15 +600,15 @@ _bas_rebind_enter() { _bas_setup() { # Disable job control monitoring to prevent "[1] 359" messages from background jobs set +m - + # Accept / dismiss bind -x '"\e[C": _bas_accept_full' bind -x '"\eOC": _bas_accept_full' bind -x '"\e[F": _bas_accept_end' bind -x '"\eOF": _bas_accept_end' bind -x '"\e[4~": _bas_accept_end' - bind -x '"\e[1;3C": _bas_accept_word' - bind -x '"\e\e[C": _bas_accept_word' + bind -x '"\e[1;5C": _bas_accept_word' + bind -x '"\e[1;5D": _bas_kill_word_handler' bind -x '"\ef": _bas_accept_word' bind -x '"\C-]": _bas_dismiss'