## bash-ido - attempt to simulate a interactive menu similar to # ido-mode in emacs # # URL: http://pgas.freeshell.org/shell/bash-ido # Author: # Version: 1.0beta2 # CVS: $Id: bash-ido,v 1.18 2010/02/13 14:50:32 pgas Exp $ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with BASH-IDO; see the file COPYING. If not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. # # Documentation: # -------------- # This is a fairly complex completion script for cd. (For now, i tried # to make the functions a bit generic so that the menu can be used for # other completions. Let me know if you need some help or something) # It mimics what ido-mode does in emacs.This script also masks the # normal cd with a function to track the list of the directory # used. It's easier to try than to describe, here is a little getting # started: # # 0) Source this script in your .bashrc (. /path/to/bash-ido) # 1) Start your cd command, press TAB, type some letters the list of dirs is # filtered according to these letters. # 2) Press RET to select the first dir in the list. # 3) DEL ie the erase key (usually backspace or ^H), to delete a search letter. # When there are no more letters, pressing DEL let you go up one dir. # 4) C-s or cycles the list to the right, C-r or to the left # 5) C-g or C-c cancels the completion. # 6) Typing 2 / will put you in /, typing /~/ will put you in $HOME # 7) up/M-s and down/M-r allows to navigate in the history # Limitations # ----------- # * You cannot start completion after a dirname in " " or ' ' # (actually it's probably possible if you modify COMP_WORDBREAKS) # * It doesn't expand the ~user/ dirs, if you need this please tell me # * The completion disables and re-enables C-c using stty, # if you use another char for intr you need to modify # the hard coded value (search for $'\003') this could be found via stty # or a parameter could be defined but how well....tell me if you feel this # is needed. # * It probably doesn't work too well with huge dirs (a security check could # perhaps be implemented). # Implementation Notes: # --------------------- # * Not sure what bash version is required. # * It Probably doesn't work too well with some strange filenames. # * All the functions and variables in this file should be prefixed by _ido_ # to avoid namespace polution # * Use stty rather than a trap to disable sigint....I couldn't do what # I wanted with trap. # * I choose to use the hardcoded ansi codes rather than tput, it should be # a tad faster and reduce the dependencies, if you it doesn't work in your # terminal please tell me. # TODO: # ----- # * add an example for other completions # * support for CDPATH in dir completion? # * implement persitent history? # * complete on ~ first and handle ~user? # Global vars # ----------- # _ido_menu -- the list of choices # _ido_f_menu -- the filtered menu # _ido_search_string -- the characters typed so far # _ido_result -- the dirname part of the search _ido_history_size=100 # -- maximum dir entries in the history # _ido_history -- list of the directories in the history # _ido_history_point -- pointer to the current history entry # Functions # --------- # _ido_print_menu -- print the filtered menu # _ido_loop -- the main keyboard event loop # _ido_filter -- filters the menu # _ido_gen_dir -- generate the original menu (list of dirs) # _ido_dir -- entry point # Changes # ------- # 1.0b2 # * fix ../ behaviour # * fix TAB behaviour shopt -s checkwinsize #so that COLUMNS stays up to date _ido_print_f_menu () { # Prints the directories limited on one line... # We would need some terminal commands to clear more than one line local prompt menu i cur prompt=${_ido_result}${_ido_search_string} if (( ${#_ido_f_menu[@]} ));then menu="{ ${_ido_f_menu[0]#* }" i=1 while cur=${_ido_f_menu[i]#* }; (( i < (${#_ido_f_menu[@]} -1) && (${#menu}+${#prompt} +${#cur}+11) < COLUMNS )) \ || (( i == (${#_ido_f_menu[@]} -1) && (${#menu}+${#prompt}+${#cur}+4) < COLUMNS)); do menu+=" | ${cur}" i=$((i+1)) done if ((i < (${#_ido_f_menu[@]} -1) )) ; then menu+=" | ... }" else menu+=" }" fi #yet another hack to put the cursor after the search string. printf "\r%*s\r%*s%s\r%s" $COLUMNS " " ${#prompt} " " "$menu" "$prompt" else printf "\r%*s\r%*s%s\r%s" $COLUMNS " " ${#prompt} " " "[ No Match]" "$prompt" fi } _ido_filter () { local i start trans_i cas quoted if shopt -q nocasematch;then cas=set else shopt -s nocasematch fi start=${_ido_f_menu[i]%% *} unset _ido_f_menu if [[ "$_ido_search_string" ]];then printf -v quoted "%q" "$_ido_search_string" else quoted="" fi for i in "${!_ido_menu[@]}";do trans_i=$(((i+start)%${#_ido_menu[@]})) if [[ "${_ido_menu[trans_i]}" = *"$quoted"* ]];then _ido_f_menu+=( "$trans_i ${_ido_menu[trans_i]}" ) fi done if [[ -z $cas ]];then shopt -u nocasematch fi } _ido_loop () { local REPLY c while :;do _ido_print_f_menu >&2 unset c while :;do #loop to read the escape sequences IFS= read -d '' -r -s -n 1 case $REPLY in $'\E') c+=$REPLY ;; \[|O) c+=$REPLY if ((${#c} ==1));then break fi ;; *) c+=$REPLY break ;; esac done case $c in $'\n'|$'\t') # RET _ido_result="${_ido_result}${_ido_f_menu[0]#* }" return 0 ;; / ) # / case $_ido_search_string in ..) _ido_result+="../" ;; \~) _ido_result="$HOME/" ;; ?*) _ido_result="${_ido_result}${_ido_f_menu[0]#* }" ;; *) _ido_result=/ ;; esac return 0 ;; '$\b'|$'\177') #DEL aka ^? or ^h if [[ $_ido_search_string ]]; then _ido_search_string=${_ido_search_string%?} _ido_filter else _ido_result+="../" return 0 fi ;; $'\a' | $'\003') # C-g | C-c return 2 ;; $'\E[C'|$'\EOC'|$'\023') # | C-s if ((${#_ido_f_menu[@]}>1));then _ido_f_menu=("${_ido_f_menu[@]:1}" "${_ido_f_menu[0]}") fi ;; $'\E[D'|$'\EOD'|$'\022') # | C-r if ((${#_ido_f_menu[@]}>1));then _ido_f_menu=("${_ido_f_menu[${#_ido_f_menu[@]}-1]}" "${_ido_f_menu[@]:0:${#_ido_f_menu[@]}-1}") fi ;; $'\E[B'|$'\EOB'|$'\367'|$'\Er') # | M-r if ((_ido_history_point>1));then _ido_history_point=$((_ido_history_point-1)) _ido_result=${_ido_history[_ido_history_point]%/}/ _ido_search_string="" return 0 else printf "\a" >&2 fi ;; $'\E[A'|$'\EOA'|$'\362'|$'\Es') # | M-s if (((_ido_history_point+1)< ${#_ido_history[@]}));then _ido_history_point=$((_ido_history_point+1)) _ido_result=${_ido_history[_ido_history_point]%/}/ _ido_search_string="" return 0 else printf "\a" >&2 fi ;; [[:print:]]) _ido_search_string+=$REPLY _ido_filter ;; *) printf "\a" >&2 # printf "%q\n" "$c" ;; esac done } _ido_gen_dir () { # return a list of subdir in _ido_result, local pattern case $_ido_search_string in ..) pattern=../ ;; .) pattern="./ ..?*/ .[!.]*/ */" ;; .?*) pattern="..?*/ .[!.]*/ */" ;; *) pattern="./ */ ..?*/ .[!.]*/" ;; esac _ido_result=${_ido_result/#~\//$HOME/} unset _ido_menu IFS=$'\n' read -r -d '' -a _ido_menu \ < <(IFS=" ";shopt -s nullglob;\ eval command cd "$_ido_result" 2>/dev/null \ && printf -- "%q\n" $pattern) if [[ $_ido_search_string ]];then _ido_filter else local i e unset _ido_f_menu for e in "${_ido_menu[@]}";do _ido_f_menu[i]="$i $e" i=$((i+1)) done fi _ido_result="${_ido_result/#$HOME\//~/}" } _ido_dir () { _ido_result=${COMP_WORDS[COMP_CWORD]} if [[ "$_ido_result" == \$* ]]; then if [[ "$_ido_result" == */* ]];then local temp temp=${_ido_result%%/*} temp=${temp#?} _ido_result=${!temp}/${_ido_result#*/} else COMPREPLY=( $( compgen -v -P '$' -- "${_ido_result#$}" ) ) return 0 fi fi stty intr undef local status unset _ido_f_menu _ido_search_string _ido_history_point printf '\E7' #tput sc = save cursor status=0 case $_ido_result in ''|.|./ ) _ido_result=$PWD/ ;; */*) _ido_search_string=${_ido_result##*/} _ido_result=${_ido_result%/*}/ _ido_result=${_ido_result/#~\//$HOME/} if [[ ! -d ${_ido_result} ]]; then printf "\a\nNo Such Directory: %s\n" "${_ido_result}" >&2 status=1 fi ;; *) _ido_search_string=$_ido_result _ido_result=$PWD/ ;; esac # normalize ie remove the // and other foo/../bar _ido_result=$(command cd "$_ido_result" && printf "%q" "${PWD%/}/") while [[ $status = 0 && $_ido_result != */./ ]]; do # hmm this part is dir specific...TB generalized.. case $_ido_result in # order is important, the last case covers the first ones /../) _ido_result=/ ;; \~/../) _ido_result=${HOME%/*}/ ;; */../) _ido_result=${_ido_result%/*/../}/ ;; esac _ido_gen_dir _ido_loop; status=$? _ido_search_string="" done printf "\r%*s\E8" $COLUMNS # clear the line, tput rc restore the cursor kill -WINCH $$ # force bash to redraw stty intr $'\003' if [[ $status = 0 ]]; then # gives the _ido_result to bash COMPREPLY[0]=${_ido_result%./} fi return $status } complete -F _ido_dir -o nospace cd cd () { if command cd "$@";then _ido_history=($(printf "%s\n" "$PWD" "${_ido_history[@]}" |\ awk -v s="$_ido_history_size" '!a[$0]++;(i++==s-1){exit}')) fi } ### Local Variables: ### mode: shell-script ### End: