addet qfc not as a submodule
This commit is contained in:
parent
cc8e05e3d5
commit
bbf0120ad6
13 changed files with 806 additions and 5 deletions
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
|
Loading…
Add table
Add a link
Reference in a new issue