PK!Y README.md # keycut ![logo](logo.jpg) [![pipeline status](https://github.com/pawamoy/keycut/badges/master/pipeline.svg)](https://github.com/pawamoy/keycut/commits/master) [![coverage report](https://github.com/pawamoy/keycut/badges/master/coverage.svg)](https://github.com/pawamoy/keycut/commits/master) [![documentation](https://img.shields.io/readthedocs/keycut.svg?style=flat)](https://keycut.readthedocs.io/en/latest/index.html) [![pypi version](https://img.shields.io/pypi/v/keycut.svg)](https://pypi.org/project/keycut/) A command line tool that helps you remembering ALL the numerous keyboard shortcuts of ALL your favorite programs. KeyCut (for keyboard shortcut) is a command line tool that helps you remembering the numerous keyboard shortcuts of your favorite programs, both graphical and command line ones, by allowing you to print them quickly in a console and search through them. Shortcut data are provided by the [keycut-data][1]. This repository contains the sources for a Python implementation of KeyCut. [keycut-data]: https://github.com/pawamoy/keycut-data ## How it looks The yellow parts are the one that matched a pattern using a regular expression. ![screenshot](http://i.imgur.com/ZaqTOUb.png) ## Requirements keycut requires Python 3.6 or above.
To install Python 3.6, I recommend using pyenv. ```bash # install pyenv git clone https://github.com/pyenv/pyenv ~/.pyenv # setup pyenv (you should also put these three lines in .bashrc or similar) export PATH="${HOME}/.pyenv/bin:${PATH}" export PYENV_ROOT="${HOME}/.pyenv" eval "$(pyenv init -)" # install Python 3.6 pyenv install 3.6.8 # make it available globally pyenv global system 3.6.8 ```
## Installation With `pip`: ```bash python3.6 -m pip install keycut ``` With [`pipx`](https://github.com/cs01/pipx): ```bash # install pipx with the recommended method curl https://raw.githubusercontent.com/cs01/pipx/master/get-pipx.py | python3 pipx install --python python3.6 keycut ``` You will also need to download the data by cloning the repository somewhere: ``` git clone https://github.com/pawamoy/keycut-data ~/.keycut-data ``` ## Usage The program needs to know where the data are. By default, it will search in the (relative) `keycut-data/default` directory. ``` export KEYCUT_DATA=~/.keycut-data/default ``` Show all bash shortcuts: ``` keycut bash ``` Show all bash shortcuts matching *proc* (in Category, Action, or Keys): ``` keycut bash proc ``` Command-line help: ``` usage: keycut [-h] APP [PATTERN] Command description. positional arguments: APP The app to print shortcuts of. PATTERN A regex pattern to search for. optional arguments: -h, --help show this help message and exit ``` PK!?ffkeycut/__init__.py""" keycut package. A command line tool that helps you remembering ALL the numerous keyboard shortcuts of ALL your favorite programs. If you read this message, you probably want to learn about the library and not the command-line tool: please refer to the README.md included in this package to get the link to the official documentation. """ __all__ = [] PK!kk5keycut/__main__.py""" Entry-point module, in case you use `python -m keycut`. Why does this file exist, and why __main__? For more info, read: - https://www.python.org/dev/peps/pep-0338/ - https://docs.python.org/2/using/cmdline.html#cmdoption-m - https://docs.python.org/3/using/cmdline.html#cmdoption-m """ import sys from keycut.cli import main if __name__ == "__main__": sys.exit(main(sys.argv[1:])) PK!kJ keycut/cli.py""" Module that contains the command line application. Why does this file exist, and why not put this in __main__? You might be tempted to import things from __main__ later, but that will cause problems: the code will get executed twice: - When you run `python -m keycut` python will execute ``__main__.py`` as a script. That means there won't be any ``keycut.__main__`` in ``sys.modules``. - When you import __main__ it will get executed again (as a module) because there's no ``keycut.__main__`` in ``sys.modules``. Also see http://click.pocoo.org/5/setuptools/#setuptools-integration. """ import argparse from . import load, ui from .search import search from .utils import print_err def main(args=None): """The main function, which is executed when you type ``keycut`` or ``python -m keycut``.""" parser = get_parser() args = parser.parse_args(args=args) document = load.from_yaml(args.app) if document: if args.pattern: document = search(document, args.pattern) ui.reload(document, clear=False) else: print_err("Document not found: %s" % args.app) return 0 def get_parser(): parser = argparse.ArgumentParser(prog="keycut", description="Command description.") parser.add_argument("app", metavar="APP", help="The app to print shortcuts of.") parser.add_argument("pattern", metavar="PATTERN", nargs="?", help="A regex pattern to search for.") return parser PK!YK#keycut/load.py# -*- coding: utf-8 -*- import os import yaml DIRECTORY = os.environ.get("KEYCUT_DATA", "keycut-data/default") def grep(cmdline): cmdline = cmdline.lower() for file in os.listdir(DIRECTORY): app = os.path.splitext(file.lower())[0] if app in cmdline: return os.path.join(DIRECTORY, file) return None def isfile(file): return os.path.isfile(file) def check(name, path=DIRECTORY): file = os.path.join(path, name) + ".yml" return file, isfile(file) def from_yaml(app, command_line=None): file, exist = check(app) if not exist and command_line is not None: file = grep(command_line) if not file: return None with open(file) as f: doc = yaml.safe_load(f) if isinstance(doc, dict): document = [dict(category=key, **v) for key, value in doc.items() for v in value] else: document = [dict(category="", **value) for value in doc] return document PK!/H H keycut/render.py# -*- coding: utf-8 -*- import termcolor import yaml MATCH_COLOR = "yellow" CATEGORY_COLOR = "blue" ACTION_COLOR = None KEY_COLOR = "white" def as_text(document): str_list = [] for item in document: category = item.get("category", None) if category: str_list.append("Category: %s\nAction: %s\nKeys: %s\n" % (category, item["action"].rstrip(), item["keys"])) else: str_list.append("Action: %s\nKeys: %s\n" % (item["action"].rstrip(), item["keys"])) return "\n".join(str_list) if str_list else "" def _color_match(line, positions, default): length = len(positions) # Concat until first pos s = [_color(line[: positions[0][0]], default)] # For each (start, end), concat colored from start to end for index, pos in enumerate(positions): s.append(_color(line[pos[0] : pos[1]], MATCH_COLOR)) # If not last (start, end), concat until next start if index < length - 1: s.append(_color(line[pos[1] : positions[index + 1][0]], default)) # Else concat until end of string else: s.append(_color(line[pos[1] :], default)) return s def _color(text, color): if color is None: return text else: return termcolor.colored(text, color) def as_colored_text(document): str_list = [] for item in document: s = [] category = item.get("category", None) if category: s.append("Category: ") category_pos = item.get("category_pos", None) if category_pos: s.extend(_color_match(category, category_pos, CATEGORY_COLOR)) s.append("\n") else: s.append("%s\n" % _color(category, CATEGORY_COLOR)) action = item["action"].rstrip("\n") action_pos = item.get("action_pos", None) s.append(" Action: ") if action_pos: s.extend(_color_match(action, action_pos, ACTION_COLOR)) s.append("\n") else: s.append("%s\n" % _color(action, ACTION_COLOR)) s.append(" Keys: ") s_key = [] keys = item["keys"] for key in keys: key_pos = item.get("keys_pos", {}).get(key) if key_pos: s_key.append("".join(_color_match(key, key_pos, KEY_COLOR))) else: s_key.append("%s" % _color(key, KEY_COLOR)) s.append(", ".join(s_key)) s.append("\n") str_list.append("".join(s)) return "\n".join(str_list) if str_list else "" def as_yaml(document): return yaml.dump(document) PK!)keycut/search.py# -*- coding: utf-8 -*- import re def _search(document, pattern, key=None, word=False): exp = r"(\b%s\b)" if word else r"(%s)" prog = re.compile(exp % pattern, re.IGNORECASE) if key is not None: return [item for item in document if prog.search(item[key])] else: items = [] for item in document: added = False mo = prog.search(item["action"]) if mo: item["action_pos"] = [] for index, group in enumerate(mo.groups()): item["action_pos"].append(mo.span(index)) if not added: items.append(item) added = True mo = prog.search(item["category"]) if mo: item["category_pos"] = [] for index, group in enumerate(mo.groups()): item["category_pos"].append(mo.span(index)) if not added: items.append(item) added = True item["keys_pos"] = {} for key in item["keys"]: mo = prog.search(str(key.encode("utf-8"))) if mo: item["keys_pos"][key] = [] for index, group in enumerate(mo.groups()): item["keys_pos"][key].append(mo.span(index)) if not added: items.append(item) added = True return items if items else document def search(document, pattern): return _search(document, pattern) def in_category(document, pattern): return _search(document, pattern, key="category") def in_action(document, pattern): return _search(document, pattern, key="action") def in_keys(document, pattern): return _search(document, pattern, key="keys") def word_search(document, pattern): return _search(document, pattern, word=True) def word_in_category(document, pattern): return _search(document, pattern, key="category", word=True) def word_in_action(document, pattern): return _search(document, pattern, key="action", word=True) def word_in_keys(document, pattern): return _search(document, pattern, key="keys", word=True) PK! keycut/ui.py# -*- coding: utf-8 -*- # import os from . import render from .search import in_action, in_category, in_keys, search, word_in_action, word_in_category, word_in_keys, word_search UI_COMMANDS = { "s": search, "a": in_action, "c": in_category, "k": in_keys, "ws": word_search, "wa": word_in_action, "wc": word_in_category, "wk": word_in_keys, } UI_DOCUMENT = None def reload(document, clear=True): global UI_DOCUMENT UI_DOCUMENT = document text = render.as_colored_text(document) # B605:start_process_with_a_shell # B607:start_process_with_partial_path # if clear: # os.system('clear') print(text) PK!/\\keycut/utils.py# -*- coding: utf-8 -*- import sys def print_err(message): sys.stderr.write(message) PK!keycut/watch.py# -*- coding: utf-8 -*- import json import time from threading import Thread import load import ui from search import search class FirefoxWatcher(Thread): # FIXME: do this dynamically f = open("/home/pawantu/.mozilla/firefox/7vjr1dfd.default/" "sessionstore-backups/recovery.js", "r") jdata = json.loads(f.read()) f.close() tab_number = jdata["windows"][0]["selected"] for win in jdata.get("windows"): for tab in win.get("tabs"): i = tab.get("index") - 1 if i == tab_number: current_url = tab.get("entries")[i].get("url") class XdotoolWatcher(Thread): def __init__(self): Thread.__init__(self) self.name = "" self.sleep = 0.2 @staticmethod def _run_command(command): pass # return Popen( # command, shell=True, stdout=PIPE # ).stdout.read().decode().rstrip('\n') def run(self): name_command = "xdotool getwindowfocus getwindowname" while True: name = self._run_command(name_command) if name != self.name: document = load.from_yaml(name, command_line=name) if document: self.name = name ui.reload(document) else: print("Not found:") print(name) time.sleep(self.sleep) class WindowFocusWatcher(Thread): def __init__(self): Thread.__init__(self) self.name = "" self.cmdline = "" self.sleep = 0.2 @staticmethod def _run_command(command): pass # return Popen( # command, shell=True, stdout=PIPE # ).stdout.read().decode().rstrip('\n') def run(self): wid_command = "xprop -root | grep _NET_ACTIVE_WINDOW\(WINDOW\) | " 'grep -o "0x.*"' pid_command = 'xprop -id %s | grep _NET_WM_PID | grep -o "[0-9]*"' name_command = "cat /proc/%s/comm" cmdline_command = "cat /proc/%s/cmdline" while True: wid = self._run_command(wid_command) pid = self._run_command(pid_command % wid) name = self._run_command(name_command % pid) cmdline = self._run_command(cmdline_command % pid) if name != self.name and cmdline != self.cmdline: document = load.from_yaml(name, cmdline) if document: self.name = name self.cmdline = cmdline ui.reload(document) else: print("Not found:") print(wid, pid) print(name, cmdline) time.sleep(self.sleep) class FileWatcher(Thread): def __init__(self, file): Thread.__init__(self) self.file = file self.current = "" self.write("") self.sleep = 0.2 def run(self): while True: line = self.read() if line: words = line.split(" ") command = words[0] if len(words) > 1: pattern = words[1] current = "%s %s" % (command, pattern) else: current = command pattern = False if current != self.current: document = load.from_yaml(command) if document: if pattern: document = search(document, pattern) self.current = current ui.reload(document) time.sleep(self.sleep) def read(self): with open(self.file) as f: return f.readline().rstrip() def write(self, command): with open(self.file, "w") as f: f.write("%s\n" % command) PK!Apyproject.toml[build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.poetry] name = "keycut" version = "0.3.0" description = "A command line tool that helps you remembering ALL the numerous keyboard shortcuts of ALL your favorite programs." authors = ["Timothée Mazzucotelli "] license = "ISC License" readme = "README.md" repository = "https://github.com/pawamoy/keycut" homepage = "https://github.com/pawamoy/keycut" keywords = [] packages = [ { include = "keycut", from = "src" } ] include = [ "README.md", "pyproject.toml" ] [tool.poetry.dependencies] python = "^3.6" PyYAML = "^3.13" termcolor = "^1.1" [tool.poetry.dev-dependencies] bandit = "^1.5" black = { version = "*", allows-prereleases = true } flake8 = "^3.6" ipython = "^7.2" isort = { version = "^4.3", extras = ["pyproject"] } jinja2-cli = { git = "https://github.com/mattrobenolt/jinja2-cli.git" } pytest = "^4.3" pytest-cov = "^2.6" pytest-sugar = "^0.9.2" pytest-xdist = "^1.26" recommonmark = "^0.4.0" safety = "^1.8" sphinx = "^1.8" sphinxcontrib-spelling = "^4.2" sphinx-rtd-theme = "^0.4.2" toml = "^0.10.0" [tool.poetry.scripts] keycut = "keycut.cli:main" [tool.black] line-length = 120 [tool.isort] line_length = 120 not_skip = "__init__.py" multi_line_output = 3 force_single_line = false balanced_wrapping = true default_section = "THIRDPARTY" known_first_party = "keycut" include_trailing_comma = true PK!Hߞ'*'keycut-0.3.0.dist-info/entry_points.txtN+I/N.,()NL.-Pz9Vy\\PK!0keycut-0.3.0.dist-info/LICENSEISC License Copyright (c) 2015, Timothée Mazzucotelli Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. PK!HڽTUkeycut-0.3.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HA(0keycut-0.3.0.dist-info/METADATAWnF}߯j"7EZ[6 Q5ew4~G3KR7d;33gf'd" ^J#:`NOO'⧺(iF0Xc@Ju.2+ `*S1BYhtm\Kʹqɂ^x1R5KmCN,l(tA%S–9WQe<$$Q%WM>V1D/O_:ڑLU]ן'͛:\uR#PaA7$D}>|ZPH[{,˴&0^~|5{דcf;O=?e"ֹ6, _c\^+mӦ~BFUaiS!u&ѫR O.HL3earbF:L`at9䰨rzĶ/[/6_prprɗ!;QZJX㞺*C0\2A'=rmҁGJUioth>LRWHi@hSԹٔ "K Pa|"1utIiBPELaPP)$l;[{wT [2u* 3W@T W,-7\oP4ZR'^`_;^P+>@= fs@UbZhbހsbgi2JǞ ) ^ZMm,Jg$D S?uH,UBO~8Y[f;WYO'v$5\ܔPJE״墴y'c¦40# 2(91M Ua@*r%Œֹ4hf6{4Q2:<$Y6=USu3㴢EK۹g6 pf4 8vMT !<'SZ2ݭr`lƱNpGyɽp.K}{qA&!9vDaA&,j 6SRXWkTEzf[kLٱPT?|2=z](r-Z ><}$zR%E FwP{@oX7ȜK*SkHi?64Nr+Ѻ̔a eiC]u<&KPtkm3ּ~p ɚ]!Dĵu7rk&no{.Eo<](Bx h]&֬yΔHh&t`~bѐbzwc(Y2481͉aQ.-mp?mYwS> \:wCfǂϲ KרSH́m8_޿r0NvU[4W69ptCdE_6ߧkB/<n[ŭv~*kfAvsQO/N֚ O8MH/jV=_^['^xko˂J!wA6$ns<3aySeT߻5JK𨣱N@ȏy7eW[yfv^"}ZvF\FJUA3f*4TOVdO~;zw'HlC6tC )V0Ogq?O)Pon,G!PK!Y README.mdPK!?ff keycut/__init__.pyPK!kk5N keycut/__main__.pyPK!kJ keycut/cli.pyPK!YK#keycut/load.pyPK!/H H keycut/render.pyPK!)W#keycut/search.pyPK! P,keycut/ui.pyPK!/\\/keycut/utils.pyPK!/keycut/watch.pyPK!A>pyproject.tomlPK!Hߞ'*'Dkeycut-0.3.0.dist-info/entry_points.txtPK!0Ekeycut-0.3.0.dist-info/LICENSEPK!HڽTUBHkeycut-0.3.0.dist-info/WHEELPK!HA(0Hkeycut-0.3.0.dist-info/METADATAPK!HX U=Okeycut-0.3.0.dist-info/RECORDPK]R