diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a48538 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_store \ No newline at end of file diff --git a/google-chrome.py b/google-chrome.py new file mode 100644 index 0000000..270205a --- /dev/null +++ b/google-chrome.py @@ -0,0 +1,131 @@ +from talon.voice import Context, Key, press, Str +from user.utils import parse_words_as_integer + +# It is recommended to use this script in tandem with Vimium, +# a Google Chrome plugin for controlling the browser via keyboard +# https://vimium.github.io/ + +context = Context('GoogleChrome', bundle='com.google.Chrome') + + +def open_focus_devtools(m): + press('cmd-shift-c') + + +def show_panel(name): + open_focus_devtools(None) + + # Open command menu + press('cmd-shift-p') + + Str('Show %s' % (name))(None) + press('enter') + + +def next_panel(m): + open_focus_devtools(None) + press('cmd-]') + + +def last_panel(m): + open_focus_devtools(None) + press('cmd-[') + + +def focus_address_bar(m): + press('cmd-l') + + +def search(m): + press('cmd-m') + + +def quit(m): + press('cmd-q') + + +def scroll_to_top(m): + press('g') + press('g') + + +def print_page(m): + press('cmd-p') + + +# Return focus from the dev-tools to the page +def refocus_page(m): + focus_address_bar(None) + + # Escape button + # This leaves the focus on the page at previous tab focused point, not the beginning of the page + press('escape') + + +def back(m): + refocus_page(None) + press('cmd-[') + + +def forward(m): + refocus_page(None) + press('cmd-]') + + +def jump_tab(m): + tab_number = parse_words_as_integer(m._words[1:]) + if tab_number is not None and 0 < tab_number < 9: + press('cmd-%s' % tab_number) + + +context.keymap({ + 'address bar': focus_address_bar, + 'search': search, + 'print': print_page, + 'top': scroll_to_top, + 'quit': quit, + + 'back[ward]': back, + 'forward': forward, + 'reload': Key('cmd-r'), + 'hard reload': Key('cmd-shift-r'), + + 'new tab': Key('cmd-t'), + 'close tab': Key('cmd-w'), + '(reopen | unclose) tab': Key('cmd-shift-t'), + + 'next tab': Key('cmd-alt-right'), + '(last | prevous) tab': Key('cmd-alt-left'), + + 'tab (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8)': jump_tab, + '(end | rightmost) tab': Key('cmd-9'), + + 'find': Key('cmd-f'), + 'next': Key('cmd-g'), + + '(last | prevous)': Key('cmd-shift-g'), + + # dev tools + 'toggle dev tools': Key('cmd-alt-i'), + 'command [menu]': Key('cmd-shift-p'), + 'next panel': next_panel, + '(last | prevous) panel': last_panel, + '[show] application [panel]': lambda m: show_panel('Application'), + '[show] audit[s] [panel]': lambda m: show_panel('Audits'), + '[show] console [panel]': lambda m: show_panel('Console'), + '[show] element[s] [panel]': lambda m: show_panel('Elements'), + '[show] memory [panel]': lambda m: show_panel('Memory'), + '[show] network [panel]': lambda m: show_panel('Network'), + '[show] performance [panel]': lambda m: show_panel('Performance'), + '[show] security [panel]': lambda m: show_panel('Security'), + '[show] source[s] [panel]': lambda m: show_panel('Sources'), + + 'refocus page': refocus_page, + '[refocus] dev tools': open_focus_devtools, + + # Clipboard + 'cut': Key('cmd-x'), + 'copy': Key('cmd-c'), + 'paste': Key('cmd-v'), + 'paste same style': Key('cmd-alt-shift-v'), +}) diff --git a/jetbrains.py b/jetbrains.py new file mode 100644 index 0000000..0b4313c --- /dev/null +++ b/jetbrains.py @@ -0,0 +1,69 @@ +from talon.voice import Context, Key + +ides = { + "com.jetbrains.intellij", + "com.jetbrains.intellij.ce", + "com.jetbrains.AppCode", + "com.jetbrains.CLion", + "com.jetbrains.datagrip", + "com.jetbrains.goland", + "com.jetbrains.PhpStorm", + "com.jetbrains.pycharm", + "com.jetbrains.rider", + "com.jetbrains.rubymine", + "com.jetbrains.WebStorm", + "com.google.android.studio", +} + + +ctx = Context('phpstorm', func=lambda app, win: any( + i in app.bundle for i in ides)) + +keymap = { + 'comment declaration': ['/**', Key('space')], + 'comment block': ['/**', Key('enter')], + + '(dupe | duplicate)': Key('cmd-d'), + 'import class': Key('alt-enter enter'), + 'quickfix': Key('alt-enter'), + 'go class': Key('cmd-o'), + 'go file': Key('cmd-shift-o'), + '(go implement | go definition)': Key('cmd-b'), + 'preev method': Key('ctrl-up'), + 'neck method': Key('ctrl-down'), + 'refactor': Key('shift-f6'), + 'generate': Key('cmd-n'), + 'recent': Key('cmd-e'), + + 'replace': Key('cmd-r'), + 'find action': Key('cmd-shift-a'), + 'settings': Key('cmd-,'), + 'grab': Key('alt-up'), + 'shrink': Key('alt-down'), + 'close': Key('cmd-w'), + # 'rename': Key('shift-f6'), + 'move file': Key('f6'), + 'global search': Key('shift shift'), + 'go to file': Key('cmd-shift-n'), + 'format': Key('cmd-alt-l'), + 'expand': Key('cmd-+'), + 'collapse': Key('cmd--'), + 'erase line': Key('cmd-backspace'), + 'last change': Key('cmd-shift-backspace'), + '((open | close) terminal | terminal (open | close) | toggle terminal | terminal)': Key('alt-f12'), + 'snip left': Key('cmd-shift-left delete'), + 'snip right': Key('cmd-shift-right delete'), + 'move up': Key('alt-shift-up'), + 'move down': Key('alt-shift-down'), + 'path': Key('cmd-shift-f'), + 'funk up': Key('cmd-shift-up'), + 'funk down': Key('cmd-shift-down'), + '(breadcrumbs | crumbs)': Key('cmd-up'), + +} + + +ctx.keymap(keymap) + + +def unload(): ctx.unload() \ No newline at end of file diff --git a/php-storm.py b/php-storm.py new file mode 100644 index 0000000..67ff5fd --- /dev/null +++ b/php-storm.py @@ -0,0 +1,26 @@ +from talon.voice import Context, Key + +ides = { + "com.jetbrains.PhpStorm", +} + + +ctx = Context('phpstorm', func=lambda app, win: any( + i in app.bundle for i in ides)) + +keymap = { + '(phiz | fizz)': ['php', Key('tab')], + # 'search': [Key('shift'), Key('shift')], + 'golf': ['fore', Key('tab')], + 'chop': ['forek', Key('tab')], + 'bat': ['?=', Key('tab')], + 'if': ['if', Key('tab')], + 'complete': Key('cmd-shift-enter'), + 'definition': Key('alt-space'), +} + + +ctx.keymap(keymap) + + +def unload(): ctx.unload() \ No newline at end of file diff --git a/slack.py b/slack.py new file mode 100644 index 0000000..f95346a --- /dev/null +++ b/slack.py @@ -0,0 +1,13 @@ +from talon.voice import Context, Key + +ctx = Context('slack', bundle='com.tinyspeck.slackmacgap') + +keymap = { + 'channel': Key('cmd-k'), + 'channel last': Key('alt-up'), + 'channel next': Key('alt-down'), + 'tools command': ['``', Key('left')], + 'tools code': ['``````', Key('left left left return return up')], +} + +ctx.keymap(keymap) \ No newline at end of file diff --git a/spotify.py b/spotify.py new file mode 100644 index 0000000..10d8803 --- /dev/null +++ b/spotify.py @@ -0,0 +1,24 @@ +from talon.voice import Context, Key + +ides = { + "com.spotify.client", +} + + +ctx = Context('phpstorm', func=lambda app, win: any( + i in app.bundle for i in ides)) + +keymap = { + '(play | pause)': Key('space'), + 'search': Key('cmd-l'), + '(lower volume | quieter)': Key('cmd-down'), + '((pump | raise) volume | louder)': Key('cmd-up'), + '(next song | next | next track)': Key('cmd-right'), + '(last song | last | last track)': Key('cmd-left'), +} + + +ctx.keymap(keymap) + + +def unload(): ctx.unload() \ No newline at end of file diff --git a/std.py b/std.py new file mode 100644 index 0000000..c889991 --- /dev/null +++ b/std.py @@ -0,0 +1,434 @@ +from talon.voice import Word, Context, Key, Rep, RepPhrase, Str, press +from talon import ctrl, clip +from talon_init import TALON_HOME, TALON_PLUGINS, TALON_USER +import string + +alpha_alt = 'air bat cap die each fail gone harm sit jury crash look mad near odd pit quest red sun trap urge vest whale box yes zip'.split() +### +alnum = list(zip(alpha_alt, string.ascii_lowercase)) + [(str(i), str(i)) for i in range(0, 10)] + +alpha = {} +alpha.update(dict(alnum)) +alpha.update({'ship %s' % word: letter for word, letter in zip(alpha_alt, string.ascii_uppercase)}) + +# modifier key mappings +fkeys = [(f'F {i}', f'f{i}') for i in range(1, 13)] +keys = [ + 'left', 'right', 'up', 'down', 'shift', 'tab', 'escape', 'enter', 'space', + 'backspace', 'delete', 'home', 'pageup', 'pagedown', 'end', 'fn', +] +keys = alnum + [(k, k) for k in keys] +keys += [ + ('tilde', '`'), + ('comma', ','), + ('dot', '.'), + ('slash', '/'), + ('(semi | semicolon)', ';'), + ('quote', "'"), + ('[left] square', '['), + ('(right | are) square', ']'), + ('backslash', '\\'), + ('minus', '-'), + ('equals', '='), +] + fkeys +alpha.update({word: Key(key) for word, key in fkeys}) +alpha.update({'control %s' % k: Key('ctrl-%s' % v) for k, v in keys}) +alpha.update({'control shift %s' % k: Key('ctrl-shift-%s' % v) for k, v in keys}) +alpha.update({'control alt %s' % k: Key('ctrl-alt-%s' % v) for k, v in keys}) +alpha.update({'command %s' % k: Key('cmd-%s' % v) for k, v in keys}) +alpha.update({'command shift %s' % k: Key('cmd-shift-%s' % v) for k, v in keys}) +alpha.update({'command alt shift %s' % k: Key('cmd-alt-shift-%s' % v) for k, v in keys}) +alpha.update({'alt %s' % k: Key('alt-%s' % v) for k, v in keys}) +alpha.update({'alt shift %s' % k: Key('alt-%s' % v) for k, v in keys}) + +# cleans up some Dragon output from +mapping = { + 'semicolon': ';', + 'new-line': '\n', + 'new-paragraph': '\n\n', +} +# used for auto-spacing +punctuation = set('.,-!?') + +def parse_word(word): + word = str(word).lstrip('\\').split('\\', 1)[0] + word = mapping.get(word, word) + return word + +def join_words(words, sep=' '): + out = '' + for i, word in enumerate(words): + if i > 0 and word not in punctuation: + out += sep + out += word + return out + +def parse_words(m): + return list(map(parse_word, m.dgndictation[0]._words)) + +def insert(s): + Str(s)(None) + +def text(m): + insert(join_words(parse_words(m)).lower()) + +def sentence_text(m): + text = join_words(parse_words(m)).lower() + insert(text.capitalize()) + +def word(m): + text = join_words(list(map(parse_word, m.dgnwords[0]._words))) + insert(text.lower()) + +def surround(by): + def func(i, word, last): + if i == 0: word = by + word + if last: word += by + return word + return func + +def rot13(i, word, _): + out = '' + for c in word.lower(): + if c in string.ascii_lowercase: + c = chr((((ord(c) - ord('a')) + 13) % 26) + ord('a')) + out += c + return out + +formatters = { + 'dunder': (True, lambda i, word, _: '__%s__' % word if i == 0 else word), + 'camel': (True, lambda i, word, _: word if i == 0 else word.capitalize()), + 'snake': (True, lambda i, word, _: word if i == 0 else '_'+word), + 'smash': (True, lambda i, word, _: word), + # spinal or kebab? + 'kebab': (True, lambda i, word, _: word if i == 0 else '-'+word), + # 'sentence': (False, lambda i, word, _: word.capitalize() if i == 0 else word), + 'title': (False, lambda i, word, _: word.capitalize()), + 'allcaps': (False, lambda i, word, _: word.upper()), + 'dubstring': (False, surround('"')), + 'string': (False, surround("'")), + 'padded': (False, surround(" ")), + 'rot-thirteen': (False, rot13), +} + +def FormatText(m): + fmt = [] + for w in m._words: + if isinstance(w, Word): + fmt.append(w.word) + try: + words = parse_words(m) + except AttributeError: + with clip.capture() as s: + press('cmd-c') + words = s.get().split(' ') + if not words: + return + + tmp = [] + spaces = True + for i, word in enumerate(words): + word = parse_word(word) + for name in reversed(fmt): + smash, func = formatters[name] + word = func(i, word, i == len(words)-1) + spaces = spaces and not smash + tmp.append(word) + words = tmp + + sep = ' ' + if not spaces: + sep = '' + Str(sep.join(words))(None) + +ctx = Context('input') +keymap = {} +keymap.update(alpha) +keymap.update({ + 'say [over]': text, + + 'sentence [over]': sentence_text, + 'comma [over]': [', ', text], + 'period [over]': ['. ', sentence_text], + 'more [over]': [' ', text], + 'word ': word, + + '(%s)+ []' % (' | '.join(formatters)): FormatText, + + 'tab': Key('tab'), + '(backtab | back tab)': Key('shift-tab'), + '(left | lef)': Key('left'), + 'right': Key('right'), + 'up': Key('up'), + 'down': Key('down'), + + 'delete': Key('backspace'), + + 'quit': Key('cmd-q'), + 'kill': Key('ctrl-c'), + + # Spectacle + '(fullscreen | full)': Key('cmd-alt-f'), + 'snapleft': Key('cmd-alt-left'), + 'snapright': Key('cmd-alt-right'), + + + 'spotlight': Key('cmd-space'), + + + 'cut': Key('cmd-x'), + 'copy': Key('cmd-c'), + 'paste': Key('cmd-v'), + 'undo': Key('cmd-z'), + 'redo': Key('cmd-shift-z'), + 'select all': Key('cmd-a'), + 'find': Key('cmd-f'), + 'find again': Key('cmd-g'), + 'print': Key('cmd-p'), + 'hide': Key('cmd-h'), + 'hide others': Key('alt-cmd-h'), + '(mini | minimize)': Key('cmd-m'), + 'save': Key('cmd-s'), + 'save as': Key('shift-cmd-s'), + 'open': Key('cmd-o'), + 'close': Key('cmd-w'), + 'emoji': Key('ctrl-cmd-space'), + 'new folder': Key('shift-cmd-n'), + '(settings | preferences)': Key('cmd-,'), + 'trash': Key('cmd-delete'), + + # Finder + 'documents': Key('shift-cmd-o'), + 'desktop': Key('shift-cmd-d'), + 'finder': Key('cmd-alt-space'), + 'icon': Key('cmd-1'), + 'list': Key('cmd-2'), + 'column': Key('cmd-3'), + 'cover': Key('cmd-4'), + 'parent folder': Key('cmd-up'), + 'open folder': Key('cmd-down'), + # 'pathway': Key('cmd-alt-c'), + + # Zoom + 'zoom in': Key('cmd-+'), + 'zoom out': Key('cmd--'), + + '(comment | uncomment)': Key('cmd-/'), + 'start': Key('cmd-left'), + 'end': Key('cmd-right'), + '(top | go to top)': Key('cmd-up'), + 'flip': Key('fn-up'), + 'flop': Key('fn-down'), + 'jump [right] word': Key('alt-right'), + 'jump left word': Key('alt-left'), + + + + 'slap': [Key('cmd-right enter')], + 'enter': Key('enter'), + '(escape | scape)': Key('esc'), + 'question [mark]': '?', + 'tilde': '~', + '(bang | exclamation point)': '!', + 'dollar [sign]': '$', + '(downscore | score)': '_', + '(dubscore | double downscore)': '__', + '(semi | semicolon)': ';', + 'colon': ':', + '(square | left square [bracket] | bracket)': '[', '(rsquare | are square | right square [bracket])': ']', + '(paren | left paren)': '(', '(rparen | are paren | right paren)': ')', + '(brace | left brace)': '{', '(rbrace | are brace | right brace)': '}', + '(angle | left angle | less than)': '<', '(rangle | are angle | right angle | greater than)': '>', + + '(star | asterisk)': '*', + '(pound | hash [sign] | octo | thorpe | number sign)': '#', + 'percent [sign]': '%', + 'caret': '^', + 'at sign': '@', + '(and sign | ampersand | amper)': '&', + 'pipe': '|', + + '(dubquote | double quote)': '"', + 'quote': "'", + 'triple quote': "'''", + '(dot | period)': '.', + 'comma': ',', + 'space': ' ', + '[forward] slash': '/', + 'backslash': '\\', + + '(dot dot | dotdot)': '..', + 'cd': 'cd ', + 'cd talon home': 'cd {}'.format(TALON_HOME), + 'cd talon user': 'cd {}'.format(TALON_USER), + 'cd talon plugins': 'cd {}'.format(TALON_PLUGINS), + + 'run make (durr | dear)': 'mkdir ', + 'run get': 'git ', + 'run get (R M | remove)': 'git rm ', + 'run get add': 'git add ', + 'run get bisect': 'git bisect ', + 'run get branch': 'git branch ', + 'run get checkout': 'git checkout ', + 'run get clone': 'git clone ', + 'run get commit': 'git commit ', + 'run get diff': 'git diff ', + 'run get fetch': 'git fetch ', + 'run get grep': 'git grep ', + 'run get in it': 'git init ', + 'run get log': 'git log ', + 'run get merge': 'git merge ', + 'run get move': 'git mv ', + 'run get pull': 'git pull ', + 'run get push': 'git push ', + 'run get rebase': 'git rebase ', + 'run get reset': 'git reset ', + 'run get show': 'git show ', + 'run get status': 'git status ', + 'run get tag': 'git tag ', + 'run (them | vim)': 'vim ', + 'run L S': 'ls\n', + 'dot pie': '.py', + 'run make': 'make\n', + 'run jobs': 'jobs\n', + + # javascript + 'const': 'const ', + 'let': 'let ', + 'var': 'var ', + + 'static': 'static ', + 'tip pent': 'int ', + 'tip char': 'char ', + 'tip byte': 'byte ', + 'tip pent 64': 'int64_t ', + 'tip you went 64': 'uint64_t ', + 'tip pent 32': 'int32_t ', + 'tip you went 32': 'uint32_t ', + 'tip pent 16': 'int16_t ', + 'tip you went 16': 'uint16_t ', + 'tip pent 8': 'int8_t ', + 'tip you went 8': 'uint8_t ', + 'tip size': 'size_t', + 'tip float': 'float ', + 'tip double': 'double ', + + 'args': ['()', Key('left')], + 'index': ['[]', Key('left')], + 'block': [' {}', Key('left enter enter up tab')], + 'empty array': '[]', + 'empty dict': '{}', + + 'state (def | deaf | deft)': 'def ', + 'state else if': 'elif ', + 'state if': 'if ', + 'state else if': [' else if ()', Key('left')], + 'state while': ['while ()', Key('left')], + 'state for': ['for ()', Key('left')], + 'state for': 'for ', + 'state switch': ['switch ()', Key('left')], + 'state case': ['case \nbreak;', Key('up')], + 'state goto': 'goto ', + 'state import': 'import ', + 'state class': 'class ', + + 'state include': '#include ', + 'state include system': ['#include <>', Key('left')], + 'state include local': ['#include ""', Key('left')], + 'state type deaf': 'typedef ', + 'state type deaf struct': ['typedef struct {\n\n};', Key('up'), '\t'], + + 'comment see': '// ', + 'comment py': '# ', + + 'word queue': 'queue', + 'word eye': 'eye', + 'word bson': 'bson', + 'word iter': 'iter', + 'word no': 'NULL', + 'word cmd': 'cmd', + 'word dup': 'dup', + 'word streak': ['streq()', Key('left')], + 'word printf': 'printf', + 'word (dickt | dictionary)': 'dict', + 'word shell': 'shell', + + 'word lunixbochs': 'lunixbochs', + 'word talon': 'talon', + 'word Point2d': 'Point2d', + 'word Point3d': 'Point3d', + 'title Point': 'Point', + 'word angle': 'angle', + + 'dunder in it': '__init__', + 'self taught': 'self.', + 'dickt in it': ['{}', Key('left')], + 'list in it': ['[]', Key('left')], + 'string utf8': "'utf8'", + 'state past': 'pass', + + 'equals': '=', + '(double equals | dub equals)': '==', + '(trip | triple equals)': '===', + '(minus | dash)': '-', + 'plus': '+', + 'arrow': '->', + '(fat arrow | fatty)': '=>', + '(call | parens | parenthesis)': '()', + 'indirect': '&', + 'dereference': '*', + '(op equals | assign)': ' = ', + 'op (minus | subtract)': ' - ', + 'op (plus | add)': ' + ', + 'op (times | multiply)': ' * ', + 'op divide': ' / ', + 'op mod': ' % ', + '[op] (minus | subtract) equals': ' -= ', + '[op] (plus | add) equals': ' += ', + '[op] (times | multiply) equals': ' *= ', + '[op] divide equals': ' /= ', + '[op] mod equals': ' %= ', + + '(op | is) greater [than]': ' > ', + '(op | is) less [than]': ' < ', + '(op | is) equal': ' == ', + '(op | is) not equal': ' != ', + '(op | is) greater [than] or equal': ' >= ', + '(op | is) less [than] or equal': ' <= ', + '(op (power | exponent) | to the power [of])': ' ** ', + 'op and': ' && ', + 'op or': ' || ', + '[op] (logical | bitwise) and': ' & ', + '[op] (logical | bitwise) or': ' | ', + '(op | logical | bitwise) (ex | exclusive) or': ' ^ ', + '[(op | logical | bitwise)] (left shift | shift left)': ' << ', + '[(op | logical | bitwise)] (right shift | shift right)': ' >> ', + '(op | logical | bitwise) and equals': ' &= ', + '(op | logical | bitwise) or equals': ' |= ', + '(op | logical | bitwise) (ex | exclusive) or equals': ' ^= ', + '[(op | logical | bitwise)] (left shift | shift left) equals': ' <<= ', + '[(op | logical | bitwise)] (right shift | shift right) equals': ' >>= ', + + 'shebang bash': '#!/bin/bash -u\n', + + 'new window': Key('cmd-n'), + 'next window': Key('cmd-`'), + 'last window': Key('cmd-shift-`'), + 'next app': Key('cmd-tab'), + 'last app': Key('cmd-shift-tab'), + 'next tab': Key('ctrl-tab'), + 'new tab': Key('cmd-t'), + 'last tab': Key('ctrl-shift-tab'), + + 'next space': Key('cmd-alt-ctrl-right'), + 'last space': Key('cmd-alt-ctrl-left'), + + 'scroll down': [Key('down')] * 30, + 'scroll down more': [Key('down')] * 60, + 'scroll up': [Key('up')] * 30, + 'scroll up more': [Key('up')] * 60, +}) +ctx.keymap(keymap) + + + diff --git a/switcher.py b/switcher.py new file mode 100644 index 0000000..c9b7e29 --- /dev/null +++ b/switcher.py @@ -0,0 +1,67 @@ +from talon.voice import Word, Context, Key, Rep, Str, press +from talon import ui +import time +import os + +running = {} +launch = {} + +def switch_app(m): + name = str(m['switcher.running'][0]) + full = running.get(name) + if not full: return + for app in ui.apps(): + if app.name == full: + app.focus() + # TODO: replace sleep with a check to see when it is in foreground + time.sleep(0.25) + break + +def launch_app(m): + name = str(m['switcher.launch'][0]) + path = launch.get(name) + if path: + ui.launch(path=path) + +ctx = Context('switcher') +ctx.keymap({ + 'focus {switcher.running}': switch_app, + 'launch {switcher.launch}': launch_app, +}) + +def update_lists(): + global running + global launch + new = {} + for app in ui.apps(): + if app.background and not app.windows(): + continue + words = app.name.split(' ') + for word in words: + if word and not word in new: + new[word] = app.name + new[app.name] = app.name + running = new + ctx.set_list('running', running.keys()) + + new = {} + for base in '/Applications', '/Applications/Photoshop', '/Applications/Utilities': + for name in os.listdir(base): + path = os.path.join(base, name) + name = name.rsplit('.', 1)[0] + new[name] = path + words = name.split(' ') + for word in words: + if word and word not in new: + if len(name) > 6 and len(word) < 3: + continue + new[word] = path + launch = new + ctx.set_list('launch', launch.keys()) + +def ui_event(event, arg): + if event in ('app_activate', 'app_launch', 'app_close', 'win_open', 'win_close'): + update_lists() + +ui.register('', ui_event) +update_lists() diff --git a/talon-control.py b/talon-control.py new file mode 100644 index 0000000..e1d6d2d --- /dev/null +++ b/talon-control.py @@ -0,0 +1,114 @@ +# Provides various facilities to interact with Talon itself +# "talon sleep" - Disables recognition except for other Talon control features listed here +# "talon wake" - Re-enables recognition +# And other commands for activating and controlling the various eye tracking modes + +# WARNING: Because this script uses ContextGroup, changes to the script may not be recognized until a Talon restart. + +from talon import app +from talon.api import lib +from talon.engine import engine +from talon.voice import Context, ContextGroup, talon +import eye_mouse, os + +# Because the Voice Commands on this Context are in their own ContextGroup, they are treated separatedly from the other ContextGroups (including the default one: talon) +# By disabling the default talon ContextGroup, we can effectively turn off recognition, save for the Voice Commands in this file +def enable_talon(): + talon.enable() + app.icon_color(0, 0.7, 0, 1) + lib.menu_check(b'!Enable Speech Recognition', True) + + # Ensure Dragon dictation is off, even if it wasn't enabled + engine.mimic('go to sleep'.split()) + +def disable_talon(): + talon.disable() + app.icon_color(1, 0, 0, 1) + lib.menu_check(b'!Enable Speech Recognition', False) + +def enable_dragon_mode(): + disable_talon() + engine.mimic('wake up'.split()) + +# Creates extra menu item for toggling Speech Recognition +def on_menu(item): + if item == '!Enable Speech Recognition': + if talon.enabled: + disable_talon() + else: + enable_talon() + +app.register('menu', on_menu) + + + +# Adapted from debug.py listed in the Slash channel +def debug_listener(topic, m): + if topic == 'cmd' and m['cmd']['cmd'] == 'g.load' and m['success'] == True: + print('[grammar reloaded]') + else: + print(topic, m) + +is_debug_enabled = False + +def set_debug_enabled(enable): + global is_debug_enabled + + # Respect the enabled-ness of default talon ContextGroup for these Voice Commands + # Also, no point in re-registering + if not talon.enabled or enable == is_debug_enabled: + return + + if enable: + engine.register('', debug_listener) + else: + engine.unregister('', debug_listener) + + is_debug_enabled = enable + + + +def open_debug_log(m): + # Opens the Talon logs in Console.app, which is where print statements and debugging data is usually sent unless Repl is active + os.system('open -a Console ~/.talon/talon.log') + + + +def on_eye_control(menu_string): + # Respect the enabled-ness of default talon ContextGroup for these Voice Commands + if talon.enabled: + eye_mouse.on_menu(menu_string) + + + +context_group = ContextGroup('talon_control') +context = Context('talon_control', group=context_group) + +context.keymap({ + # Enable/disable voice recognition + 'talon [voice] sleep': lambda m: disable_talon(), + 'talon [voice] wake': lambda m: enable_talon(), + + # Switch between Dragon dictation and Talon recognition + 'talon dragon mode': lambda m: enable_dragon_mode(), + 'talon standard mode': lambda m: enable_talon(), + + # Enable/disable debug logging to console + 'talon [voice] debugging on': lambda m: set_debug_enabled(True), + 'talon [voice] debugging off': lambda m: set_debug_enabled(False), + + # Open talon.log in Console.app + 'talon show log': open_debug_log, + + # Toggle various eye tracking systems + 'talon (calibrate | calibration)': lambda m: on_eye_control('Eye Tracking >> Calibrate'), + 'talon mouse [control]': lambda m: on_eye_control('Eye Tracking >> Control Mouse'), + 'talon zoom [mouse]': lambda m: on_eye_control('Eye Tracking >> Control Mouse (Zoom)'), # Currently doesn't work for unknown reasons + 'talon keyboard': lambda m: on_eye_control('Eye Tracking >> Keyboard'), # Currently doesn't work for unknown reasons + 'talon eye debug': lambda m: on_eye_control('Eye Tracking >> Show Debug Overlay'), + 'talon eye camera': lambda m: on_eye_control('Eye Tracking >> Show Camera Overlay'), +}) + +# Startup. +enable_talon() +context_group.load() \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..5c9323b --- /dev/null +++ b/utils.py @@ -0,0 +1,53 @@ +# Various generic tools +# To use in other files, for example: "from user.utils import parse_words_as_integer" +# or "from .utils import parse_words_as_integer" + +# NOTE: If you see an error from Talon about "ImportError: cannot import name X", +# where X is one of these functions, restart Talon + +import itertools + + +# Useful for identifying app/window information for context selection +def context_func(app, win): + print('---') + # print(app) + print(app.bundle) + print(win) + print(win.title) + print(win.doc) + print('---') + return True + + +number_conversions = { + 'oh': '0', # 'oh' => zero +} +for i, w in enumerate(['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']): + number_conversions.update({ + str(i): str(i), + w: str(i), + '%s\\number' % (w): str(i), + }) + + +# TODO: Once implemented, use number input value rather than manually parsing number words with this function +def parse_words_as_integer(words): + # Ignore any potential trailing non-number words + number_words = list(itertools.takewhile(lambda w: str(w) in number_conversions, words)) + + # Somehow, no numbers were detected + if len(number_words) == 0: + return None + + # Map number words to simple number values + number_values = list(map(lambda w: number_conversions[w.word], number_words)) + + # Join the numbers into single string, and remove leading zeros + number_string = ''.join(number_values).lstrip('0') + + # If the entire sequence was zeros, return single zero + if len(number_string) == 0: + return 0 + + return int(number_string) diff --git a/vs-code.py b/vs-code.py new file mode 100644 index 0000000..be6280a --- /dev/null +++ b/vs-code.py @@ -0,0 +1,60 @@ +from talon.voice import Context, Key, press, Str +from user.utils import parse_words_as_integer + +context = Context('VSCode', bundle='com.microsoft.VSCode') + +def jump_to_line(m): + line_number = parse_words_as_integer(m._words[1:]) + + if line_number == None: + return + + # Zeroth line should go to first line + if line_number == 0: + line_number = 1 + + # TODO: Directly interface with VSCode to accomplish the following + + # Open the jump to line input + press('ctrl-g') + + # TODO: If requesting line that is beyond the end of the focused document, jump to last line instead + + # Enter whole line number data as if from keyboard + Str(str(line_number))(None) + + # Confirm the navigation + press('enter') + + # Position cursor at the beginning of meaningful text on the current line (Mac OS X) + press('cmd-right') + press('cmd-left') + +def jump_to_next_word_instance(m): + press('escape') + press('cmd-f') + Str(' '.join([str(s) for s in m.dgndictation[0]._words]))(None) + press('escape') + +context.keymap({ + # Navigating text + 'pizza (0 | oh | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)+': jump_to_line, + + + + # Selecting text + 'select line': Key('cmd-right cmd-shift-left'), + 'select start': Key('cmd-shift-left'), + 'select end': Key('cmd-shift-right'), + 'select word': Key('alt-shift-right'), + 'select left word': Key('alt-shift-left'), + 'select right': Key('shift-right'), + 'select left': Key('shift-left'), + 'select instances': Key('cmd-shift-l'), + + # Finding text + '(previous | last)': Key('cmd-shift-g'), + 'find next ': jump_to_next_word_instance, + + +}) \ No newline at end of file diff --git a/words.py b/words.py new file mode 100644 index 0000000..6be81e7 --- /dev/null +++ b/words.py @@ -0,0 +1,79 @@ +# from talon.voice import Context, Key, Str + +# ctx = Context('words') +# def shrink_word(m): +# word = str(m.dgndictation[0]._words[0]).lower() +# if not word in shrink_map: +# raise Exception('%s not in shrink map' % word) +# Str(shrink_map[word])(None) + +# keymap = { +# 'dot company': '.com', +# 'word parse integer': ['parseInt()', Key('left')], + +# 'word and': 'end', +# 'word a sink': 'async', +# 'word acxiom': 'axios', +# 'word adam': 'atom', +# 'word cycling': 'cyclom', +# 'word define': 'def ', +# 'word doctor': 'docker', +# 'word for each': 'forEach', +# 'word import': 'import ', +# 'word display in line': 'display: inline-block;', +# 'word display block': 'display: block;', +# 'word get': 'git', +# 'word get hub': 'github', +# 'word glitch': 'glitch', +# 'word just': 'gist', +# 'word hero': 'heroku', +# 'word protocol': 'http', +# 'word secure cprotocol': 'http', +# 'word jason': 'JSON', +# 'word no': 'null', +# 'word no super': 'NULL', +# 'word printf': 'printf', +# 'word slack': 'slack', +# 'word string': 'JSON.stringify', +# 'word pseudo-\\\\pseudo': 'sudo ', +# 'word them': 'vim', +# 'word two strings': 'toString', +# 'word will': 'twilio', +# 'word with': 'width', +# 'word while': 'wow', +# 'word zeppelin': 'zeplin', +# 'shrink ': shrink_word, +# } + +# shrink_map = { + +# 'administrator': 'admin', +# 'alternate': 'alt', +# 'apartment': 'apt', +# 'applications': 'apps', +# 'arguments': 'args', +# 'attributes': 'attrs', +# 'button': 'btn', +# 'command': 'cmd', +# 'compute': 'cmp', +# 'context': 'ctx', +# 'concatenate': 'concat', +# 'configure': 'config', +# 'control': 'ctrl', +# 'format': 'fmt', +# 'image': 'img', +# 'jason': 'json', +# 'message': 'msg', +# 'package': 'pkg', +# 'parameter': 'param', +# 'parameters': 'params', +# 'source': 'src', +# 'standard': 'std', +# 'temporary': 'tmp', +# 'text': 'txt', +# 'user': 'usr', +# 'user id': 'uid', +# 'utilities': 'utils', +# } + +# ctx.keymap(keymap) \ No newline at end of file