PK ! Y README.md
# keycut

[](https://github.com/pawamoy/keycut/commits/master)
[](https://github.com/pawamoy/keycut/commits/master)
[](https://keycut.readthedocs.io/en/latest/index.html)
[](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.

## 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 ! ?f f keycut/__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 ! kk5 keycut/__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 ! A pyproject.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 ! 0 keycut-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ڽT U keycut-0.3.0.dist-info/WHEEL
A
н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK !HA(0 keycut-0.3.0.dist-info/METADATAWnF}߯j"7EZ[6 Q5ew4~G3KR7d;33gf'd"^J#:`NOO'⧺(iF0Xc@Ju.2+`*S1BYhtm\Kʹqɂ^x1R5KmCN,l(tA%S9WQe<$$Q%WM>V1D/O_:ڑLU]ן'͛:\uR#PaA7$D}>|ZPH[{,˴&0^~|5{דcf;O=?e"ֹ6,_c\^+mӦ~BFUaiS!u&ѫR O.H L3earbF:L`at9䰨rzĶ/[/6_prprɗ!;QZJX㞺*C0\2A'=rmҁGJUioth>LRWHi@hSԹٔ"KPa|"1utIiBPELaPP)$l;[{wT
[2u*
3W@T
W,-7\oP4ZR'^`_;^P+> @ =fs@UbZhbހsbgi2JǞ)
^ZMm,Jg$DS?uH,UBO~8Y[f;WYO'v$5\ܔPJE ״墴y'c¦40# 2(91MUa@*r%Œֹ4hf6{4Q2:<$Y6=US u3㴢EK۹g6pf48vMT
!<'SZ2ݭr`lƱNpGyɽp.K}{qA&!9vDaA&,j
6SRXWkTEzf[k LٱPT?|2=z](r-Z><}$zR%EFwP{@oX7ȜK*SkHi?64Nr+Ѻ̔a
eiC]u<&KP