pax_global_header00006660000000000000000000000064125563655620014531gustar00rootroot0000000000000052 comment=831e9c5e41f79885f89d88a012fe44c28301d750 percol-0.2.1/000077500000000000000000000000001255636556200130155ustar00rootroot00000000000000percol-0.2.1/.gitignore000066400000000000000000000001101255636556200147750ustar00rootroot00000000000000*.pyc #*# .*~ *~ Thumbs.db .DS_Store tags build/ dist/ percol.egg-info/ percol-0.2.1/README.md000066400000000000000000000326051255636556200143020ustar00rootroot00000000000000# percol __ ____ ___ ______________ / / / __ \/ _ \/ ___/ ___/ __ \/ / / /_/ / __/ / / /__/ /_/ / / / .___/\___/_/ \___/\____/_/ /_/ percol adds flavor of interactive selection to the traditional pipe concept on UNIX. - [What's this](#whats-this) - [Features](#features) - [Related projects](#related-projects) - [Installation](#installation) - [PyPI](#pypi) - [Manual](#manual) - [Usage](#usage) - [Example](#example) - [Interactive pgrep / pkill](#interactive-pgrep--pkill) - [zsh history search](#zsh-history-search) - [tmux](#tmux) - [Configuration](#configuration) - [Customizing prompt](#customizing-prompt) - [Dynamic prompt](#dynamic-prompt) - [Custom format specifiers](#custom-format-specifiers) - [Customizing styles](#customizing-styles) - [Foreground Colors](#foreground-colors) - [Background Color](#background-color) - [Attributes](#attributes) - [Matching Method](#matching-method) - [Migemo support](#migemo-support) - [Dictionary settings](#dictionary-settings) - [Minimum query length](#minimum-query-length) - [Pinyin support](#pinyin-support) - [Switching matching method dynamically](#switching-matching-method-dynamically) - [Tips](#tips) - [Selecting multiple candidates](#selecting-multiple-candidates) - [Z Shell support](#z-shell-support) ## What's this ![optimized](http://mooz.github.io/percol/percol_overview.gif) percol is an **interactive grep tool** in your terminal. percol 1. receives input lines from `stdin` or a file, 2. lists up the input lines, 3. waits for your input that filter/select the line(s), 4. and finally outputs the selected line(s) to `stdout`. Since percol just filters the input and output the result to stdout, it can be used in command-chains with `|` in your shell (**UNIX philosophy!**). ### Features - **Efficient**: With **lazy loads** of input lines and **query caching**, percol handles huge inputs efficiently. - **Customizable**: Through configuration file (`rc.py`), percol's behavior including prompts, keymaps, and color schemes can be **heavily customizable**. - See [configuration](https://github.com/mooz/percol#configuration) for details. - **Migemo support**: By supporting [C/Migemo](http://code.google.com/p/cmigemo/), **percol filters Japanese inputs blazingly fast**. - See [matching method](https://github.com/mooz/percol#matching-method) for details. ### Related projects - [canything by @keiji0](https://github.com/keiji0/canything) - A seminal work in interactive grep tools. - [zaw by @nakamuray](https://github.com/zsh-users/zaw) - A zsh-friendly interactive grep tool. - [peco by @lestrrat](https://github.com/lestrrat/peco) - An interactive grep tool written in Go language. - [fzf by @junegunn](https://github.com/junegunn/fzf) - An interactive grep tool written in Go language. ## Installation percol currently supports only Python 2.x. ### PyPI $ sudo pip install percol ### Manual First, clone percol repository and go into the directory. $ git clone git://github.com/mooz/percol.git $ cd percol Then, run a command below. $ sudo python setup.py install If you don't have a root permission (or don't wanna install percol with sudo), try next one. $ python setup.py install --prefix=~/.local $ export PATH=~/.local/bin:$PATH ## Usage Specifying a filename. $ percol /var/log/syslog Specifying a redirection. $ ps aux | percol ## Example ### Interactive pgrep / pkill Here is an interactive version of pgrep, $ ps aux | percol | awk '{ print $2 }' and here is an interactive version of pkill. $ ps aux | percol | awk '{ print $2 }' | xargs kill For zsh users, command versions are here (`ppkill` accepts options like `-9`). ```sh function ppgrep() { if [[ $1 == "" ]]; then PERCOL=percol else PERCOL="percol --query $1" fi ps aux | eval $PERCOL | awk '{ print $2 }' } function ppkill() { if [[ $1 =~ "^-" ]]; then QUERY="" # options only else QUERY=$1 # with a query [[ $# > 0 ]] && shift fi ppgrep $QUERY | xargs kill $* } ``` ### zsh history search In your `.zshrc`, put the lines below. ```sh function exists { which $1 &> /dev/null } if exists percol; then function percol_select_history() { local tac exists gtac && tac="gtac" || { exists tac && tac="tac" || { tac="tail -r" } } BUFFER=$(fc -l -n 1 | eval $tac | percol --query "$LBUFFER") CURSOR=$#BUFFER # move cursor zle -R -c # refresh } zle -N percol_select_history bindkey '^R' percol_select_history fi ``` Then, you can display and search your zsh histories incrementally by pressing `Ctrl + r` key. ### tmux Here are some examples of tmux and percol integration. bind b split-window "tmux lsw | percol --initial-index $(tmux lsw | awk '/active.$/ {print NR-1}') | cut -d':' -f 1 | tr -d '\n' | xargs -0 tmux select-window -t" bind B split-window "tmux ls | percol --initial-index $(tmux ls | awk \"/^$(tmux display-message -p '#{session_name}'):/ {print NR-1}\") | cut -d':' -f 1 | tr -d '\n' | xargs -0 tmux switch-client -t" By putting above 2 settings into `tmux.conf`, you can select a tmux window with `${TMUX_PREFIX} b` keys and session with `${TMUX_PREFIX} B` keys. Attaching to running tmux sessions can also be made easier with percol with this function(tested to work in bash and zsh) ```sh function pattach() { if [[ $1 == "" ]]; then PERCOL=percol else PERCOL="percol --query $1" fi sessions=$(tmux ls) [ $? -ne 0 ] && return session=$(echo $sessions | eval $PERCOL | cut -d : -f 1) if [[ -n "$session" ]]; then tmux att -t $session fi } ``` ## Configuration Configuration file for percol should be placed under `${HOME}/.percol.d/` and named `rc.py`. Here is an example `~/.percol.d/rc.py`. ```python # X / _ / X percol.view.PROMPT = ur"X / _ / X %q" # Emacs like percol.import_keymap({ "C-h" : lambda percol: percol.command.delete_backward_char(), "C-d" : lambda percol: percol.command.delete_forward_char(), "C-k" : lambda percol: percol.command.kill_end_of_line(), "C-y" : lambda percol: percol.command.yank(), "C-t" : lambda percol: percol.command.transpose_chars(), "C-a" : lambda percol: percol.command.beginning_of_line(), "C-e" : lambda percol: percol.command.end_of_line(), "C-b" : lambda percol: percol.command.backward_char(), "C-f" : lambda percol: percol.command.forward_char(), "M-f" : lambda percol: percol.command.forward_word(), "M-b" : lambda percol: percol.command.backward_word(), "M-d" : lambda percol: percol.command.delete_forward_word(), "M-h" : lambda percol: percol.command.delete_backward_word(), "C-n" : lambda percol: percol.command.select_next(), "C-p" : lambda percol: percol.command.select_previous(), "C-v" : lambda percol: percol.command.select_next_page(), "M-v" : lambda percol: percol.command.select_previous_page(), "M-<" : lambda percol: percol.command.select_top(), "M->" : lambda percol: percol.command.select_bottom(), "C-m" : lambda percol: percol.finish(), "C-j" : lambda percol: percol.finish(), "C-g" : lambda percol: percol.cancel(), }) ``` ### Customizing prompt In percol, a prompt consists of two part: _PROMPT_ and _RPROMPT_, like zsh. As the following example shows, each part appearance can be customized by specifying a prompt format into `percol.view.PROMPT` and `percol.view.RPROMPT` respectively. ```python percol.view.PROMPT = ur"Input: %q" percol.view.RPROMPT = ur"(%F) [%i/%I]" ``` In prompt formats, a character preceded by `%` indicates a _prompt format specifier_ and is expanded into a corresponding system value. - `%%` - Display `%` itself - `%q` - Display query and caret - `%Q` - Display query without caret - `%n` - Page number - `%N` - Total page number - `%i` - Current line number - `%I` - Total line number - `%c` - Caret position - `%k` - Last input key #### Dynamic prompt By changing percol.view.PROMPT into a getter, percol prompts becomes more fancy. ```python # Change prompt in response to the status of case sensitivity percol.view.__class__.PROMPT = property( lambda self: ur"QUERY [a]: %q" if percol.model.finder.case_insensitive else ur"QUERY [A]: %q" ) ``` #### Custom format specifiers ```python # Display finder name in RPROMPT percol.view.prompt_replacees["F"] = lambda self, **args: self.model.finder.get_name() percol.view.RPROMPT = ur"(%F) [%i/%I]" ``` ### Customizing styles For now, styles of following 4 items can be customized in `rc.py`. ```python percol.view.CANDIDATES_LINE_BASIC = ("on_default", "default") percol.view.CANDIDATES_LINE_SELECTED = ("underline", "on_yellow", "white") percol.view.CANDIDATES_LINE_MARKED = ("bold", "on_cyan", "black") percol.view.CANDIDATES_LINE_QUERY = ("yellow", "bold") ``` Each RHS is a tuple of style specifiers listed below. #### Foreground Colors - `"black"` for `curses.COLOR_BLACK` - `"red"` for `curses.COLOR_RED` - `"green"` for `curses.COLOR_GREEN` - `"yellow"` for `curses.COLOR_YELLOW` - `"blue"` for `curses.COLOR_BLUE` - `"magenta"` for `curses.COLOR_MAGENTA` - `"cyan"` for `curses.COLOR_CYAN` - `"white"` for `curses.COLOR_WHITE` #### Background Color - `"on_black"` for `curses.COLOR_BLACK` - `"on_red"` for `curses.COLOR_RED` - `"on_green"` for `curses.COLOR_GREEN` - `"on_yellow"` for `curses.COLOR_YELLOW` - `"on_blue"` for `curses.COLOR_BLUE` - `"on_magenta"` for `curses.COLOR_MAGENTA` - `"on_cyan"` for `curses.COLOR_CYAN` - `"on_white"` for `curses.COLOR_WHITE` #### Attributes - `"altcharset"` for `curses.A_ALTCHARSET` - `"blink"` for `curses.A_BLINK` - `"bold"` for `curses.A_BOLD` - `"dim"` for `curses.A_DIM` - `"normal"` for `curses.A_NORMAL` - `"standout"` for `curses.A_STANDOUT` - `"underline"` for `curses.A_UNDERLINE` - `"reverse"` for `curses.A_REVERSE` ## Matching Method By default, percol interprets input queries by users as **string**. If you prefer **regular expression**, try `--match-method` command line option. $ percol --match-method regex ### Migemo support percol supports **migemo** (http://0xcc.net/migemo/) matching, which allows us to search Japanese documents with ASCII characters. $ percol --match-method migemo To use this feature, you need to install C/Migemo (https://github.com/koron/cmigemo). In Ubuntu, it's simple: $ sudo apt-get install cmigemo After that, by specifying a command line argument `--match-method migemo`, you can use migemo in percol. NOTE: This feature uses `python-cmigemo` package (https://github.com/mooz/python-cmigemo). Doing `pip install percol` also installs this package too. #### Dictionary settings By default, percol assumes the path of a dictionary for migemo is `/usr/local/share/migemo/utf-8/migemo-dict`. If the dictionary is located in a different place, you should tell the location via `rc.py`. For example, if the path of the dictionary is `/path/to/a/migemo-dict`, put lines below into your `rc.py`. ```python from percol.finder import FinderMultiQueryMigemo FinderMultiQueryMigemo.dictionary_path = "/path/to/a/migemo-dict" ``` #### Minimum query length If the query length is **too short**, migemo generates **very long** regular expression. To deal with this problem, percol does not pass a query if the length of the query is shorter than **2** and treat the query as raw regular expression. To change this behavior, change the value of `FinderMultiQueryMigemo.minimum_query_length` like following settings. ```python from percol.finder import FinderMultiQueryMigemo FinderMultiQueryMigemo.minimum_query_length = 1 ``` ### Pinyin support Now percol supports **pinyin** (http://en.wikipedia.org/wiki/Pinyin) for matching Chinese characters. $ percol --match-method pinyin In this matching method, first char of each Chinese character's pinyin sequence is used for matching. For example, 'zw' matches '中文' (ZhongWen), '中午'(ZhongWu), '作为' (ZuoWei) etc. Extra package pinin(https://pypi.python.org/pypi/pinyin/0.2.5) needed. ### Switching matching method dynamically Matching method can be switched dynamically (at run time) by executing `percol.command.specify_finder(FinderClass)` or `percol.command.toggle_finder(FinderClass)`. In addition, `percol.command.specify_case_sensitive(case_sensitive)` and `percol.command.toggle_case_sensitive()` change the matching status of case sensitivity. ```python from percol.finder import FinderMultiQueryMigemo, FinderMultiQueryRegex percol.import_keymap({ "M-c" : lambda percol: percol.command.toggle_case_sensitive(), "M-m" : lambda percol: percol.command.toggle_finder(FinderMultiQueryMigemo), "M-r" : lambda percol: percol.command.toggle_finder(FinderMultiQueryRegex) }) ``` ## Tips ### Selecting multiple candidates You can select and let percol to output multiple candidates by `percol.command.toggle_mark_and_next()` (which is bound to `C-SPC` by default). `percol.command.mark_all()`, `percol.command.unmark_all()` and `percol.command.toggle_mark_all()` are useful to mark / unmark all candidates at once. ## Z Shell support A zsh completing-function for percol is available in https://github.com/mooz/percol/blob/master/tools/zsh/_percol . percol-0.2.1/bin/000077500000000000000000000000001255636556200135655ustar00rootroot00000000000000percol-0.2.1/bin/percol000077500000000000000000000004631255636556200150020ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os, sys # add load path if __name__ == '__main__': libdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) if os.path.exists(os.path.join(libdir, "percol")): sys.path.insert(0, libdir) from percol.cli import main main() percol-0.2.1/bin/sample/000077500000000000000000000000001255636556200150465ustar00rootroot00000000000000percol-0.2.1/bin/sample/open_dropbox_pdf.sh000077500000000000000000000002341255636556200207330ustar00rootroot00000000000000#!/bin/sh gnome-terminal --title "Find PDF files" --hide-menubar \ -x sh -c "find ~/Dropbox/ -name \"*.pdf\" -type f | percol --quote | xargs evince" percol-0.2.1/debian/000077500000000000000000000000001255636556200142375ustar00rootroot00000000000000percol-0.2.1/debian/changelog000066400000000000000000000002231255636556200161060ustar00rootroot00000000000000python-percol (0.1-1) unstable; urgency=low * Initial public release -- Kozo Nishida Sun, 23 Aug 2014 4:15:00 +0200 percol-0.2.1/debian/compat000066400000000000000000000000021255636556200154350ustar00rootroot000000000000009 percol-0.2.1/debian/control000066400000000000000000000013001255636556200156340ustar00rootroot00000000000000Source: python-percol Section: python Priority: extra Maintainer: Kozo Nishida Build-Depends: debhelper (>= 9), python Standards-Version: 3.9.5 Package: python-percol Architecture: all Description: percol adds flavor of interactive selection to the traditional pipe concept on UNIX. percol is an interactive grep tool in your terminal. percol 1. receives input lines from stdin or a file, 2. lists up the input lines, 3. waits for your input that filter/select the line(s), 4. and finally outputs the selected line(s) to stdout. Since percol just filters the input and output the result to stdout, it can be used in command-chains with | in your shell (UNIX philosophy!). percol-0.2.1/debian/rules000077500000000000000000000000551255636556200153170ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with python2 percol-0.2.1/percol/000077500000000000000000000000001255636556200143015ustar00rootroot00000000000000percol-0.2.1/percol/__init__.py000066400000000000000000000221551255636556200164170ustar00rootroot00000000000000# -*- coding: utf-8 -*- import percol.info __doc__ = info.__doc__ __version__ = info.__version__ __logo__ = info.__logo__ import sys import signal import curses import threading import six from percol import debug, action from percol.display import Display from percol.finder import FinderMultiQueryString from percol.key import KeyHandler from percol.model import SelectorModel from percol.view import SelectorView from percol.command import SelectorCommand class TerminateLoop(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class Percol(object): def __init__(self, descriptors = None, encoding = "utf-8", finder = None, action_finder = None, candidates = None, actions = None, query = None, caret = None, index = None): # initialization self.global_lock = threading.Lock() self.encoding = encoding if descriptors is None: self.stdin = sys.stdin self.stdout = sys.stdout self.stderr = sys.stderr else: self.stdin = descriptors["stdin"] self.stdout = descriptors["stdout"] self.stderr = descriptors["stderr"] if finder is None: finder = FinderMultiQueryString if action_finder is None: action_finder = FinderMultiQueryString self.actions = actions # wraps candidates (iterator) from percol.lazyarray import LazyArray self.candidates = LazyArray(candidates or []) # create model self.model_candidate = SelectorModel(percol = self, collection = self.candidates, finder = finder, query = query, caret = caret, index = index) self.model_action = SelectorModel(percol = self, collection = [action.desc for action in actions], finder = action_finder) self.model = self.model_candidate def has_no_candidate(self): return not self.candidates.has_nth_value(0) def has_only_one_candidate(self): return self.candidates.has_nth_value(0) and not self.candidates.has_nth_value(1) def __enter__(self): # init curses and it's wrapper self.screen = curses.initscr() self.display = Display(self.screen, self.encoding) # create keyhandler self.keyhandler = key.KeyHandler(self.screen) # create view self.view = SelectorView(percol = self) # create command self.command_candidate = SelectorCommand(self.model_candidate, self.view) self.command_action = SelectorCommand(self.model_action, self.view) # suppress SIGINT termination signal.signal(signal.SIGINT, lambda signum, frame: None) # handle special keys like , , ... self.screen.keypad(True) curses.raw() curses.noecho() curses.cbreak() curses.nonl() return self def __exit__(self, exc_type, exc_value, traceback): curses.endwin() self.execute_action() args_for_action = None def execute_action(self): selected_actions = self.model_action.get_selected_results_with_index() if selected_actions and self.args_for_action: for name, _, act_idx in selected_actions: try: action = self.actions[act_idx] if action: action.act([arg for arg, _, _ in self.args_for_action], self) except Exception as e: debug.log("execute_action", e) # ============================================================ # # Statuses # ============================================================ # @property def opposite_model(self): """ Returns opposite model for self.model """ if self.model is self.model_action: return self.model_candidate else: return self.model_action def switch_model(self): self.model = self.opposite_model @property def command(self): """ Returns corresponding model wrapper which provides advanced commands """ if self.model is self.model_action: return self.command_action else: return self.command_candidate # ============================================================ # # Main Loop # ============================================================ # SEARCH_DELAY = 0.05 def loop(self): self.view.refresh_display() self.result_updating_timer = None def search_and_refresh_display(): self.model.do_search(self.model.query) self.view.refresh_display() while True: try: self.handle_key(self.screen.getch()) if self.model.should_search_again(): # search again with self.global_lock: # critical section if not self.result_updating_timer is None: # clear timer self.result_updating_timer.cancel() self.result_updating_timer = None # with bounce t = threading.Timer(self.SEARCH_DELAY, search_and_refresh_display) self.result_updating_timer = t t.start() self.view.refresh_display() except TerminateLoop as e: return e.value # ============================================================ # # Key Handling # ============================================================ # keymap = { "C-i" : lambda percol: percol.switch_model(), # text "C-h" : lambda percol: percol.command.delete_backward_char(), "" : lambda percol: percol.command.delete_backward_char(), "C-w" : lambda percol: percol.command.delete_backward_word(), "C-u" : lambda percol: percol.command.clear_query(), "" : lambda percol: percol.command.delete_forward_char(), # caret "" : lambda percol: percol.command.backward_char(), "" : lambda percol: percol.command.forward_char(), # line "" : lambda percol: percol.command.select_next(), "" : lambda percol: percol.command.select_previous(), # page "" : lambda percol: percol.command.select_next_page(), "" : lambda percol: percol.command.select_previous_page(), # top / bottom "" : lambda percol: percol.command.select_top(), "" : lambda percol: percol.command.select_bottom(), # mark "C-SPC" : lambda percol: percol.command.toggle_mark_and_next(), # finish "RET" : lambda percol: percol.finish(), # Is RET never sent? "C-m" : lambda percol: percol.finish(), "C-j" : lambda percol: percol.finish(), "C-c" : lambda percol: percol.cancel() } def import_keymap(self, keymap, reset = False): if reset: self.keymap = {} else: self.keymap = dict(self.keymap) for key, cmd in six.iteritems(keymap): self.keymap[key] = cmd # default last_key = None def handle_key(self, ch): if ch == curses.KEY_RESIZE: self.last_key = self.handle_resize(ch) elif ch != -1 and self.keyhandler.is_utf8_multibyte_key(ch): self.last_key = self.handle_utf8(ch) else: self.last_key = self.handle_normal_key(ch) def handle_resize(self, ch): self.display.update_screen_size() # XXX: trash -1 (it seems that resize key sends -1) self.keyhandler.get_key_for(self.screen.getch()) return key.SPECIAL_KEYS[ch] def handle_utf8(self, ch): ukey = self.keyhandler.get_utf8_key_for(ch) self.model.insert_string(ukey) return ukey.encode(self.encoding) def handle_normal_key(self, ch): k = self.keyhandler.get_key_for(ch) if k in self.keymap: self.keymap[k](self) elif self.keyhandler.is_displayable_key(ch): self.model.insert_char(ch) return k # ------------------------------------------------------------ # # Finish / Cancel # ------------------------------------------------------------ # def finish(self): # save selected candidates and use them later (in execute_action) raise TerminateLoop(self.finish_with_exit_code()) # success def cancel(self): raise TerminateLoop(self.cancel_with_exit_code()) # failure def finish_with_exit_code(self): self.args_for_action = self.model_candidate.get_selected_results_with_index() return 0 def cancel_with_exit_code(self): return 1 percol-0.2.1/percol/action.py000066400000000000000000000006421255636556200161320ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ============================================================ # # Action # ============================================================ # class Action(object): def __init__(self, desc, act, args): self.act = act self.desc = desc self.args = args def action(**args): def act_handler(act): return Action(act.__doc__, act, args) return act_handler percol-0.2.1/percol/actions.py000066400000000000000000000014621255636556200163160ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys, six from percol.action import action def double_quote_string(string): return '"' + string.replace('"', r'\"') + '"' def get_raw_stream(stream): if six.PY2: return stream else: return stream.buffer @action() def output_to_stdout(lines, percol): "output marked (selected) items to stdout" stdout = get_raw_stream(sys.stdout) for line in lines: stdout.write(percol.display.get_raw_string(line)) stdout.write(six.b("\n")) @action() def output_to_stdout_double_quote(lines, percol): "output marked (selected) items to stdout with double quotes" stdout = get_raw_stream(sys.stdout) for line in lines: stdout.write(percol.display.get_raw_string(double_quote_string(line))) stdout.write(six.b("\n")) percol-0.2.1/percol/ansi.py000066400000000000000000000044711255636556200156130ustar00rootroot00000000000000# -*- coding: utf-8 -*- from percol.markup import MarkupParser import sys import re # http://graphcomp.com/info/specs/ansi_col.html DISPLAY_ATTRIBUTES = { "reset" : 0, "bold" : 1, "bright" : 1, "dim" : 2, "underline" : 4, "underscore" : 4, "blink" : 5, "reverse" : 7, "hidden" : 8, # Colors "black" : 30, "red" : 31, "green" : 32, "yellow" : 33, "blue" : 34, "magenta" : 35, "cyan" : 36, "white" : 37, "on_black" : 40, "on_red" : 41, "on_green" : 42, "on_yellow" : 43, "on_blue" : 44, "on_magenta" : 45, "on_cyan" : 46, "on_white" : 47, } markup_parser = MarkupParser() def markup(string): return decorate_parse_result(markup_parser.parse(string)) def remove_escapes(string): return re.sub(r"\x1B\[(?:[0-9]{1,2}(?:;[0-9]{1,2})?)?[m|K]", "", string) def decorate_parse_result(parse_result): decorated_string = "" for (fragment_string, attributes) in parse_result: decorated_string += decorate_string_with_attributes(fragment_string, attributes) return decorated_string def decorate_string_with_attributes(string, attributes): attribute_numbers = attribute_names_to_numbers(attributes) attribute_format = ";".join(attribute_numbers) return "\033[{0}m{1}\033[0m".format(attribute_format, string) def attribute_names_to_numbers(attribute_names): return [str(DISPLAY_ATTRIBUTES[name]) for name in attribute_names if name in DISPLAY_ATTRIBUTES] if __name__ == "__main__": tests = ( "hello", "hello red normal", "hello with background green this is underline and red then, normal", "baaaaaaaaaaaaaaaa", # unmatch "baaaaaaaaaaaaaaaa", "hello \\red\\ normal", # escape u"マルチバイト文字テスト", # multibyte ) for test in tests: try: print("----------------------------------------------------------") print(markup(test)) except Exception as e: print("fail: " + str(e)) percol-0.2.1/percol/cli.py000066400000000000000000000263501255636556200154300ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys import os import locale import six from optparse import OptionParser import percol from percol import Percol from percol import tty from percol import debug from percol import ansi INSTRUCTION_TEXT = ansi.markup("""{logo} {version} You did not give any inputs to percol. Check following typical usages and try again. (1) Giving a filename, $ percol /var/log/syslog (2) or specifying a redirection. $ ps aux | percol """).format(logo = percol.__logo__, version = percol.__version__) class LoadRunCommandFileError(Exception): def __init__(self, error): self.error = error def __str__(self): return "Error in rc.py: " + str(self.error) CONF_ROOT_DIR = os.path.expanduser("~/.percol.d/") DEFAULT_CONF_PATH = CONF_ROOT_DIR + "rc.py" def create_default_rc_file(): if not os.path.exists(CONF_ROOT_DIR): os.makedirs(CONF_ROOT_DIR) with open(DEFAULT_CONF_PATH, "w+") as file: file.write("# Run command file for percol\n") def load_rc(percol, path = None, encoding = 'utf-8'): if path is None: if not os.path.exists(DEFAULT_CONF_PATH): create_default_rc_file() path = DEFAULT_CONF_PATH try: with open(path, "rb") as file: exec(compile(file.read(), path, 'exec'), locals()) except Exception as e: raise LoadRunCommandFileError(e) def eval_string(percol, string_to_eval, encoding = 'utf-8'): try: import six if not isinstance(string_to_eval, six.text_type): string_to_eval = string_to_eval.decode(encoding) exec(string_to_eval, locals()) except Exception as e: debug.log("Exception in eval_string", e) def error_message(message): return ansi.markup("[Error] " + message) def setup_options(parser): parser.add_option("--tty", dest = "tty", help = "path to the TTY (usually, the value of $TTY)") parser.add_option("--rcfile", dest = "rcfile", help = "path to the settings file") parser.add_option("--output-encoding", dest = "output_encoding", help = "encoding for output") parser.add_option("--input-encoding", dest = "input_encoding", default = "utf8", help = "encoding for input and output (default 'utf8')") parser.add_option("-v", "--invert-match", action="store_true", dest = "invert_match", default = False, help = "select non-matching lines") parser.add_option("--query", dest = "query", help = "pre-input query") parser.add_option("--eager", action = "store_true", dest = "eager", default = False, help = "suppress lazy matching (slower, but display correct candidates count)") parser.add_option("--eval", dest = "string_to_eval", help = "eval given string after loading the rc file") parser.add_option("--prompt", dest = "prompt", default = None, help = "specify prompt (percol.view.PROMPT)") parser.add_option("--right-prompt", dest = "right_prompt", default = None, help = "specify right prompt (percol.view.RPROMPT)") parser.add_option("--match-method", dest = "match_method", default = "", help = "specify matching method for query. `string` (default) and `regex` are currently supported") parser.add_option("--caret-position", dest = "caret", help = "position of the caret (default length of the `query`)") parser.add_option("--initial-index", dest = "index", help = "position of the initial index of the selection (numeric, \"first\" or \"last\")") parser.add_option("--case-sensitive", dest = "case_sensitive", default = False, action="store_true", help = "whether distinguish the case of query or not") parser.add_option("--reverse", dest = "reverse", default = False, action="store_true", help = "whether reverse the order of candidates or not") parser.add_option("--auto-fail", dest = "auto_fail", default = False, action="store_true", help = "auto fail if no candidates") parser.add_option("--auto-match", dest = "auto_match", default = False, action="store_true", help = "auto matching if only one candidate") parser.add_option("--prompt-top", dest = "prompt_on_top", default = None, action="store_true", help = "display prompt top of the screen (default)") parser.add_option("--prompt-bottom", dest = "prompt_on_top", default = None, action="store_false", help = "display prompt bottom of the screen") parser.add_option("--result-top-down", dest = "results_top_down", default = None, action="store_true", help = "display results top down (default)") parser.add_option("--result-bottom-up", dest = "results_top_down", default = None, action="store_false", help = "display results bottom up instead of top down") parser.add_option("--quote", dest = "quote", default = False, action="store_true", help = "whether quote the output line") parser.add_option("--peep", action = "store_true", dest = "peep", default = False, help = "exit immediately with doing nothing to cache module files and speed up start-up time") def set_proper_locale(options): locale.setlocale(locale.LC_ALL, '') output_encoding = locale.getpreferredencoding() if options.output_encoding: output_encoding = options.output_encoding return output_encoding def read_input(filename, encoding, reverse=False): import codecs if filename: if six.PY2: stream = codecs.getreader(encoding)(open(filename, "r"), "replace") else: stream = open(filename, "r", encoding=encoding) else: if six.PY2: stream = codecs.getreader(encoding)(sys.stdin, "replace") else: import io stream = io.TextIOWrapper(sys.stdin.buffer, encoding=encoding) if reverse: lines = reversed(stream.readlines()) else: lines = stream for line in lines: yield ansi.remove_escapes(line.rstrip("\r\n")) stream.close() def decide_match_method(options): if options.match_method == "regex": from percol.finder import FinderMultiQueryRegex return FinderMultiQueryRegex elif options.match_method == "migemo": from percol.finder import FinderMultiQueryMigemo return FinderMultiQueryMigemo elif options.match_method == "pinyin": from percol.finder import FinderMultiQueryPinyin return FinderMultiQueryPinyin else: from percol.finder import FinderMultiQueryString return FinderMultiQueryString def main(): from percol import __version__ parser = OptionParser(usage = "Usage: %prog [options] [FILE]", version = "%prog {0}".format(__version__)) setup_options(parser) options, args = parser.parse_args() if options.peep: exit(1) def exit_program(msg = None, show_help = True): if not msg is None: print(msg) if show_help: parser.print_help() exit(1) # get ttyname ttyname = options.tty or tty.get_ttyname() if not ttyname: exit_program(error_message("""No tty name is given and failed to guess it from descriptors. Maybe all descriptors are redirecred.""")) # decide which encoding to use output_encoding = set_proper_locale(options) input_encoding = options.input_encoding def open_tty(ttyname): if six.PY2: return open(ttyname, "r+w") else: # See https://github.com/stefanholek/term/issues/1 return open(ttyname, "wb+", buffering=0) with open_tty(ttyname) as tty_f: if not tty_f.isatty(): exit_program(error_message("{0} is not a tty file".format(ttyname)), show_help = False) filename = args[0] if len(args) > 0 else None if filename and not os.access(filename, os.R_OK): exit_program(error_message("Cannot read a file '" + filename + "'"), show_help=False) if filename is None and sys.stdin.isatty(): tty_f.write(INSTRUCTION_TEXT.encode(output_encoding)) exit_program(show_help = False) # read input try: candidates = read_input(filename, input_encoding, reverse=options.reverse) except KeyboardInterrupt: exit_program("Canceled", show_help = False) # setup actions import percol.actions as actions if (options.quote): acts = (actions.output_to_stdout_double_quote, ) else: acts = (actions.output_to_stdout, actions.output_to_stdout_double_quote) # arrange finder class candidate_finder_class = action_finder_class = decide_match_method(options) def set_finder_attribute_from_option(finder_instance): finder_instance.lazy_finding = not options.eager finder_instance.case_insensitive = not options.case_sensitive finder_instance.invert_match = options.invert_match def set_if_not_none(src, dest, name): value = getattr(src, name) if value is not None: setattr(dest, name, value) with Percol(descriptors = tty.reconnect_descriptors(tty_f), candidates = candidates, actions = acts, finder = candidate_finder_class, action_finder = action_finder_class, query = options.query, caret = options.caret, index = options.index, encoding = output_encoding) as percol: # load run-command file load_rc(percol, options.rcfile) # override prompts if options.prompt is not None: percol.view.__class__.PROMPT = property(lambda self: options.prompt) if options.right_prompt is not None: percol.view.__class__.RPROMPT = property(lambda self: options.right_prompt) # evalutate strings specified by the option argument if options.string_to_eval is not None: eval_string(percol, options.string_to_eval, locale.getpreferredencoding()) # finder settings from option values set_finder_attribute_from_option(percol.model_candidate.finder) # view settings from option values set_if_not_none(options, percol.view, 'prompt_on_top') set_if_not_none(options, percol.view, 'results_top_down') # enter main loop if options.auto_fail and percol.has_no_candidate(): exit_code = percol.cancel_with_exit_code() elif options.auto_match and percol.has_only_one_candidate(): exit_code = percol.finish_with_exit_code() else: exit_code = percol.loop() exit(exit_code) percol-0.2.1/percol/command.py000066400000000000000000000170321255636556200162740ustar00rootroot00000000000000# -*- coding: utf-8 -*- import six class SelectorCommand(object): """ Wraps up SelectorModel and provides advanced commands """ def __init__(self, model, view): self.model = model self.view = view # ------------------------------------------------------------ # # Selection # ------------------------------------------------------------ # # Line def select_successor(self): self.model.select_index(self.model.index + 1) def select_predecessor(self): self.model.select_index(self.model.index - 1) def select_next(self): if self.view.results_top_down: self.select_successor() else: self.select_predecessor() def select_previous(self): if self.view.results_top_down: self.select_predecessor() else: self.select_successor() # Top / Bottom def select_top(self): if self.view.results_top_down: self.model.select_top() else: self.model.select_bottom() def select_bottom(self): if self.view.results_top_down: self.model.select_bottom() else: self.model.select_top() # Page def select_successor_page(self): self.model.select_index(self.model.index + self.view.RESULTS_DISPLAY_MAX) def select_predecessor_page(self): self.model.select_index(self.model.index - self.view.RESULTS_DISPLAY_MAX) def select_next_page(self): if self.view.results_top_down: self.select_successor_page() else: self.select_predecessor_page() def select_previous_page(self): if self.view.results_top_down: self.select_predecessor_page() else: self.select_successor_page() # ------------------------------------------------------------ # # Mark # ------------------------------------------------------------ # def toggle_mark(self): self.model.set_is_marked(not self.model.get_is_marked()) def toggle_mark_and_next(self): self.toggle_mark() self.select_successor() def __get_all_mark_indices(self): return six.moves.range(self.model.results_count) def mark_all(self): for mark_index in self.__get_all_mark_indices(): self.model.set_is_marked(True, mark_index) def unmark_all(self): for mark_index in self.__get_all_mark_indices(): self.model.set_is_marked(False, mark_index) def toggle_mark_all(self): for mark_index in self.__get_all_mark_indices(): self.model.set_is_marked(not self.model.get_is_marked(mark_index), mark_index) # ------------------------------------------------------------ # # Caret # ------------------------------------------------------------ # def beginning_of_line(self): self.model.set_caret(0) def end_of_line(self): self.model.set_caret(len(self.model.query)) def backward_char(self): self.model.set_caret(self.model.caret - 1) def forward_char(self): self.model.set_caret(self.model.caret + 1) def _get_backward_word_begin(self): from re import match skippable_substring = match(r'\s*\S*', self.model.query[:self.model.caret][::-1]) return self.model.caret - skippable_substring.end() def _get_forward_word_end(self): from re import match skippable_substring = match(r'\s*\S*', self.model.query[self.model.caret:]) return self.model.caret + skippable_substring.end() def backward_word(self): self.model.set_caret(self._get_backward_word_begin()) def forward_word(self): self.model.set_caret(self._get_forward_word_end()) # ------------------------------------------------------------ # # Text # ------------------------------------------------------------ # def delete_backward_char(self): if self.model.caret > 0: self.backward_char() self.delete_forward_char() def delete_forward_char(self): caret = self.model.caret self.model.query = self.model.query[:caret] + self.model.query[caret + 1:] def delete_backward_word(self): backword_word_begin = self._get_backward_word_begin() backword_word_end = self.model.caret self.model.query = self.model.query[:backword_word_begin] + self.model.query[backword_word_end:] self.model.set_caret(backword_word_begin) def delete_forward_word(self): forward_word_begin = self.model.caret forward_word_end = self._get_forward_word_end() self.model.query = self.model.query[:forward_word_begin] + self.model.query[forward_word_end:] self.model.set_caret(forward_word_begin) def delete_end_of_line(self): self.model.query = self.model.query[:self.model.caret] def clear_query(self): self.model.query = u"" self.model.set_caret(0) def transpose_chars(self): caret = self.model.caret qlen = len(self.model.query) if qlen <= 1: self.end_of_line() elif caret == 0: self.forward_char() self.transpose_chars() elif caret == qlen: self.backward_char() self.transpose_chars() else: self.model.query = self.model.query[:caret - 1] + \ self.model.query[caret] + \ self.model.query[caret - 1] + \ self.model.query[caret + 1:] self.forward_char() def unnarrow(self): """ Clears the query, but keeps the current line selected. Useful to show context around a search match. """ try: original_index = self.model.results[self.model.index][2] except IndexError: original_index = 0 self.clear_query() self.model.do_search("") self.model.select_index(original_index) # ------------------------------------------------------------ # # Text > kill # ------------------------------------------------------------ # def kill_end_of_line(self): self.model.killed = self.model.query[self.model.caret:] self.model.query = self.model.query[:self.model.caret] killed = None # default def yank(self): if self.model.killed: self.model.insert_string(self.model.killed) # ------------------------------------------------------------ # # Finder # ------------------------------------------------------------ # def specify_case_sensitive(self, case_sensitive): self.model.finder.case_insensitive = not case_sensitive self.model.force_search() def toggle_case_sensitive(self): self.model.finder.case_insensitive = not self.model.finder.case_insensitive self.model.force_search() def specify_split_query(self, split_query): self.model.finder.split_query = split_query self.model.force_search() def toggle_split_query(self): self.model.finder.split_query = not self.model.finder.split_query self.model.force_search() def specify_finder(self, preferred_finder_class): self.model.remake_finder(preferred_finder_class) self.model.force_search() def toggle_finder(self, preferred_finder_class): if self.model.finder.__class__ == preferred_finder_class: self.model.remake_finder(self.model.original_finder_class) else: self.model.remake_finder(preferred_finder_class) self.model.force_search() percol-0.2.1/percol/debug.py000066400000000000000000000004701255636556200157420ustar00rootroot00000000000000# -*- coding: utf-8 -*- import syslog syslog.openlog("Percol") def log(name, s = ""): syslog.syslog(syslog.LOG_ALERT, str(name) + ": " + str(s)) def dump(obj): import pprint pp = pprint.PrettyPrinter(indent=2) syslog.syslog(syslog.LOG_ALERT, str(name) + ": " + pp.pformat(obj)) return obj percol-0.2.1/percol/display.py000066400000000000000000000250521255636556200163240ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unicodedata import six import curses import re from percol import markup, debug FG_COLORS = { "black" : curses.COLOR_BLACK, "red" : curses.COLOR_RED, "green" : curses.COLOR_GREEN, "yellow" : curses.COLOR_YELLOW, "blue" : curses.COLOR_BLUE, "magenta" : curses.COLOR_MAGENTA, "cyan" : curses.COLOR_CYAN, "white" : curses.COLOR_WHITE, } BG_COLORS = dict(("on_" + name, value) for name, value in six.iteritems(FG_COLORS)) ATTRS = { "altcharset" : curses.A_ALTCHARSET, "blink" : curses.A_BLINK, "bold" : curses.A_BOLD, "dim" : curses.A_DIM, "normal" : curses.A_NORMAL, "standout" : curses.A_STANDOUT, "underline" : curses.A_UNDERLINE, "reverse" : curses.A_REVERSE, } COLOR_COUNT = len(FG_COLORS) # ============================================================ # # Markup # ============================================================ # def get_fg_color(attrs): for attr in attrs: if attr in FG_COLORS: return FG_COLORS[attr] return FG_COLORS["default"] def get_bg_color(attrs): for attr in attrs: if attr in BG_COLORS: return BG_COLORS[attr] return BG_COLORS["on_default"] def get_attributes(attrs): for attr in attrs: if attr in ATTRS: yield ATTRS[attr] # ============================================================ # # Unicode # ============================================================ # def screen_len(s, beg = None, end = None): if beg is None: beg = 0 if end is None: end = len(s) if "\t" in s: # consider tabstop (very naive approach) beg = len(s[0:beg].expandtabs()) end = len(s[beg:end].expandtabs()) s = s.expandtabs() if not isinstance(s, six.text_type): return end - beg dis_len = end - beg for i in six.moves.range(beg, end): if unicodedata.east_asian_width(s[i]) in ("W", "F"): dis_len += 1 return dis_len def screen_length_to_bytes_count(string, screen_length_limit, encoding): bytes_count = 0 screen_length = 0 for unicode_char in string: screen_length += screen_len(unicode_char) char_bytes_count = len(unicode_char.encode(encoding)) bytes_count += char_bytes_count if screen_length > screen_length_limit: bytes_count -= char_bytes_count break return bytes_count # ============================================================ # # Display # ============================================================ # class Display(object): def __init__(self, screen, encoding): self.screen = screen self.encoding = encoding self.markup_parser = markup.MarkupParser() curses.start_color() self.has_default_colors = curses.COLORS > COLOR_COUNT if self.has_default_colors: # xterm-256color curses.use_default_colors() FG_COLORS["default"] = -1 BG_COLORS["on_default"] = -1 self.init_color_pairs() elif curses.COLORS != 0: # ansi linux rxvt ...etc. self.init_color_pairs() FG_COLORS["default"] = curses.COLOR_WHITE BG_COLORS["on_default"] = curses.COLOR_BLACK else: # monochrome, curses.COLORS == 0 # vt100 x10term wy520 ...etc. FG_COLORS["default"] = curses.COLOR_WHITE BG_COLORS["on_default"] = curses.COLOR_BLACK self.update_screen_size() def update_screen_size(self): self.HEIGHT, self.WIDTH = self.screen.getmaxyx() @property def Y_BEGIN(self): return 0 @property def Y_END(self): return self.HEIGHT - 1 @property def X_BEGIN(self): return 0 @property def X_END(self): return self.WIDTH - 1 # ============================================================ # # Color Pairs # ============================================================ # def init_color_pairs(self): for fg_s, fg in six.iteritems(FG_COLORS): for bg_s, bg in six.iteritems(BG_COLORS): if not (fg == bg == 0): curses.init_pair(self.get_pair_number(fg, bg), fg, bg) def get_normalized_number(self, number): return COLOR_COUNT if number < 0 else number def get_pair_number(self, fg, bg): if self.has_default_colors: # Assume the number of colors is up to 16 (2^4 = 16) return self.get_normalized_number(fg) | (self.get_normalized_number(bg) << 4) else: return self.get_normalized_number(fg) + self.get_normalized_number(bg) * COLOR_COUNT def get_color_pair(self, fg, bg): return curses.color_pair(self.get_pair_number(fg, bg)) # ============================================================ # # Aligned string # ============================================================ # def get_pos_x(self, x_align, x_offset, whole_len): position = 0 if x_align == "left": position = x_offset elif x_align == "right": position = self.WIDTH - whole_len - x_offset elif x_align == "center": position = x_offset + (int(self.WIDTH - whole_len) / 2) return position def get_pos_y(self, y_align, y_offset): position = 0 if y_align == "top": position = y_offset elif y_align == "bottom": position = self.HEIGHT - y_offset elif y_align == "center": position = y_offset + int(self.HEIGHT / 2) return position def get_flag_from_attrs(self, attrs): flag = self.get_color_pair(get_fg_color(attrs), get_bg_color(attrs)) for attr in get_attributes(attrs): flag |= attr return flag def add_aligned_string_markup(self, markup, **keywords): return self.add_aligned_string_tokens(self.markup_parser.parse(markup), **keywords) def add_aligned_string_tokens(self, tokens, y_align = "top", x_align = "left", y_offset = 0, x_offset = 0, fill = False, fill_char = " ", fill_style = None): dis_lens = [screen_len(s) for (s, attrs) in tokens] whole_len = sum(dis_lens) pos_x = self.get_pos_x(x_align, x_offset, whole_len) pos_y = self.get_pos_y(y_align, y_offset) org_pos_x = pos_x for i, (s, attrs) in enumerate(tokens): self.add_string(s, pos_y, pos_x, self.attrs_to_style(attrs)) pos_x += dis_lens[i] if fill: self.add_filling(fill_char, pos_y, 0, org_pos_x, fill_style) self.add_filling(fill_char, pos_y, pos_x, self.WIDTH, fill_style) return pos_y, org_pos_x def add_aligned_string(self, s, y_align = "top", x_align = "left", y_offset = 0, x_offset = 0, style = None, fill = False, fill_char = " ", fill_style = None): dis_len = screen_len(s) pos_x = self.get_pos_x(x_align, x_offset, dis_len) pos_y = self.get_pos_y(y_align, y_offset) self.add_string(s, pos_y, pos_x, style) if fill: if fill_style is None: fill_style = style self.add_filling(fill_char, pos_y, 0, pos_x, fill_style) self.add_filling(fill_char, pos_y, pos_x + dis_len, self.WIDTH, fill_style) return pos_y, pos_x def add_filling(self, fill_char, pos_y, pos_x_beg, pos_x_end, style): filling_len = pos_x_end - pos_x_beg if filling_len > 0: self.add_string(fill_char * filling_len, pos_y, pos_x_beg, style) def attrs_to_style(self, attrs): if attrs is None: return 0 style = self.get_color_pair(get_fg_color(attrs), get_bg_color(attrs)) for attr in get_attributes(attrs): style |= attr return style def add_string(self, s, pos_y = 0, pos_x = 0, style = None, n = -1): self.addnstr(pos_y, pos_x, s, n if n >= 0 else self.WIDTH - pos_x, style) # ============================================================ # # Fundamental # ============================================================ # def erase(self): self.screen.erase() def clear(self): self.screen.clear() def refresh(self): self.screen.refresh() def get_raw_string(self, s): return s.encode(self.encoding) if isinstance(s, six.text_type) else s def addnstr(self, y, x, s, n, style): if not isinstance(style, six.integer_types): style = self.attrs_to_style(style) # Compute bytes count of the substring that fits in the screen bytes_count_to_display = screen_length_to_bytes_count(s, n, self.encoding) try: sanitized_str = re.sub(r'[\x00-\x08\x0a-\x1f]', '?', s) raw_str = self.get_raw_string(sanitized_str) self.screen.addnstr(y, x, raw_str, bytes_count_to_display, style) return True except curses.error: return False if __name__ == "__main__": import locale locale.setlocale(locale.LC_ALL, '') screen = curses.initscr() display = Display(screen, locale.getpreferredencoding()) display.add_string("-" * display.WIDTH, pos_y = 2) display.add_aligned_string_markup("foo bar baz qux", x_align = "center", y_offset = 3) display.add_aligned_string_markup(u"ああ,なんて赤くて太くて太い,そして赤いリンゴ", y_offset = 4, x_offset = -20, x_align = "center", fill = True, fill_char = "*") display.add_aligned_string(u"こんにちは", y_offset = 5, x_offset = 0, x_align = "right", fill = True, fill_char = '*', fill_style = display.attrs_to_style(("bold", "white", "on_green"))) display.add_aligned_string(u" foo bar baz qux ", x_align = "center", y_align = "center", style = display.attrs_to_style(("bold", "white", "on_default")), fill = True, fill_char = '-') screen.getch() percol-0.2.1/percol/finder.py000066400000000000000000000207431255636556200161300ustar00rootroot00000000000000# -*- coding: utf-8 -*- from abc import ABCMeta, abstractmethod from percol.lazyarray import LazyArray import six # ============================================================ # # Finder # ============================================================ # class Finder(object): __metaclass__ = ABCMeta def __init__(self, **args): pass def clone_as(self, new_finder_class): new_finder = new_finder_class(collection = self.collection) new_finder.invert_match = self.invert_match new_finder.lazy_finding = self.lazy_finding return new_finder @abstractmethod def get_name(self): pass @abstractmethod def find(self, query, collection = None): pass invert_match = False lazy_finding = True def get_results(self, query, collection = None): if self.lazy_finding: return LazyArray((result for result in self.find(query, collection))) else: return [result for result in self.find(query, collection)] # ============================================================ # # Cached Finder # ============================================================ # class CachedFinder(Finder): def __init__(self, **args): self.results_cache = {} def get_collection_from_trie(self, query): """ If any prefix of the query matches a past query, use its result as a collection to improve performance (prefix of the query constructs a trie) """ for i in six.moves.range(len(query) - 1, 0, -1): query_prefix = query[0:i] if query_prefix in self.results_cache: return (line for (line, res, idx) in self.results_cache[query_prefix]) return None def get_results(self, query): if query in self.results_cache: return self.results_cache[query] collection = self.get_collection_from_trie(query) or self.collection return Finder.get_results(self, query, collection) # ============================================================ # # Finder > multiquery # ============================================================ # class FinderMultiQuery(CachedFinder): def __init__(self, collection, split_str = " "): CachedFinder.__init__(self) self.collection = collection self.split_str = split_str def clone_as(self, new_finder_class): new_finder = Finder.clone_as(self, new_finder_class) new_finder.case_insensitive = self.case_insensitive new_finder.and_search = self.and_search return new_finder split_query = True case_insensitive = True dummy_res = [["", [(0, 0)]]] def find(self, query, collection = None): query_is_empty = query == "" # Arrange queries if self.case_insensitive: query = query.lower() # Split query when split_query is True if self.split_query: queries = [self.transform_query(sub_query) for sub_query in query.split(self.split_str)] else: queries = [self.transform_query(query)] if collection is None: collection = self.collection for idx, line in enumerate(collection): if query_is_empty: res = self.dummy_res else: if self.case_insensitive: line_to_match = line.lower() else: line_to_match = line res = self.find_queries(queries, line_to_match) # When invert_match is enabled (via "-v" option), # select non matching line if self.invert_match: res = None if res else self.dummy_res if res: yield line, res, idx and_search = True def find_queries(self, sub_queries, line): res = [] and_search = self.and_search for subq in sub_queries: if subq: find_info = self.find_query(subq, line) if find_info: res.append((subq, find_info)) elif and_search: return None return res @abstractmethod def find_query(self, needle, haystack): # return [(pos1, pos1_len), (pos2, pos2_len), ...] # # where `pos1', `pos2', ... are begining positions of all occurence of needle in `haystack' # and `pos1_len', `pos2_len', ... are its length. pass # override this method if needed def transform_query(self, query): return query # ============================================================ # # Finder > AND search # ============================================================ # class FinderMultiQueryString(FinderMultiQuery): def get_name(self): return "string" trie_style_matching = True def find_query(self, needle, haystack): stride = len(needle) start = 0 res = [] while True: found = haystack.find(needle, start) if found < 0: break res.append((found, stride)) start = found + stride return res # ============================================================ # # Finder > AND search > Regular Expression # ============================================================ # class FinderMultiQueryRegex(FinderMultiQuery): def get_name(self): return "regex" def transform_query(self, needle): try: import re return re.compile(needle) except: return None def find_query(self, needle, haystack): try: matched = needle.search(haystack) return [(matched.start(), matched.end() - matched.start())] except: return None # ============================================================ # # Finder > AND search > Migemo # ============================================================ # class FinderMultiQueryMigemo(FinderMultiQuery): def get_name(self): return "migemo" dictionary_path = None minimum_query_length = 2 dictionary_path_candidates = [ "/usr/share/cmigemo/utf-8/migemo-dict", "/usr/share/migemo/utf-8/migemo-dict", "/usr/local/share/cmigemo/utf-8/migemo-dict", "/usr/local/share/migemo/utf-8/migemo-dict" ] def guess_dictionary_path(self): import os for path in [self.dictionary_path] + self.dictionary_path_candidates: path = os.path.expanduser(path) if os.access(path, os.R_OK): return path return None migemo_instance = None @property def migemo(self): import cmigemo, os if self.migemo_instance is None: dictionary_path = self.guess_dictionary_path() if dictionary_path is None: raise Exception("Error: Cannot find migemo dictionary. Install it and set dictionary_path.") self.migemo_instance = cmigemo.Migemo(dictionary_path) return self.migemo_instance def transform_query(self, needle): if len(needle) >= self.minimum_query_length: regexp_string = self.migemo.query(needle) else: regexp_string = needle import re return re.compile(regexp_string) def find_query(self, needle, haystack): try: matched = needle.search(haystack) return [(matched.start(), matched.end() - matched.start())] except: return None # ============================================================ # # Finder > AND search > Pinyin support # ============================================================ # class FinderMultiQueryPinyin(FinderMultiQuery): """ In this matching method, first char of each Chinese character's pinyin sequence is used for matching. For example, 'zw' matches '中文' (ZhongWen), '中午'(ZhongWu), '作为' (ZuoWei) etc. Extra package 'pinyin' needed. """ def get_name (self): return "pinyin" def find_query (self, needle, haystack): try: import pinyin haystack_py = pinyin.get_initial(haystack, '' ) needle_len = len(needle) start = 0 result = [] while True : found = haystack_py.find(needle, start) if found < 0 : break result.append((found, needle_len)) start = found + needle_len return result except : return None percol-0.2.1/percol/info.py000066400000000000000000000011371255636556200156100ustar00rootroot00000000000000# -*- coding: utf-8 -*- __doc__ = """Adds flavor of interactive filtering to the traditional pipe concept of shell. Try:: $ A | percol | B and you can display the output of command A and filter it interactively and then pass it to command B. The interface of percol is highly inspired by anything.el for Emacs. Full details at https://github.com/mooz/percol""" __version__ = "0.2.1" __logo__ = """ __ ____ ___ ______________ / / / __ \/ _ \/ ___/ ___/ __ \/ / / /_/ / __/ / / /__/ /_/ / / / .___/\___/_/ \___/\____/_/ /_/""" percol-0.2.1/percol/key.py000066400000000000000000000202601255636556200154430ustar00rootroot00000000000000# -*- coding: utf-8 -*- import curses, array import six SPECIAL_KEYS = { curses.KEY_A1 : "", curses.KEY_A3 : "", curses.KEY_B2 : "", curses.KEY_BACKSPACE : "", curses.KEY_BEG : "", curses.KEY_BREAK : "", curses.KEY_BTAB : "", curses.KEY_C1 : "", curses.KEY_C3 : "", curses.KEY_CANCEL : "", curses.KEY_CATAB : "", curses.KEY_CLEAR : "", curses.KEY_CLOSE : "", curses.KEY_COMMAND : "", curses.KEY_COPY : "", curses.KEY_CREATE : "", curses.KEY_CTAB : "", curses.KEY_DC : "", curses.KEY_DL : "
", curses.KEY_DOWN : "", curses.KEY_EIC : "", curses.KEY_END : "", curses.KEY_ENTER : "", curses.KEY_EOL : "", curses.KEY_EOS : "", curses.KEY_EXIT : "", curses.KEY_F0 : "", curses.KEY_F1 : "", curses.KEY_F10 : "", curses.KEY_F11 : "", curses.KEY_F12 : "", curses.KEY_F13 : "", curses.KEY_F14 : "", curses.KEY_F15 : "", curses.KEY_F16 : "", curses.KEY_F17 : "", curses.KEY_F18 : "", curses.KEY_F19 : "", curses.KEY_F2 : "", curses.KEY_F20 : "", curses.KEY_F21 : "", curses.KEY_F22 : "", curses.KEY_F23 : "", curses.KEY_F24 : "", curses.KEY_F25 : "", curses.KEY_F26 : "", curses.KEY_F27 : "", curses.KEY_F28 : "", curses.KEY_F29 : "", curses.KEY_F3 : "", curses.KEY_F30 : "", curses.KEY_F31 : "", curses.KEY_F32 : "", curses.KEY_F33 : "", curses.KEY_F34 : "", curses.KEY_F35 : "", curses.KEY_F36 : "", curses.KEY_F37 : "", curses.KEY_F38 : "", curses.KEY_F39 : "", curses.KEY_F4 : "", curses.KEY_F40 : "", curses.KEY_F41 : "", curses.KEY_F42 : "", curses.KEY_F43 : "", curses.KEY_F44 : "", curses.KEY_F45 : "", curses.KEY_F46 : "", curses.KEY_F47 : "", curses.KEY_F48 : "", curses.KEY_F49 : "", curses.KEY_F5 : "", curses.KEY_F50 : "", curses.KEY_F51 : "", curses.KEY_F52 : "", curses.KEY_F53 : "", curses.KEY_F54 : "", curses.KEY_F55 : "", curses.KEY_F56 : "", curses.KEY_F57 : "", curses.KEY_F58 : "", curses.KEY_F59 : "", curses.KEY_F6 : "", curses.KEY_F60 : "", curses.KEY_F61 : "", curses.KEY_F62 : "", curses.KEY_F63 : "", curses.KEY_F7 : "", curses.KEY_F8 : "", curses.KEY_F9 : "", curses.KEY_FIND : "", curses.KEY_HELP : "", curses.KEY_HOME : "", curses.KEY_IC : "", curses.KEY_IL : "", curses.KEY_LEFT : "", curses.KEY_LL : "", curses.KEY_MARK : "", curses.KEY_MAX : "", curses.KEY_MESSAGE : "", curses.KEY_MIN : "", curses.KEY_MOUSE : "", curses.KEY_MOVE : "", curses.KEY_NEXT : "", curses.KEY_NPAGE : "", curses.KEY_OPEN : "", curses.KEY_OPTIONS : "", curses.KEY_PPAGE : "", curses.KEY_PREVIOUS : "", curses.KEY_PRINT : "", curses.KEY_REDO : "", curses.KEY_REFERENCE : "", curses.KEY_REFRESH : "", curses.KEY_REPLACE : "", curses.KEY_RESET : "", curses.KEY_RESIZE : "", curses.KEY_RESTART : "", curses.KEY_RESUME : "", curses.KEY_RIGHT : "", curses.KEY_SAVE : "", curses.KEY_SBEG : "", curses.KEY_SCANCEL : "", curses.KEY_SCOMMAND : "", curses.KEY_SCOPY : "", curses.KEY_SCREATE : "", curses.KEY_SDC : "", curses.KEY_SDL : "", curses.KEY_SELECT : "