addet qfc not as a submodule
This commit is contained in:
parent
cc8e05e3d5
commit
bbf0120ad6
13 changed files with 806 additions and 5 deletions
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -1,4 +0,0 @@
|
||||||
[submodule ".qfc"]
|
|
||||||
path = .qfc
|
|
||||||
url = https://github.com/pindexis/qfc
|
|
||||||
branch = master
|
|
1
.qfc
1
.qfc
|
@ -1 +0,0 @@
|
||||||
Subproject commit 3ed9e530af70a1ce9c6b23abb10c5f7617217494
|
|
50
.qfc/README.md
Normal file
50
.qfc/README.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# qfc
|
||||||
|
Quick Command-line File Completion
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
qfc is a shell auto-complete alternative which features real-time multi-directories matching: It provides results while you type against files in the current directory and its sub-directories.
|
||||||
|
This is useful, to avoid the burden of writing the whole path whenever you want to `cd` or `vim` a file, which is frequent especially if you use the terminal as your IDE(The terminal is the best IDE, remember! :-) ).
|
||||||
|
|
||||||
|
|
||||||
|
## Features:
|
||||||
|
- Real-time matching: Results are displayed while you type.
|
||||||
|
- Multi-directories && Context relevant matching: if you're in a cvs(git,mercurial) managed directory, qfc will matches against your tracked(or new) files only. This is very useful to avoid 10000+ of dependency files cluttering up the results. for unmanaged dirs, qfc looks for unhidden files up to a maximum depth(set to 3).
|
||||||
|
- Enhanced Filtering/Sorting of matches.
|
||||||
|
- No dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- python (2.7+ or 3.0+)
|
||||||
|
- Bash-4.0+ or Zshell.
|
||||||
|
- Linux Or OSX
|
||||||
|
In OSX, it seems like Bash 3.x is the default shell which is not supported. you have to [update your Bash to 4.0+](http://apple.stackexchange.com/a/24635) or [change your shell to zshell](http://stackoverflow.com/a/1822126/1117720).
|
||||||
|
|
||||||
|
|
||||||
|
## Installation:
|
||||||
|
- `git clone https://github.com/pindexis/qfc $HOME/.qfc`
|
||||||
|
- Add the following line to your *rc (.zshrc, .bashrc, .bash_profile in OSX):
|
||||||
|
`[[ -s "$HOME/.qfc/bin/qfc.sh" ]] && source "$HOME/.qfc/bin/qfc.sh"`
|
||||||
|
|
||||||
|
|
||||||
|
## Usage:
|
||||||
|
- `Ctrl-f` : complete the word under cursor using qfc
|
||||||
|
- while qfc is open:
|
||||||
|
- `TAB`: Append the selected match to the current path.
|
||||||
|
- `ENTER`: Append the selected match to the current path and returns the result.
|
||||||
|
- `Ctrl-f`: Returns the current path.
|
||||||
|
- `Arrow keys`: Navigation between files.
|
||||||
|
|
||||||
|
|
||||||
|
## Even more Productivity:
|
||||||
|
If you're using zshell or Bash 4.3+, You can combine qfc with commands you frequently use to get one key-stroke experience. For example, I have the following lines in my .zshrc:
|
||||||
|
```
|
||||||
|
qfc_quick_command 'cd' '\C-b' 'cd $0'
|
||||||
|
qfc_quick_command 'vim' '\C-p' 'vim $0'
|
||||||
|
```
|
||||||
|
This allows me to switch directories by just pressing Ctrl-b(or editing a file by pressing Ctrl-p).
|
||||||
|

|
||||||
|
|
||||||
|
`qfc_quick_command` expects an `id`, `a shortcut`, and a command with `$0` placeholder(which will be replaced with the completion path).
|
||||||
|
It's recommended to choose a 2-5 length letters only `id`(else you may encounter issues).
|
||||||
|
Also, be careful with what keyboard shortcuts to choose(mapping some keys can prevent the terminal from working correctly).
|
33
.qfc/bin/qfc
Executable file
33
.qfc/bin/qfc
Executable file
|
@ -0,0 +1,33 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)),'..'))
|
||||||
|
from qfc import core
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
parser = argparse.ArgumentParser(description="Qfc: Quick command-line file competion")
|
||||||
|
|
||||||
|
parser.add_argument("--search", type=str, help="Search string")
|
||||||
|
parser.add_argument("--stdout", type=str, help="Where to store result(defaut to stdout)")
|
||||||
|
parser.set_defaults(func=cmd_get)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args.func(args)
|
||||||
|
|
||||||
|
def cmd_get(args):
|
||||||
|
output = core.get_selected_command_or_input(args.search)
|
||||||
|
if not output:
|
||||||
|
output = ""
|
||||||
|
if args.stdout:
|
||||||
|
with open(args.stdout,'w+') as save_file:
|
||||||
|
# the newline character is to make sure 'wc -l' executes correctly
|
||||||
|
if output:
|
||||||
|
output+="\n"
|
||||||
|
save_file.write(output)
|
||||||
|
else:
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parse_arguments()
|
126
.qfc/bin/qfc.sh
Executable file
126
.qfc/bin/qfc.sh
Executable file
|
@ -0,0 +1,126 @@
|
||||||
|
# default key bindings
|
||||||
|
complete_shortcut="${qfc_complete_SHORTCUT:-\C-f}"
|
||||||
|
|
||||||
|
function get_cursor_position(){
|
||||||
|
# based on a script from http://invisible-island.net/xterm/xterm.faq.html
|
||||||
|
exec < /dev/tty
|
||||||
|
oldstty=$(stty -g)
|
||||||
|
stty raw -echo min 0
|
||||||
|
# on my system, the following line can be replaced by the line below it
|
||||||
|
echo -en "\033[6n" > /dev/tty
|
||||||
|
# tput u7 > /dev/tty # when TERM=xterm (and relatives)
|
||||||
|
IFS=';' read -r -d R row col
|
||||||
|
stty $oldstty
|
||||||
|
# change from one-based to zero based so they work with: tput cup $row $col
|
||||||
|
row=$((${row:2} - 1)) # strip off the esc-[
|
||||||
|
col=$((${col} - 1))
|
||||||
|
echo "$row $col"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -d ~/.qfc/ ]]; then
|
||||||
|
export PATH=~/.qfc/bin:"${PATH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$ZSH_VERSION" ]]; then
|
||||||
|
# zshell
|
||||||
|
function qfc_complete {
|
||||||
|
# Add a letter and remove it from the buffer.
|
||||||
|
# when using zsh autocomplete(pressing Tab), then running qfc, the BUFFER(qfc input) won't contain the trailing forward slash(which should happen when using zsh autocomplete for directories).
|
||||||
|
# pressing a character then removing it makes sure that BUFFER contains what you see on the screen.
|
||||||
|
BUFFER=${BUFFER}'a'
|
||||||
|
BUFFER=${BUFFER[0,-2]}
|
||||||
|
# get the cursor offset within the user input
|
||||||
|
offset=${CURSOR}
|
||||||
|
zle beginning-of-line
|
||||||
|
# get the offset from the start of comandline prompt
|
||||||
|
col=$(echo $(get_cursor_position) | cut -f 2 -d " ")
|
||||||
|
# place the cursor at the next line
|
||||||
|
</dev/tty echo ''
|
||||||
|
|
||||||
|
# get the word under cursor
|
||||||
|
word=${BUFFER[0,offset]}
|
||||||
|
word=${word##* }
|
||||||
|
|
||||||
|
# instruct qfc to store the result (completion path) into a temporary file
|
||||||
|
tmp_file=$(mktemp -t qfc.XXXXXXX)
|
||||||
|
</dev/tty qfc --search="$word" --stdout="$tmp_file"
|
||||||
|
result=$(<$tmp_file)
|
||||||
|
rm -f $tmp_file
|
||||||
|
|
||||||
|
# append the completion path to the user buffer
|
||||||
|
word_length=${#word}
|
||||||
|
result_length=${#result}
|
||||||
|
BUFFER=${BUFFER[1,$((offset-word_length))]}${result}${BUFFER[$((offset+word_length)),-1]}
|
||||||
|
let "offset = offset - word_length + result_length"
|
||||||
|
|
||||||
|
# reset the absolute and relative cursor position, note that it's necessary to get row position after qfc is run, because it may be changed during qfc execution
|
||||||
|
row=$(echo $(get_cursor_position) | cut -f 1 -d " ")
|
||||||
|
tput cup $(($row - 1)) $col
|
||||||
|
CURSOR=${offset}
|
||||||
|
}
|
||||||
|
|
||||||
|
zle -N qfc_complete
|
||||||
|
bindkey "$complete_shortcut" qfc_complete
|
||||||
|
|
||||||
|
function qfc_quick_command(){
|
||||||
|
if [[ ! -z $1 ]] && [[ ! -z $2 ]] && [[ ! -z $3 ]]; then
|
||||||
|
func_name='quick_'$1
|
||||||
|
eval $"function $func_name(){
|
||||||
|
zle kill-whole-line
|
||||||
|
qfc_complete
|
||||||
|
if [[ ! -z \${BUFFER} ]]; then
|
||||||
|
c='$3'
|
||||||
|
BUFFER=\${c//'\$0'/\$BUFFER}
|
||||||
|
zle accept-line
|
||||||
|
fi
|
||||||
|
}"
|
||||||
|
zle -N $func_name
|
||||||
|
bindkey "$2" $func_name
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
elif [[ -n "$BASH" ]]; then
|
||||||
|
|
||||||
|
function qfc_complete {
|
||||||
|
# pretty similar to zsh flow
|
||||||
|
offset=${READLINE_POINT}
|
||||||
|
READLINE_POINT=0
|
||||||
|
col=$(get_cursor_position | cut -f 2 -d " ")
|
||||||
|
|
||||||
|
word=${READLINE_LINE:0:offset}
|
||||||
|
word=${word##* }
|
||||||
|
|
||||||
|
tmp_file=$(mktemp -t qfc.XXXXXXX)
|
||||||
|
</dev/tty qfc --search="$word" --stdout="$tmp_file"
|
||||||
|
result=$(<$tmp_file)
|
||||||
|
rm -f $tmp_file
|
||||||
|
|
||||||
|
word_length=${#word}
|
||||||
|
result_length=${#result}
|
||||||
|
READLINE_LINE=${READLINE_LINE:0:$((offset-word_length))}${result}${READLINE_LINE:$((offset))}
|
||||||
|
offset=$(($offset - $word_length + $result_length))
|
||||||
|
|
||||||
|
row=$(get_cursor_position | cut -f 1 -d " ")
|
||||||
|
tput cup $row $col
|
||||||
|
READLINE_POINT=${offset}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind -x '"'"$complete_shortcut"'":"qfc_complete"'
|
||||||
|
|
||||||
|
function qfc_quick_command {
|
||||||
|
if [[ ! -z $1 ]] && [[ ! -z $2 ]] && [[ ! -z $3 ]]; then
|
||||||
|
func_name='quick_'$1
|
||||||
|
eval $"function $func_name(){
|
||||||
|
READLINE_LINE=''
|
||||||
|
qfc_complete
|
||||||
|
if [[ ! -z \${READLINE_LINE} ]]; then
|
||||||
|
c='$3'
|
||||||
|
READLINE_LINE=\${c//'\$0'/\$READLINE_LINE}
|
||||||
|
fi
|
||||||
|
}"
|
||||||
|
bind -x '"\e-'"$1"'":"'"${func_name}"'"'
|
||||||
|
bind '"'"$2"'":""\e-'"$1"'\n"'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fi
|
0
.qfc/qfc/__init__.py
Normal file
0
.qfc/qfc/__init__.py
Normal file
60
.qfc/qfc/ansi.py
Normal file
60
.qfc/qfc/ansi.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BOLD = "\x1b[1m"
|
||||||
|
CLEAR_FORMATTING = "\x1b[0m"
|
||||||
|
ERASE_SCREEN = "\x1b[J"
|
||||||
|
ERASE_LINE = "\x1b[2K"
|
||||||
|
FOREGROUND_BLACK = "\x1b[30m"
|
||||||
|
BACKGROUND_WHITE = "\x1b[47m"
|
||||||
|
|
||||||
|
def _CURSOR_COLUMN(pos):
|
||||||
|
# ideally, CSI n G escape code is used to set the absolute horizental position
|
||||||
|
# Sadly, it's not an Ansi.sys escape code (not supported in all terminals)
|
||||||
|
# This shim try to simulate it by moving cursor backwards 1000 characters(terminal row width is assumed to be less than that number, which may not be the case for aliens laptops :))
|
||||||
|
# Then, move cursor pos - 1 characthers forward (the - 1 is because the cursor is at position 1)
|
||||||
|
c = "\x1b[1000D"
|
||||||
|
if pos:
|
||||||
|
c += "\x1b["+str(pos - 1)+"C"
|
||||||
|
return c
|
||||||
|
|
||||||
|
def _CURSOR_PREVIOUS_LINES(number):
|
||||||
|
return "\x1b["+str(number)+"A"
|
||||||
|
|
||||||
|
def _CURSOR_NEXT_LINES(number):
|
||||||
|
return "\x1b["+str(number)+"B"
|
||||||
|
|
||||||
|
def select_text(text):
|
||||||
|
return (FOREGROUND_BLACK +
|
||||||
|
BACKGROUND_WHITE +
|
||||||
|
text.replace(
|
||||||
|
CLEAR_FORMATTING,
|
||||||
|
CLEAR_FORMATTING + FOREGROUND_BLACK + BACKGROUND_WHITE)+
|
||||||
|
CLEAR_FORMATTING)
|
||||||
|
|
||||||
|
def bold_text(text):
|
||||||
|
return (BOLD +
|
||||||
|
text.replace(
|
||||||
|
CLEAR_FORMATTING,
|
||||||
|
CLEAR_FORMATTING + BOLD)+
|
||||||
|
CLEAR_FORMATTING)
|
||||||
|
|
||||||
|
def move_cursor_line_beggining():
|
||||||
|
sys.stdout.write(_CURSOR_COLUMN(0))
|
||||||
|
|
||||||
|
def move_cursor_horizental(n):
|
||||||
|
sys.stdout.write(_CURSOR_COLUMN(n))
|
||||||
|
|
||||||
|
def move_cursor_previous_lines(number_of_lines):
|
||||||
|
sys.stdout.write(_CURSOR_PREVIOUS_LINES(number_of_lines))
|
||||||
|
|
||||||
|
def move_cursor_next_lines(number_of_lines):
|
||||||
|
sys.stdout.write(_CURSOR_NEXT_LINES(number_of_lines))
|
||||||
|
|
||||||
|
def erase_from_cursor_to_end():
|
||||||
|
sys.stdout.write(ERASE_SCREEN)
|
||||||
|
|
||||||
|
def erase_line():
|
||||||
|
sys.stdout.write(ERASE_LINE)
|
||||||
|
|
||||||
|
def flush():
|
||||||
|
sys.stdout.flush()
|
231
.qfc/qfc/core.py
Normal file
231
.qfc/qfc/core.py
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from . import keys
|
||||||
|
from . import ui
|
||||||
|
from . import readchar
|
||||||
|
from . import dirhandler
|
||||||
|
|
||||||
|
|
||||||
|
def get_selected_command_or_input(search):
|
||||||
|
state = State(search)
|
||||||
|
# draw the screen (prompt + matched strings)
|
||||||
|
ui.refresh(state)
|
||||||
|
# wait for user input
|
||||||
|
prompt(state)
|
||||||
|
# clear the screen
|
||||||
|
ui.erase()
|
||||||
|
# state.input holds the path selected by the user
|
||||||
|
return state.input
|
||||||
|
|
||||||
|
|
||||||
|
def prompt(state):
|
||||||
|
while True:
|
||||||
|
c = readchar.get_symbol()
|
||||||
|
if c == keys.ENTER:
|
||||||
|
if state.get_matches():
|
||||||
|
state.append_match_to_input()
|
||||||
|
break
|
||||||
|
elif c == keys.CTRL_F:
|
||||||
|
break
|
||||||
|
elif c == keys.TAB:
|
||||||
|
if state.get_matches():
|
||||||
|
state.append_match_to_input()
|
||||||
|
elif c == keys.CTRL_C or c == keys.ESC:
|
||||||
|
state.reset_input()
|
||||||
|
break
|
||||||
|
elif c == keys.CTRL_U:
|
||||||
|
state.clear_input()
|
||||||
|
elif c == keys.BACKSPACE:
|
||||||
|
state.set_input(state.input[0:-1])
|
||||||
|
elif c == keys.UP or c == keys.CTRL_K:
|
||||||
|
state.select_previous()
|
||||||
|
elif c == keys.DOWN or c == keys.CTRL_J:
|
||||||
|
state.select_next()
|
||||||
|
elif c == keys.LEFT or c == keys.CTRL_H:
|
||||||
|
state.go_back()
|
||||||
|
elif c == keys.RIGHT or c == keys.CTRL_L:
|
||||||
|
if state.get_matches():
|
||||||
|
state.append_match_to_input()
|
||||||
|
elif c < 256 and c >= 32:
|
||||||
|
state.set_input(state.input + chr(c))
|
||||||
|
ui.refresh(state)
|
||||||
|
|
||||||
|
|
||||||
|
class State(object):
|
||||||
|
''' The Current User state, including user written characters, matched commands, and selected one '''
|
||||||
|
|
||||||
|
def __init__(self, default_input):
|
||||||
|
self._selected_command_index = 0
|
||||||
|
self.matches = []
|
||||||
|
self.default_input = default_input
|
||||||
|
self.set_input(default_input)
|
||||||
|
|
||||||
|
def get_matches(self):
|
||||||
|
return self.matches
|
||||||
|
|
||||||
|
def reset_input(self):
|
||||||
|
self.input = self.default_input
|
||||||
|
|
||||||
|
def set_input(self, input):
|
||||||
|
self.input = input if input else ""
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def append_match_to_input(self):
|
||||||
|
self.set_input(os.path.join(os.path.dirname(self.input), self.get_selected_match()))
|
||||||
|
|
||||||
|
def go_back(self):
|
||||||
|
isdir = is_dir(self.input)
|
||||||
|
input_stripped = self.input.rstrip(os.sep)
|
||||||
|
if not input_stripped:
|
||||||
|
return
|
||||||
|
input_splitted = input_stripped.split(os.sep)
|
||||||
|
entry_name = input_splitted[-1]
|
||||||
|
if isdir:
|
||||||
|
entry_name += os.sep
|
||||||
|
new_input = os.sep.join(input_splitted[0:-1])
|
||||||
|
if new_input:
|
||||||
|
new_input += os.sep
|
||||||
|
self.set_input(new_input)
|
||||||
|
self.set_selected_entry(entry_name)
|
||||||
|
|
||||||
|
def clear_input(self):
|
||||||
|
self.set_input("")
|
||||||
|
|
||||||
|
def clear_selection(self):
|
||||||
|
self._selected_command_index = 0
|
||||||
|
|
||||||
|
def select_next(self):
|
||||||
|
self._selected_command_index = (self._selected_command_index + 1) % len(self.matches) if len(self.matches) else 0
|
||||||
|
|
||||||
|
def select_previous(self):
|
||||||
|
self._selected_command_index = (self._selected_command_index - 1) % len(self.matches) if len(self.matches) else 0
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
self.matches = get_matches(os.getcwd(),self.input)
|
||||||
|
self._selected_command_index = 0
|
||||||
|
|
||||||
|
def get_output(self):
|
||||||
|
return self.input
|
||||||
|
|
||||||
|
def get_selected_match(self):
|
||||||
|
if len(self.matches):
|
||||||
|
return self.matches[self._selected_command_index]
|
||||||
|
else:
|
||||||
|
raise Exception('No matches found')
|
||||||
|
|
||||||
|
def set_selected_entry(self, entry):
|
||||||
|
if not (entry in self.matches):
|
||||||
|
return
|
||||||
|
self._selected_command_index = self.matches.index(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def get_matches(root_dir, user_input):
|
||||||
|
start_dir = join_paths(root_dir, os.path.dirname(user_input))
|
||||||
|
search_str = os.path.basename(user_input)
|
||||||
|
if not os.path.isdir(start_dir):
|
||||||
|
return []
|
||||||
|
source_files = dirhandler.get_source_files(start_dir)
|
||||||
|
filtered_files = filter_files(source_files, search_str)
|
||||||
|
sorted_files = sort_matches(filtered_files, search_str)
|
||||||
|
return sorted_files
|
||||||
|
|
||||||
|
|
||||||
|
def filter_files(files, search_str):
|
||||||
|
""" Filter a list of files based on a search string
|
||||||
|
:param files: list of files to filter (ie ['/a','/a/b', '/a/b/c', '/b']), the order doesn't matter. 'a' and 'a/' are considered different.
|
||||||
|
:param search_str, the filtering string (ie 'a')
|
||||||
|
|
||||||
|
This function return only first level files/dirs that match the given search string. That is, filtering ['/a','/a/b','/a/b/c'] with the string 'a' returns only ['/a'],
|
||||||
|
This is to avoid polluting the screen with all files inside a directory when a user look for that directory
|
||||||
|
"""
|
||||||
|
matched = set()
|
||||||
|
if not search_str:
|
||||||
|
for f in files:
|
||||||
|
# right strip to the first seperator(ie '/')
|
||||||
|
f = f[:_index_or_len(f, os.sep)+1]
|
||||||
|
matched.add(f)
|
||||||
|
else:
|
||||||
|
search_str = search_str.lower()
|
||||||
|
for f in files:
|
||||||
|
if search_str in f.lower():
|
||||||
|
index = f.lower().index(search_str)
|
||||||
|
trail = f[index:]
|
||||||
|
f = f[:index + _index_or_len(trail, os.sep)+1]
|
||||||
|
matched.add(f)
|
||||||
|
return matched
|
||||||
|
|
||||||
|
def sort_matches(matches, string):
|
||||||
|
""" Sort paths according to a string """
|
||||||
|
files = sorted(matches, key=lambda s: s.lower())
|
||||||
|
return sorted(files, key=lambda p:get_weight(p, string))
|
||||||
|
|
||||||
|
def get_weight(path, string):
|
||||||
|
""" calculate how much a path matches a given string on a scale of 0->10000, a lower number means a better match.
|
||||||
|
The string should be present in the path, or this function will fail
|
||||||
|
|
||||||
|
The weight is calculated using the following formula:
|
||||||
|
0 00 0
|
||||||
|
| | |
|
||||||
|
Number of elements in the path <----------------| | |--------------------------> is a directory(0) or not(1)
|
||||||
|
(ie 2 for 'aa/bb' and 3 for 'aa/bb/cc') |
|
||||||
|
|
|
||||||
|
v
|
||||||
|
There is a word in the matched path element that exactly match the given string? ('aa_bb' matches user input 'aa' but doesn't match 'a')
|
||||||
|
|
|
||||||
|
No <--------------------|-----------------------------> yes
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
path element starts with the user input? There is only one word in the matched path element ('aa' but not 'aa_bb'),
|
||||||
|
| which means the path elements exactly match the string
|
||||||
|
| |
|
||||||
|
No <-------|-----Yes(2) No -----|----- Yes (0)
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
index of the string in the path element + 10 (index of the matched word within path element words + 1)
|
||||||
|
|
||||||
|
|
||||||
|
Maximums are added to make sure things doesn't overlap
|
||||||
|
"""
|
||||||
|
weight = 0
|
||||||
|
if not is_dir(path):
|
||||||
|
weight += 1
|
||||||
|
|
||||||
|
string = string.lower()
|
||||||
|
p = path.rstrip(os.sep).lower()
|
||||||
|
path_elems = p.split(os.sep)
|
||||||
|
weight += min(len(path_elems) * 1000, 10000) # Max 10 elements in path(set it to 10 for longer paths)
|
||||||
|
|
||||||
|
if not string:
|
||||||
|
return weight
|
||||||
|
|
||||||
|
elm = next(e for e in path_elems if string in e)
|
||||||
|
elm_words = [_f for _f in re.split('[\W_]+', elm) if _f]
|
||||||
|
if string in elm_words:
|
||||||
|
if len(elm_words) > 1:
|
||||||
|
weight += 10 * (min(elm_words.index(string),8) + 1) # Max 8 words per path entry
|
||||||
|
else:
|
||||||
|
if elm.startswith(string):
|
||||||
|
weight += 10 * 2
|
||||||
|
else:
|
||||||
|
weight += 10 * (10 + min(elm.index(string), 89)) # Max path element with 89 characters
|
||||||
|
return weight
|
||||||
|
|
||||||
|
# Helper functions:
|
||||||
|
|
||||||
|
def join_paths(p1, p2):
|
||||||
|
p1 = (os.path.expandvars(os.path.expanduser(p1)))
|
||||||
|
p2 = (os.path.expandvars(os.path.expanduser(p2)))
|
||||||
|
return os.path.normpath(os.path.join(p1, p2))
|
||||||
|
|
||||||
|
def _index_or_len(s, c):
|
||||||
|
if c in s:
|
||||||
|
return s.index(c)
|
||||||
|
else:
|
||||||
|
return len(s)
|
||||||
|
|
||||||
|
def is_dir(p):
|
||||||
|
if p.endswith(os.sep):
|
||||||
|
return True
|
||||||
|
return False
|
114
.qfc/qfc/dirhandler.py
Normal file
114
.qfc/qfc/dirhandler.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class CVSHandler():
|
||||||
|
""" Handler of CVS (fir, mercurial...) directories,
|
||||||
|
The main purpose of this class is to cache external cvs command output, and determine the appropriate files to yield when navigating to a subdirectory of a project.
|
||||||
|
This basically means that the external command is run once (ie git ls-files), cached, and when calling get_source_files on a subdirectory of the project root (ie project-root/subdir),
|
||||||
|
filtering from all project files of is done here.
|
||||||
|
"""
|
||||||
|
def __init__(self, cvs):
|
||||||
|
self._roots_cache = {}
|
||||||
|
self._not_tracked_cache = set()
|
||||||
|
self.cvs = cvs
|
||||||
|
|
||||||
|
def _get_root_from_cache(self, directory):
|
||||||
|
""" a directory is considered cached if it's the project root or a subdirectory of that project root.
|
||||||
|
returns the project root dir, or None if the directory is not cached.
|
||||||
|
"""
|
||||||
|
if directory in self._roots_cache:
|
||||||
|
return directory
|
||||||
|
if os.path.dirname(directory) == directory:
|
||||||
|
return None
|
||||||
|
return self._get_root_from_cache(os.path.dirname(directory))
|
||||||
|
|
||||||
|
def get_source_files(self, directory):
|
||||||
|
if directory in self._not_tracked_cache:
|
||||||
|
return None
|
||||||
|
|
||||||
|
root_dir = self._get_root_from_cache(directory)
|
||||||
|
if not root_dir:
|
||||||
|
try:
|
||||||
|
# check if it's a tracked cvs dir, if yes, get the project root and the source files
|
||||||
|
root_dir = self.cvs._get_root(directory)
|
||||||
|
self._roots_cache[root_dir] = self.cvs._get_tracked_files(root_dir)
|
||||||
|
except Exception as e:
|
||||||
|
# not a cvs tracked dir, save it to not issue that command again
|
||||||
|
self._not_tracked_cache.add(directory)
|
||||||
|
return None
|
||||||
|
|
||||||
|
files = self._roots_cache[root_dir]
|
||||||
|
# the passed directory argument is a subdirectory of the project root
|
||||||
|
if directory != root_dir:
|
||||||
|
rel_dir = os.path.relpath(directory, root_dir)
|
||||||
|
files = [f[len(rel_dir)+1:] for f in files if f.startswith(rel_dir)]
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
class Git():
|
||||||
|
@staticmethod
|
||||||
|
def _get_root(directory):
|
||||||
|
return run_command("cd %s && git rev-parse --show-toplevel" % directory).strip()
|
||||||
|
@staticmethod
|
||||||
|
def _get_tracked_files(directory):
|
||||||
|
return run_command("cd %s && git ls-files && git ls-files --others --exclude-standard" % directory).strip().split('\n')
|
||||||
|
|
||||||
|
class Mercurial():
|
||||||
|
@staticmethod
|
||||||
|
def _get_root(directory):
|
||||||
|
return run_command("cd %s && hg root" % directory).strip()
|
||||||
|
@staticmethod
|
||||||
|
def _get_tracked_files(directory):
|
||||||
|
return run_command("cd %s && (hg status -marcu | cut -d' ' -f2)" % directory).strip().split('\n')
|
||||||
|
|
||||||
|
class DefaultDirHandler():
|
||||||
|
""" The default directory handler uses the 'find' external program to return all the files inside a given directory up to MAX_depth depth (ie, if maxdepth=2, returns all files inside that dir, and all files in a subdir of that directory)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._cache = {}
|
||||||
|
self.MAX_DEPTH = 3
|
||||||
|
|
||||||
|
def _walk_down(self, start_dir):
|
||||||
|
try:
|
||||||
|
out = run_command("find %s -maxdepth %s -type f -not -path '*/\.*'" % (start_dir, self.MAX_DEPTH))
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# Find returns a non 0 exit status if listing a directory fails (for example, permission denied), but still output all files in other dirs
|
||||||
|
# ignore those failed directories.
|
||||||
|
out = e.output
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
out = out.decode('utf-8')
|
||||||
|
if not out:
|
||||||
|
return []
|
||||||
|
files = out.split('\n')
|
||||||
|
return [os.path.relpath(f, start_dir) for f in files if f]
|
||||||
|
|
||||||
|
def get_source_files(self, start_dir):
|
||||||
|
if not start_dir in self._cache:
|
||||||
|
self._cache[start_dir] = self._walk_down(start_dir)
|
||||||
|
return self._cache[start_dir]
|
||||||
|
|
||||||
|
def run_command(string):
|
||||||
|
''' fork a process to execute the command string given as argument, returning the string written to STDOUT '''
|
||||||
|
DEVNULL = open(os.devnull, 'wb')
|
||||||
|
out = subprocess.check_output(string, stderr=DEVNULL, shell=True)
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
return out.decode('utf-8')
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
git = CVSHandler(Git)
|
||||||
|
hg = CVSHandler(Mercurial)
|
||||||
|
default = DefaultDirHandler()
|
||||||
|
|
||||||
|
def get_source_files(directory):
|
||||||
|
""" check first if the given directory is inside a git tracked project, if no, check with mercurial, if no, fallback to the default handler """
|
||||||
|
files = git.get_source_files(directory)
|
||||||
|
# if the returned files list is empty, it's considered not a tracked directory
|
||||||
|
if files:
|
||||||
|
return files
|
||||||
|
files = hg.get_source_files(directory)
|
||||||
|
if files:
|
||||||
|
return files
|
||||||
|
return default.get_source_files(directory)
|
||||||
|
|
19
.qfc/qfc/keys.py
Normal file
19
.qfc/qfc/keys.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
CTRL_C = 3 # Ctrl-c
|
||||||
|
CTRL_H = 8 # ctrl-h
|
||||||
|
CTRL_J = 10 # ctrl-h
|
||||||
|
CTRL_K = 11 # ctrl-h
|
||||||
|
CTRL_L = 12 # ctrl-h
|
||||||
|
CTRL_F = 6 # ctrl-f
|
||||||
|
ENTER = 13 # Enter
|
||||||
|
CTRL_U = 21 # Ctrl+u
|
||||||
|
ESC = 27 # Escape
|
||||||
|
BACKSPACE = 127 # Backspace
|
||||||
|
TAB = 9 # Tab
|
||||||
|
RIGHT = -1 # FAKE CODE to abstract away the fact that a multibyte string is needed to represent arrow keys
|
||||||
|
DOWN = -2 # same
|
||||||
|
UP = -3 # same
|
||||||
|
LEFT = -4 # same
|
||||||
|
SHIFTTAB = -5 # same
|
||||||
|
SHIFTENTER = -6
|
||||||
|
SPACE = 32
|
||||||
|
ANTISLASH = 47
|
65
.qfc/qfc/readchar.py
Normal file
65
.qfc/qfc/readchar.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import sys
|
||||||
|
import tty
|
||||||
|
import termios
|
||||||
|
import fcntl
|
||||||
|
import os
|
||||||
|
from . import keys
|
||||||
|
|
||||||
|
|
||||||
|
def get_symbol():
|
||||||
|
''' Read a symbol, which can be a single byte character or a multibyte string'''
|
||||||
|
ch = read_char()
|
||||||
|
ch_code = ord(ch)
|
||||||
|
# check for multibyte string
|
||||||
|
if ch_code == keys.ESC:
|
||||||
|
ch = read_char_no_blocking()
|
||||||
|
if ch == '':
|
||||||
|
# ESC key pressed
|
||||||
|
return keys.ESC
|
||||||
|
elif ch != 'O' and ch != '[':
|
||||||
|
return ord(ch)
|
||||||
|
else:
|
||||||
|
ch = read_char_no_blocking()
|
||||||
|
if ch == 'A':
|
||||||
|
return keys.UP
|
||||||
|
elif ch == 'B':
|
||||||
|
return keys.DOWN
|
||||||
|
elif ch == 'C':
|
||||||
|
return keys.RIGHT
|
||||||
|
elif ch == 'D':
|
||||||
|
return keys.LEFT
|
||||||
|
elif ch == 'Z':
|
||||||
|
return keys.SHIFTTAB
|
||||||
|
return ch_code
|
||||||
|
|
||||||
|
|
||||||
|
def read_char():
|
||||||
|
''' Read a character '''
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd, termios.TCSADRAIN)
|
||||||
|
ch = sys.stdin.read(1)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
return ch
|
||||||
|
|
||||||
|
|
||||||
|
def read_char_no_blocking():
|
||||||
|
''' Read a character in nonblocking mode, if no characters are present in the buffer, return an empty string '''
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd, termios.TCSADRAIN)
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
|
||||||
|
return sys.stdin.read(1)
|
||||||
|
except IOError as e:
|
||||||
|
ErrorNumber = e[0]
|
||||||
|
# IOError with ErrorNumber 11(35 in Mac) is thrown when there is nothing to read(Resource temporarily unavailable)
|
||||||
|
if (sys.platform.startswith("linux") and ErrorNumber != 11) or (sys.platform == "darwin" and ErrorNumber != 35):
|
||||||
|
raise
|
||||||
|
return ""
|
||||||
|
finally:
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
60
.qfc/qfc/ui.py
Normal file
60
.qfc/qfc/ui.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
from . import ansi
|
||||||
|
import os
|
||||||
|
import math
|
||||||
|
|
||||||
|
def _get_terminal_columns():
|
||||||
|
''' get the number of terminal columns, used to determine spanned lines of a mark(reqansired for cursor placement) '''
|
||||||
|
_, columns = os.popen('stty size', 'r').read().split()
|
||||||
|
return int(columns)
|
||||||
|
|
||||||
|
def erase():
|
||||||
|
''' the commandline cursor is always at the first line (user prompt)
|
||||||
|
Therefore, erasing the current and following lines clear all output
|
||||||
|
'''
|
||||||
|
ansi.move_cursor_line_beggining()
|
||||||
|
ansi.erase_from_cursor_to_end()
|
||||||
|
|
||||||
|
def refresh(state):
|
||||||
|
''' Redraw the output, this function will be triggered on every user interaction(key pressed)'''
|
||||||
|
erase()
|
||||||
|
lines, num_rows = _construct_output(state)
|
||||||
|
for line in lines:
|
||||||
|
print(line)
|
||||||
|
# go up
|
||||||
|
ansi.move_cursor_previous_lines(num_rows)
|
||||||
|
# palce the cursor at the end of first line)
|
||||||
|
ansi.move_cursor_horizental(len(lines[0])+1)
|
||||||
|
ansi.flush()
|
||||||
|
|
||||||
|
def _construct_output(state):
|
||||||
|
columns = _get_terminal_columns()
|
||||||
|
def number_of_rows(line):
|
||||||
|
return int(math.ceil(float(len(line))/columns))
|
||||||
|
displayed_lines = []
|
||||||
|
# Number of terminal rows spanned by the output, used to determine how many lines we need to go up(to place the cursor after the prompt) after displaying the output
|
||||||
|
num_rows = 0
|
||||||
|
prompt_line = 'Path: ' + state.input
|
||||||
|
displayed_lines.append(prompt_line)
|
||||||
|
num_rows += number_of_rows(prompt_line)
|
||||||
|
matches = state.get_matches()
|
||||||
|
if matches:
|
||||||
|
# display commands from Max(0,selected_command_index - 10 +1 ) to Max(10,SelectedCommandIndex + 1)
|
||||||
|
selected_command_index = matches.index(state.get_selected_match())
|
||||||
|
matches_to_display = matches[max(0, selected_command_index - 10 + 1):max(10, selected_command_index + 1)]
|
||||||
|
for index, m in enumerate(matches_to_display):
|
||||||
|
fm = ' ' + m
|
||||||
|
num_rows += number_of_rows(fm)
|
||||||
|
# Formatting text(make searched word bold)
|
||||||
|
for w in state.input.split(' '):
|
||||||
|
if w:
|
||||||
|
fm = fm.replace(w, ansi.bold_text(w))
|
||||||
|
# highlighting selected command
|
||||||
|
if m == state.get_selected_match():
|
||||||
|
fm = ansi.select_text(fm)
|
||||||
|
displayed_lines.append(fm)
|
||||||
|
else:
|
||||||
|
not_found_line = 'Nothing found'
|
||||||
|
displayed_lines.append(not_found_line)
|
||||||
|
num_rows += number_of_rows(not_found_line)
|
||||||
|
return displayed_lines, num_rows
|
||||||
|
|
48
.qfc/tests/test_match.py
Normal file
48
.qfc/tests/test_match.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)),'..'))
|
||||||
|
from qfc.core import filter_files, get_weight
|
||||||
|
|
||||||
|
def _equals(marks_list1, marks_list2):
|
||||||
|
l1 = sorted(marks_list1)
|
||||||
|
l2 = sorted(marks_list2)
|
||||||
|
if len(l1) != len(l2):
|
||||||
|
return False
|
||||||
|
for i,_ in enumerate(l1):
|
||||||
|
if l1[i] != l2[i]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_filter_files():
|
||||||
|
files = [
|
||||||
|
'/',
|
||||||
|
'/a/',
|
||||||
|
'/b/',
|
||||||
|
'/a/b',
|
||||||
|
'/a/b/c',
|
||||||
|
'/b/a/',
|
||||||
|
'/b/a/c',
|
||||||
|
'd',
|
||||||
|
'da'
|
||||||
|
]
|
||||||
|
assert(_equals(filter_files(files,''), ['/','d','da']))
|
||||||
|
assert(_equals(filter_files(files,'/'), ['/']))
|
||||||
|
assert(_equals(filter_files(files,'a'), ['/a/', '/b/a/', 'da']))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_weight():
|
||||||
|
assert(get_weight('a','') == 1001)
|
||||||
|
assert(get_weight('a/','') == 1000)
|
||||||
|
assert(get_weight('a/b/','') == 2000)
|
||||||
|
assert(get_weight('a/b/c','') == 3001)
|
||||||
|
assert(get_weight('a','a') == 1001)
|
||||||
|
assert(get_weight('ab','a') == 1021)
|
||||||
|
assert(get_weight('bab','a') == 1111)
|
||||||
|
assert(get_weight('a_b','a') == 1011)
|
||||||
|
assert(get_weight('root/a_b','a') == 2011)
|
||||||
|
assert(get_weight('root/a_b_c_d_e_f_g_h_i_j_k','k') == 2091)
|
||||||
|
assert(get_weight('a/b/c/d/e/f/g/h/i/j/k','k') == 10001)
|
||||||
|
assert(get_weight('a/B/','b') == 2000)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue