class Repl(object): def __init__(self, options): self.found_files = [] self.opt = options def highlight_func(self, query_string): if termstyle is None: return lambda x: x highlight = Highlight(query_string) return lambda x: highlight.replace(x, green) def summarise(self, result_iter, query_string): subprocess.call(['clear']) self.found_files = [] i = 0 highlight = self.highlight_func(query_string) for filename, fullpath in result_iter: self.found_files.append(fullpath) relpath = os.path.split(fullpath)[0] explanation = '' if relpath: explanation = "(in %s)" % (relpath,) index = str(i+1).rjust(2) filename = filename.ljust(30) print " %s%s %s %s" % (yellow(index), yellow(":"), highlight(filename), black(explanation)) i += 1 def open(self, index): index -= 1 # indexes start at 1 for readability if len(self.found_files) <= index: logging.warning("no such index: %s" % (index,)) return filepath = self.found_files[index] self.opt.open(filepath) def _loop(self): q = raw_input(blue("\nfind/open file: ")) if len(q) == 0: q = 1 # open the first found file by default try: index = int(q) except ValueError: index = None if index is not None: self.open(index) else: self.finder.find(Search(q)) search = self.finder.results() self.summarise(search.results, search.text) def run(self): work_thread = threading.Thread(target=self._run, name="repl") work_thread.daemon = True work_thread.start() # the main thread is just going to wait till someone tells it to quit try: QUITTING_TIME.wait() except KeyboardInterrupt: # somehow the main thread fails to exit when it is the one # to receive KeyboardInterrupt ! pass def _run(self): self.finder = FileFinder(self.opt.base_path, path_filter=self.opt.path_filter, quit_indicator=QUITTING_TIME) logging.info("getting file list...") self.finder.populate() try: while True: self._loop() except (KeyboardInterrupt, EOFError): print return 0 except Exception: import traceback traceback.print_exc() finally: QUITTING_TIME.set()
class CursesUI(object): def __init__(self, options): self.opt = options self.status = "" self.ui_lock = threading.Lock() self.status_queue = Queue() self._num_files = 0 self.query = None def run(self): rootLogger = logging.getLogger() rootLogger.addHandler(QueueHandler(self.status_queue, level=logging.INFO)) logging.info("scanning ...") def _doit(): try: self.finder = FileFinder(self.opt.base_path, path_filter=self.opt.path_filter, quit_indicator=QUITTING_TIME) self.finder.populate() curses.wrapper(self._run) finally: QUITTING_TIME.set() work_thread = threading.Thread(target=log_exceptions(_doit), name="[curses] master") work_thread.start() # the main thread is just going to wait till someone tells it to quit try: QUITTING_TIME.wait() except KeyboardInterrupt: # somehow the main thread fails to exit when it is the one # to receive KeyboardInterrupt ! QUITTING_TIME.set() def refresh_results(self): self.set_query(self.query) @log_exceptions def results_loop(self): while True: search = self.finder.results() self.ui_lock.acquire() self.set_results(search) self.update() self.ui_lock.release() @log_exceptions def status_loop(self): if(QUITTING_TIME.is_set()): return def _stat(msg): self.ui_lock.acquire() self.status = msg self.update() self.ui_lock.release() while True: try: status_msg = self.status_queue.get(timeout=1.2) _stat(status_msg) sleep(1) except Empty: pass if self.status_queue.empty(): num_files = self.update_files_indexed() _stat("%s files indexed" % (num_files,)) def update_files_indexed(self): new_num_files = self.finder.file_count if self._num_files != new_num_files: self.requery_if_doing_nothing() self._num_files = new_num_files return new_num_files def requery_if_doing_nothing(self): if not self.finder.has_pending_queries: logging.debug("resubmitting query: %s" % (self.query,)) self.set_query(self.query, is_repeat = True) @log_exceptions def _run(self, mainscr): self.mainscr = mainscr self._init_colors() self._init_screens() self._init_input() self.update() display_thread = threading.Thread(target=self.results_loop, name="[curses] results handler") status_thread = threading.Thread(target=self.status_loop, name="[curses] status updater") display_thread.daemon = True status_thread.daemon = True display_thread.start() status_thread.start() self._input_loop() import time #time.sleep(0.1) # random sleep, otherwise curses sometimes calls knickers.twist() def _init_colors(self): global A_INPUT, A_FILENAME, A_PATH, A_HIGHLIGHT, A_ERR, A_PROMPT, A_STATUS curses.use_default_colors() curses.curs_set(1) # line (input) cursor A_INPUT = curses.A_REVERSE n_filename = 1 n_path = 2 n_hi = 3 n_err = 4 n_prompt = 5 n_status = 6 bg_index = -1 curses.init_pair(n_filename, curses.COLOR_WHITE, bg_index) curses.init_pair(n_path, curses.COLOR_BLUE, bg_index) curses.init_pair(n_hi, curses.COLOR_GREEN, bg_index) curses.init_pair(n_err, curses.COLOR_WHITE, curses.COLOR_RED) curses.init_pair(n_prompt, curses.COLOR_BLUE, bg_index) curses.init_pair(n_status, curses.COLOR_BLACK, bg_index) A_FILENAME = curses.color_pair(n_filename) A_PATH = curses.color_pair(n_path) A_HIGHLIGHT = curses.color_pair(n_hi) | curses.A_BOLD A_ERR = curses.color_pair(n_err) | curses.A_BOLD A_PROMPT = curses.color_pair(n_prompt) A_STATUS = curses.color_pair(n_status) def _init_screens(self): self.win_height, self.win_width = self.mainscr.getmaxyx() self.input_win = curses.newwin(1, self.win_width, 0, 0) self.results_win = curses.newpad(MAX_RESULTS, self.win_width) self.status_win = curses.newwin(1, self.win_width, self.win_height-1, 0) #IMPORTANT: input_win *must* be the last, so that it gets redrawed # last (and therefore gets the cursor) self.screens = (self.results_win, self.status_win, self.input_win) def resize(self): self._init_screens() def _init_input(self): curses.raw() self.results = [] self.input_position = 0 self.selected = 0 self.results_scroll = 0 self.set_query("") def update(self): if (self.win_height, self.win_width) != self.mainscr.getmaxyx(): logging.debug("resizing...") self.resize() self.draw_input() self.draw_results() self.draw_status() self._redraw() def draw_input(self): self.input_win.clear() find_text = "Find: " self.input_win.addnstr(0,0, find_text, self.win_width, A_PROMPT) self.input_win.addnstr(0, len(find_text), self.query, self.win_width, A_INPUT) self.input_win.bkgdset(' ', curses.A_REVERSE) self.input_win.move(0,self.input_position + len(find_text)) def draw_results(self): #TODO: scroll results buffer linepos = 0 indent_width = 6 filename_len = min(int(self.win_width / 1.5), 50) path_len = self.win_width - filename_len - 1 - indent_width self.results_win.clear() for file, path in self.results: attr_mod = curses.A_REVERSE if linepos == self.selected else curses.A_NORMAL drawn_chars = 0 remaining_chars = filename_len for highlighted, segment in self.highlight(file): attrs = A_FILENAME | A_HIGHLIGHT if highlighted else A_FILENAME self.results_win.insnstr(linepos, indent_width + drawn_chars, segment, remaining_chars, attrs | attr_mod) drawn_chars += len(segment) remaining_chars -= len(segment) if remaining_chars <= 0: break # now draw the path relpath = os.path.split(path)[0] explanation = '' if relpath: explanation = "(in %s)" % (relpath,) self.results_win.insnstr(linepos, indent_width + filename_len + 1, explanation, path_len, A_PATH) linepos += 1 if linepos >= MAX_RESULTS: break if linepos == 0 and len(self.query) >= MIN_QUERY and not self.finder.has_pending_queries: self.results_win.insnstr(linepos, indent_width, 'No Matches...', self.win_width - indent_width, A_ERR) def draw_status(self): self.status_win.clear() self.status_win.insnstr(0, 0, self.status, self.win_width, A_STATUS) def with_selected(self, func): index = self.selected if len(self.results) <= index: logging.warning("no such index: %s" % (index,)) self.clear_status() return filepath = self.results[index][-1] func(filepath) def open_selected(self): def action(path): self.opt.open(path) self.flash(" ** opened **") self.with_selected(action) def select(self, amount): self.ui_lock.acquire() if amount == NEXT or amount == PREVIOUS: self.selected += amount elif amount == START: self.selected = 0 elif amount == END: self.selected = len(self.results)-1 self.selected = min(self.selected, len(self.results)-1) self.selected = max(self.selected, 0) self.ui_lock.release() def set_query(self, new_query, is_repeat = False): if len(new_query) >= MIN_QUERY: self.finder.find(Search(new_query, is_repeat=is_repeat)) else: self.finder.find(None) self.ui_lock.acquire() self.query = new_query if self.input_position > len(self.query): self.input_position = len(self.query) self.ui_lock.release() def set_results(self, search): self.highlight = Highlight(search.text) self.results = list(search.results) if search.is_repeat: self.selected = min(self.selected, len(self.results)-1) else: self.selected = 0 def add_char(self, ch): new_query = self.modify_query_as_list(lambda q: q.insert(self.input_position, ch)) self.input_position += 1 self.set_query(new_query) logging.debug("query = %s" % (self.query, )) def modify_query_as_list(self, proc): query_list = list(self.query) proc(query_list) return ''.join(query_list) def remove_char(self, forwards=False): letter_index = self.input_position if forwards else self.input_position - 1 if letter_index >= len(self.query) or letter_index < 0: return new_query = self.modify_query_as_list(lambda q: q.pop(letter_index)) if forwards: self.input_position = max(self.input_position, len(self.query)) elif self.input_position > 0: self.input_position -= 1 self.set_query(new_query) def move_cursor(self, backwards=False): offset = -1 if backwards else 1 self.input_position += offset self.input_position = max(0, min(self.input_position, len(self.query))) def move_cursor_to(self, index): self.input_position = index def flash(self, str): self.status_queue.put(str) def clear_status(self): self.status_queue.put("") def _redraw(self, *screens): if QUITTING_TIME.is_set(): return logging.debug("redrawing...") if not screens: screens = self.screens for scr in screens: if scr is self.results_win: scr.noutrefresh( self.results_scroll, 0, 1, 0, self.win_height-2, self.win_width) else: scr.noutrefresh() curses.doupdate() def copy_selected_path_to_clipboard(self): def action(filepath): try: import pyperclip pyperclip.copy(self.opt.full_path(filepath)) self.flash(" ** copied **") except StandardError, e: logging.warn("error: %s" % (e,)) logging.exception("error copying to clipboard") self.with_selected(action)