├── .gitignore ├── .isort.cfg ├── LICENSE ├── README.md ├── apps ├── amethyst.py ├── editor.py ├── firefox.py ├── jetbrains.py ├── jetbrains_psi.py ├── jira.py ├── one_password.py ├── onenote.py ├── outlook.py ├── selection.zsh ├── slack.py ├── spotify.py ├── terminal.py ├── ticktick.py ├── tridactyl.py ├── vscode │ ├── line.js │ ├── readme.md │ ├── search.js │ ├── terminal.js │ ├── vscode.py_ │ └── vscode_terminal.py_ ├── vscode_simple.py └── zoom.py ├── debug.py ├── lang └── language.py ├── misc ├── basic_keys.py ├── dictation.py ├── eye_3mon_snap.py ├── eye_control.py ├── eye_hide.py ├── eye_vertical_snap.py ├── help.py ├── luxafor.py ├── maestro.py ├── misc.py ├── mouse.py ├── mouse_jump.py ├── mouse_ocr.py ├── mouse_rc.py ├── mouse_snap.py ├── mouse_snap9.py ├── mouse_sonar.py ├── mouse_squid.py ├── noise.py ├── phrase_frequency.py ├── phrase_history.py ├── picker.py ├── popups.py ├── repeat.py ├── speech_toggle.py └── switcher.py ├── stubs.py ├── text ├── formatters.py ├── homophones.csv ├── homophones.py ├── jargon.json └── std.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /talon/ 3 | /talon_plugins/ 4 | .idea/ 5 | /vocab.json 6 | misc/warps.json 7 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | known_third_party = requests, talon, talon_plugins 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. 2 | 3 | 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. 4 | 5 | 6 | Some Portions Copyright (c) 2018 Ryan Hileman (modified talonvoice/examples code) 7 | 8 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anonfunc's Talon user files 2 | 3 | [Talon](https://talonvoice.com) scripts with a "real english words" approach. 4 | 5 | Most commands are organized around "verb adjective noun adverb". 6 | 7 | # Installation for Talon users 8 | 9 | Clone to `~/.talon/user`. 10 | 11 | Most of this is focused around programming in JetBrains IDEs, so you'll want to install [this plugin](https://github.com/anonfunc/voicecode-intellij). 12 | (Search the Marketplace for "Voicecode", it's installable that way.) 13 | 14 | # Full bootstrapping for users new to Talon 15 | 16 | - Install Talon from https://talonvoice.com or the #beta channel on the Talon Slack. 17 | - (Optional but recommended) Install Dragon (discontinued, buy from Amazon/Ebay. Physical copies included download codes.) 18 | - (Required if using Dragon) Install Dragon [6.0.8 update](https://dnsriacontent.nuance.com/dpifm/EN/6.0.8/Dragon_14812.zip) 19 | - (Optional, but recommended) Buy a Tobii 4C (also on Amazon) 20 | - Start Talon, start Dragon (?). 21 | - Follow "Installation for Talon users" instructions. 22 | 23 | ## Taxonomy of verbs: 24 | Not comprehensive, look at the scripts for the final word. 25 | 26 | ### Go: changes the "current" location (focus, cursor, etc.) in the current application. 27 | - `Go back` 28 | - `Go next tab` 29 | - `Go unread` 30 | 31 | ### Focus: changes to a different application, space or desktop. 32 | - `Focus Slack` 33 | - `Focus Space 5` 34 | - `Focus 5` (Space is implied) 35 | - `Focus next window` (Using [Amethyst](https://github.com/ianyh/Amethyst)) 36 | 37 | ### Editor commands: 38 | - Go moves the cursor: `go down`, `go line start` 39 | - Select moves the cursor and selects: `select all`, `select line`, `select word left` 40 | - Clear selects and then deletes: `delete line 99`, `delete word right` 41 | 42 | ### IDE commands: 43 | - Quick fixes: `fix this`, `fix next error`, `fix line 31` 44 | - Dragging lines: `drag up/down` 45 | - Going to even more things: `go next method`, `go declaration` 46 | - Growing/shrinking selection: `select more`, `select less` 47 | - Refactorings: `refactor signature`, `extract variable` 48 | 49 | ### Toggle: 50 | - `Toggle Dark` (turns on screen saver) 51 | - `Toggle history` 52 | - `Toggle frequency` 53 | 54 | ### Repeating or Extending commands: 55 | - `repeat 3`: For commands. 56 | - `extend`, `extend 2`: For things like selections/deletions. 57 | 58 | ### Misc: 59 | - `alfred`: Launch [Alfred](https://www.alfredapp.com/) 60 | - `snippet`: Alfred snippets 61 | - `clippings`: Alfred clipboard manager 62 | - `learn selection`: Saves the current selection to ~/.talon/vocab.json, which is injected into the Dragon vocab. 63 | 64 | ### Text Insertion: 65 | - `say blah blah blah over` -> `blah blah blah` 66 | - `acronym automated teller machine` -> `ATM` 67 | - `tree long` -> `lon` 68 | - `quad longer` -> `long` 69 | - `dunder set` -> `__set__` 70 | - `dunder quad initialize` -> `__init__` (Formatters are stackable.) 71 | - `camel new ATM machine` -> `newAtmMachine` 72 | - `private new ATM machine` -> `newATMMachine` 73 | - `public new ATM machine` -> `NewATMMachine` 74 | - `call mathod` -> `.method` 75 | - `snake two words` -> `two_words` 76 | - `spine two words` -> `two-words` 77 | - `smash two words` -> `twowords` 78 | - `sentence the quick red fox` -> `The quick red fox` 79 | - `jargon jason` -> `json` (From `~/.talon/user/jargon.json`) 80 | - `title watership down` -> `Watership Down` 81 | - `allcaps / lowcaps Proper Noun` -> `PROPER NOUN` / `proper noun` 82 | - `string foo` / `ticks foo` -> `"foo"` `'foo'` 83 | - `backticks foo` 84 | 85 | ## Example Nouns 86 | - `line` 87 | - `left/right/up/down` 88 | - `word left/right/up/down` 89 | - `camel left/right/up/down` (IDE only) 90 | 91 | ## Standard Talon Alphabet 92 | - `air bat cap drum each fine gust harp sit jury crunch look made near odd pit quench red sun trap urge vest whale plex yank zip` 93 | - `ship air bat cap` -> `ABC` 94 | - `ship air sunk bat cap` -> `Abc` 95 | - `uppercase air lowercase bat cap` -> `Abc` 96 | - `shift air bat cap` -> `Abc` 97 | 98 | ## Misc 99 | 100 | ### stubs.py 101 | 102 | Generates .pyi files from a dump of the talon packages, so you'll have some completion in PyCharm. 103 | 104 | ### Portions from: 105 | 106 | * [Official Example Scripts](https://github.com/talonvoice/examples) 107 | * [zdwiel's scripts](https://github.com/dwiel/talon_community) 108 | * [tabrat's scripts](https://github.com/tabrat/talon_user) 109 | * [tuomassalo's scripts](https://github.com/tuomassalo/talon_user) 110 | * [dopey's scripts](https://github.com/dopey/talon_user) 111 | * [dwighthouse/unofficial-talonvoice-docs](https://github.com/dwighthouse/unofficial-talonvoice-docs) -------------------------------------------------------------------------------- /apps/amethyst.py: -------------------------------------------------------------------------------- 1 | from talon import ctrl 2 | from talon.voice import Context, Key 3 | 4 | from ..utils import numerals, text_to_number, delay 5 | 6 | ctx = Context("amethyst") 7 | 8 | 9 | def mod1(key): 10 | return Key("alt+shift+" + key) 11 | 12 | 13 | def mod2(key): 14 | def do_it(_): 15 | ctrl.key_press(key, shift=True, ctrl=True, alt=True) 16 | 17 | return do_it 18 | 19 | 20 | def number(control=False, mod_one=False): 21 | def do_it(m): 22 | # noinspection PyProtectedMember 23 | num = text_to_number(m._words[-1]) 24 | if num < 10: 25 | ctrl.key_press(str(num), ctrl=control, alt=mod_one, shift=mod_one) 26 | 27 | return do_it 28 | 29 | 30 | ctx.keymap( 31 | { 32 | ### 33 | # Spaces! 34 | ### 35 | f"move to next space": mod2("right"), 36 | f"move to last space": mod2("left"), 37 | # Needs space shortcuts 38 | f"move to [space] {numerals}": [number(mod_one=True, control=True), number(control=True)], 39 | f"send to [space] {numerals}": number(mod_one=True, control=True), 40 | # Not strictly Amethyst 41 | f"focus [space] {numerals}": number(control=True), 42 | "mission control": Key("ctrl-up"), 43 | "(app | application) windows": Key("ctrl-down"), 44 | f"focus next space": Key("ctrl-right"), 45 | f"focus last space": Key("ctrl-left"), 46 | 47 | ### 48 | # Top / Bottom Screens, Bottom Main 49 | ### 50 | "focus [the] (first | bottom) screen": mod1("w"), 51 | "send [to] (first | bottom) screen": [mod2("w"), delay(0.3), mod1("e")], 52 | "move [to] (first | bottom) screen": mod2("w"), 53 | "swap [with] (first | bottom) screen": [mod2("w"), delay(0.3), mod1("j"), delay(0.3), mod2("e")], 54 | "take [from] (first | bottom) screen": [mod1("w"), delay(0.3), mod2("e")], 55 | 56 | "focus [the] (second | top) screen": mod1("e"), 57 | "send [to] (second | top) screen": [mod2("e"), delay(0.3), mod1("w")], 58 | "move [to] (second | top) screen": mod2("e"), 59 | "swap [with] (second | top) screen": [mod2("e"), delay(0.3), mod1("j"), delay(0.3), mod2("e")], 60 | "take [from] (second | top) screen": [mod1("e"), delay(0.3), mod2("w")], 61 | 62 | ### 63 | # Left / Middle / Right, Middle Main 64 | ### 65 | # "focus [the] (first | left)": mod1("w"), 66 | # "send [to] (first | left)": [mod2("w"), delay(0.3), mod1("e")], 67 | # "move [to] (first | left)": mod2("w"), 68 | # "swap [with] (first | left)": [mod2("w"), delay(0.3), mod1("j"), delay(0.3), mod2("e")], 69 | # "take [from] (first | left)": [mod1("w"), delay(0.3), mod2("e")], 70 | 71 | # "focus [the] (second | middle)": mod1("e"), 72 | # "send [to] (second | middle)": [mod2("e"), delay(0.3), mod1("w")], 73 | # "move [to] (second | middle)": mod2("e"), 74 | # "swap [with] (second | middle)": [mod2("e"), delay(0.3), mod1("j"), delay(0.3), mod2("e")], 75 | # "take [from] (second | middle)": [mod1("e"), delay(0.3), mod2("w")], 76 | 77 | # "focus [the] (third | right)": mod1("r"), 78 | # "send [to] (third | right)": [mod2("r"), delay(0.3), mod1("e")], 79 | # "move [to] (third | right)": mod2("r"), 80 | # "swap [with] (third | right)": [mod2("r"), delay(0.3), mod1("j"), delay(0.3), mod2("e")], 81 | # "take [from] (third | right)": [mod1("r"), delay(0.3), mod2("e")], 82 | 83 | ### 84 | # Pane / Window Commands 85 | # Calling the managed "windows" slots to distinguish from Cmd-` as "next window of app" 86 | ### 87 | "shrink slot": mod1("h"), 88 | "grow slot": mod1("l"), 89 | 90 | "more slots": mod1(","), 91 | "fewer slots": mod1("."), 92 | "[focus] last slot": mod1("j"), 93 | "move [to] last slot": mod2("j"), 94 | "[focus] next slot": mod1("k"), 95 | "move [to] next slot": mod2("k"), 96 | # Not binding these... 97 | # "move (next | clockwise) space": mod2("h"), 98 | # "move (last | previous | counter clockwise) space": mod2("l"), 99 | "move [to] main slot": mod1("return"), 100 | 101 | ### 102 | # Layout commands 103 | ### 104 | "toggle (float | floating)": mod1("t"), 105 | "toggle (amethyst | tiling)": mod2("t"), 106 | "next layout": mod1("space"), 107 | "last layout": mod2("down"), # REBIND in AMETHYST 108 | "show (layouts | layout)": mod1("i"), 109 | "tall layout": mod1("a"), 110 | "wide layout": mod1("s"), 111 | "fix layout": mod1("z"), 112 | "(full | full screen) layout": mod1("d"), 113 | "(call on | column) layout": mod1("f"), 114 | # "binary layout": mod1("g"), 115 | } 116 | ) 117 | -------------------------------------------------------------------------------- /apps/editor.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import talon.clip as clip 4 | from talon.voice import Context, Key, press 5 | 6 | from .. import utils 7 | from ..apps.jetbrains import port_mapping 8 | from ..text.homophones import raise_homophones 9 | from ..misc.basic_keys import alphabet 10 | 11 | try: 12 | from ..text.homophones import all_homophones 13 | 14 | # Map from every homophone back to the row it was in. 15 | homophone_lookup = { 16 | item.lower(): words for canon, words in all_homophones.items() for item in words 17 | } 18 | except ImportError: 19 | homophone_lookup = {"right": ["right", "write"], "write": ["right", "write"]} 20 | all_homophones = homophone_lookup.keys() 21 | 22 | extension = lambda _: None 23 | 24 | 25 | def extendable(d): 26 | def wrapper(m): 27 | global extension 28 | extension = Key(d) 29 | extension(m) 30 | 31 | return wrapper 32 | 33 | 34 | def set_extension(d): 35 | def wrapper(_): 36 | global extension 37 | extension = d 38 | 39 | return wrapper 40 | 41 | 42 | def do_extension(m): 43 | # noinspection PyProtectedMember 44 | count = max(utils.text_to_number([utils.parse_word(w) for w in m._words[1:]]), 1) 45 | for _ in range(count): 46 | extension(m) 47 | 48 | 49 | supported_apps = {"com.googlecode.iterm2"} 50 | supported_apps.update(port_mapping.keys()) 51 | 52 | 53 | def not_supported_editor(app, _): 54 | if str(app.bundle) in supported_apps: 55 | return False 56 | return True 57 | 58 | 59 | ctx = Context("editor", func=not_supported_editor) 60 | 61 | 62 | def fix_format(m): 63 | press("cmd-right") 64 | press("shift-cmd-left") 65 | s = "" 66 | with clip.capture() as _s: 67 | press("cmd-c", wait=2000) 68 | s = str(_s.get()) 69 | utils.insert(re.sub(r' +', ' ', s)) 70 | 71 | 72 | def select_text_from_cursor(direction): 73 | # jcooper-korg from talon slack 74 | def fn(m): 75 | words = utils.parse_words(m) 76 | if not words: 77 | return 78 | if direction == "left": 79 | more = "home" 80 | back = "right" 81 | else: 82 | more = "end" 83 | back = "left" 84 | key = utils.join_words(words).lower() 85 | keys = homophone_lookup.get(key, [key]) 86 | keys = sorted(keys, key=len, reverse=True) 87 | text = _get_line(direction, back, more).lower() 88 | result = -1 if direction == "left" else len(text) + 1 89 | for needle_up in keys: 90 | needle = needle_up.lower() 91 | find = text.find(needle.lower()) 92 | if direction == "left" and find > result: 93 | result = find 94 | key = needle 95 | # There could be a closer one... 96 | find = text.find(needle, result + 1) 97 | while find > result: 98 | result = find 99 | find = text.find(needle, result + 1) 100 | break 101 | if direction == "right" and find == -1: 102 | continue 103 | if direction == "right" and find < result: 104 | result = find 105 | key = needle 106 | break 107 | # print(direction, key, result, text) 108 | if result == (-1 if direction == "left" else len(text) + 1): 109 | return 110 | _select_in_text(direction, key, result, text) 111 | global extension 112 | extension = lambda _: fn(m) 113 | 114 | return fn 115 | 116 | 117 | def select_bounded_from_cursor(direction): 118 | # jcooper-korg from talon slack 119 | def fn(m): 120 | if direction == "left": 121 | more = "home" 122 | back = "right" 123 | else: 124 | more = "end" 125 | back = "left" 126 | keys = [alphabet[k] for k in m["editor.alphabet"]] 127 | regex = r"\b" + r"[^-_ .()]*?".join(keys) 128 | text = _get_line(direction, back, more) 129 | print(regex, text, direction) 130 | result = -1 if direction == "left" else len(text) + 1 131 | key = "" 132 | if direction == "left": 133 | print("going left") 134 | for hit in re.finditer(regex, text): 135 | result = hit.start() 136 | key = hit.string 137 | print(key, result, hit) 138 | else: 139 | match = re.search(regex, text) 140 | if match is None: 141 | return 142 | key = match.string 143 | result = match.start() 144 | print(key, result, match) 145 | if result == -1: 146 | return 147 | # print(direction, regex, key, result, text) 148 | _select_in_text(direction, key, result, text) 149 | global extension 150 | extension = lambda _: fn(m) 151 | 152 | return fn 153 | 154 | 155 | def _get_line(direction, back, more): 156 | press(direction, wait=2000) 157 | press(back, wait=2000) 158 | press("shift-" + more, wait=2000) 159 | with clip.capture() as s: 160 | press("cmd-c", wait=2000) 161 | press(back, wait=2000) 162 | text = s.get() 163 | 164 | return text 165 | 166 | 167 | def _select_in_text(direction, key, result, text): 168 | if direction == "left": 169 | count = len(text) - result 170 | size = len(key) 171 | else: 172 | count = result 173 | size = len(key) 174 | print(direction, text, key, result, len(text), count) 175 | # cursor over to the found key text 176 | for i in range(0, count): 177 | press(direction, wait=100) 178 | # now select the matching key text 179 | for i in range(0, size): 180 | press("shift-right") 181 | 182 | 183 | ctx.keymap( 184 | { 185 | "phones last [over]": [ 186 | select_text_from_cursor("left"), 187 | lambda m: raise_homophones(m, is_selection=True), 188 | ], 189 | "phones next [over]": [ 190 | select_text_from_cursor("right"), 191 | lambda m: raise_homophones(m, is_selection=True), 192 | ], 193 | f"extend {utils.optional_numerals}": do_extension, 194 | # moving 195 | "go word left": extendable("alt-left"), 196 | "go word right": extendable("alt-right"), 197 | "go line start": extendable("cmd-left"), 198 | "go line end": extendable("cmd-right"), 199 | "go way left": extendable("cmd-left"), 200 | "go way right": extendable("cmd-right"), 201 | "go way down": extendable("cmd-down"), 202 | "go way up": extendable("cmd-up"), 203 | "go phrase left": [utils.select_last_insert, extendable("left")], 204 | # selecting 205 | "select all": [Key("cmd-a")], 206 | "(correct | select phrase)": utils.select_last_insert, 207 | "select last [over]": select_text_from_cursor("left"), 208 | "select next [over]": select_text_from_cursor("right"), 209 | "select last bounded {editor.alphabet}+": select_bounded_from_cursor("left"), 210 | "select next bounded {editor.alphabet}+": select_bounded_from_cursor("right"), 211 | "select line": extendable("cmd-left cmd-left cmd-shift-right"), 212 | "select left": extendable("shift-left"), 213 | "select right": extendable("shift-right"), 214 | "select up": extendable("shift-up"), 215 | "select down": extendable("shift-down"), 216 | "select word left": [ 217 | Key("left shift-right left alt-left alt-right shift-alt-left"), 218 | set_extension(Key("shift-alt-left")), 219 | ], 220 | "select word right": [ 221 | Key("right shift-left right alt-right alt-left shift-alt-right"), 222 | set_extension(Key("shift-alt-right")), 223 | ], 224 | "select way left": extendable("cmd-shift-left"), 225 | "select way right": extendable("cmd-shift-right"), 226 | "select way up": extendable("cmd-shift-up"), 227 | "select way down": extendable("cmd-shift-down"), 228 | # deleting 229 | "clear phrase": [utils.select_last_insert, extendable("backspace")], 230 | "clear line": extendable("cmd-left cmd-left cmd-shift-right delete cmd-right"), 231 | "clear left": extendable("backspace"), 232 | "clear right": extendable("delete"), 233 | "clear up": extendable("shift-up delete"), 234 | "clear down": extendable("shift-down delete"), 235 | "clear word left": extendable("alt-backspace"), 236 | "clear word right": extendable("alt-delete"), 237 | "clear way left": extendable("cmd-shift-left delete"), 238 | "clear way right": extendable("cmd-shift-right delete"), 239 | "clear way up": extendable("cmd-shift-up delete"), 240 | "clear way down": extendable("cmd-shift-down delete"), 241 | # searching 242 | "search []": [Key("cmd-f"), utils.text], 243 | # clipboard 244 | "cut this": Key("cmd-x"), 245 | "copy this": Key("cmd-c"), 246 | "paste [here]": Key("cmd-v"), 247 | # Copying 248 | "copy phrase": [utils.select_last_insert, Key("cmd-c")], 249 | "copy all": [Key("cmd-a cmd-c")], 250 | "copy line": extendable("cmd-left cmd-left cmd-shift-right cmd-c cmd-right"), 251 | "copy word left": extendable("shift-alt-left cmd-c"), 252 | "copy word right": extendable("shift-alt-right cmd-c"), 253 | "copy way left": extendable("cmd-shift-left cmd-c"), 254 | "copy way right": extendable("cmd-shift-right cmd-c"), 255 | "copy way up": extendable("cmd-shift-up cmd-c"), 256 | "copy way down": extendable("cmd-shift-down cmd-c"), 257 | # Cutting 258 | "cut phrase": [utils.select_last_insert, Key("cmd-x")], 259 | "cut all": [Key("cmd-a cmd-x")], 260 | "cut line": extendable("cmd-left cmd-left cmd-shift-right cmd-x cmd-right"), 261 | "cut word left": extendable("shift-alt-left cmd-x"), 262 | "cut word right": extendable("shift-alt-right cmd-x"), 263 | "cut way left": extendable("cmd-shift-left cmd-x"), 264 | "cut way right": extendable("cmd-shift-right cmd-x"), 265 | "cut way up": extendable("cmd-shift-up cmd-x"), 266 | "cut way down": extendable("cmd-shift-down cmd-x"), 267 | "fix format": fix_format, 268 | } 269 | ) 270 | ctx.set_list("alphabet", alphabet.keys()) 271 | -------------------------------------------------------------------------------- /apps/firefox.py: -------------------------------------------------------------------------------- 1 | from talon.ui import active_app 2 | from talon.voice import Context, Key 3 | 4 | from ..utils import delay, text 5 | 6 | enabled = False 7 | 8 | 9 | def is_firefox(app, _): 10 | global enabled 11 | return enabled and app.name == "Firefox" 12 | 13 | 14 | # Assuming that https://addons.mozilla.org/en-US/firefox/addon/modeless-keyboard-navigation/ 15 | # is installed: 16 | # 17 | # open a link in the current tab 18 | # open a link in a new tab 19 | # open multiple links in new tabs 20 | # cycle forward to the next frame 21 | # open bookmark 22 | # open bookmark in a new tab 23 | # 24 | # Assuming https://addons.mozilla.org/en-US/firefox/addon/detach-tab/?src=search 25 | # Default keyboard shortcut for detaching tab is Ctrl+Shift+Space. 26 | # You should change it to MacCtrl+Shift+Space, as the other is really Command-Shift-Space. 27 | # 28 | # Assuming https://addons.mozilla.org/en-US/firefox/addon/tab_search/?src=search 29 | # Ctrl + Shift + F - Toggle extension (Windows/Linux) 30 | # Cmd + Shift + L - Toggle extension (macOS) 31 | # Ctrl + Backspace - Delete tab 32 | # Enter - Open selected tab or first in list if not selected 33 | # Up/Left - Select previous tab 34 | # Down/Right - Select next tab 35 | # Alt + R - Refresh tab 36 | # Alt + P - Pin tab 37 | # Ctrl + C - Copy Tab URL 38 | # Alt + Shift + D - Delete all duplicate tabs 39 | # Alt + M - Mute (only if tab is audible) 40 | # 41 | # 42 | 43 | ctx = Context("firefox", func=is_firefox) 44 | ctx.keymap( 45 | { 46 | "(follow | go link)": Key("ctrl-,"), 47 | "go tab": Key("ctrl-."), 48 | "go back": Key("cmd-["), 49 | "go forward": Key("cmd-]"), 50 | "clear tab": Key("cmd-w"), 51 | "restore tab": Key("cmd-shift-t"), 52 | "jump [] [over]": [ 53 | Key("cmd-shift-l"), 54 | delay(0.2), 55 | text, 56 | ], 57 | "tab search [] [over]": [Key("cmd-shift-l"), delay(0.2), text], 58 | # "open here [] [over]": [Key("o"), delay(0.1), text], 59 | # "open tab [] [over]": [Key("t"), delay(0.1), text], 60 | "search": Key("cmd-f"), 61 | "search [] [over]": [Key("cmd-f"), delay(0.1), text], 62 | "go next tab": Key("cmd-shift-]"), 63 | "go last tab": Key("cmd-shift-["), 64 | "toggle mark": Key("cmd-d"), 65 | "toggle reader": Key("cmd-alt-r"), 66 | "mark pin board": Key("alt-p"), 67 | "copy (URL | location)": [Key("cmd-l cmd-c")], 68 | # "go next page": Key("]]"), 69 | # "go last page": Key("[["), 70 | "zoom out": Key("cmd-="), 71 | "zoom in": Key("cmd--"), 72 | "zoom clear": Key("cmd-0"), 73 | "detach tab": Key("ctrl-shift-space"), 74 | } 75 | ) 76 | -------------------------------------------------------------------------------- /apps/jetbrains_psi.py: -------------------------------------------------------------------------------- 1 | # If you plan on adding more, the PsiViewer plugin is essential. 2 | # Poke around the tree, and note that I'm using the elementType / node field. 3 | # (Giving a regex here, so that Py:IF_STATEMENT is matched by IF_STATEMENT, STATEMENT and IF_) 4 | # If the elementType field varies depending on containing types, you can use | to specify more than one. 5 | # (i.e., you want 'parameter' to work in both methods and functions, but they have different element types) 6 | # The "_" gives a default ordinal/offset for the word. 7 | # 8 | # Putting ## in the path puts the ordinality modifier there instead of the end. 9 | # Notes: 10 | # - CSharp's AST is 80% dummy nodes. 11 | # We need a selector like: DUMMY_BLOCK>[regex] which filters by regex on element.text 12 | # in order to do anything useful. (Not entirely sure that would work either...) 13 | 14 | PSI_PATHS = { 15 | # "block": { 16 | # "_": "this", 17 | # "cs": "DUMMY_BLOCK,DUMMY_BLOCK" 18 | # }, 19 | "parameters": { 20 | "_": "this", # You probably want the parameters of the current function 21 | "+": ["left", None], 22 | "go": "FILE,METHOD_DECLARATION|FUNCTION_DECLARATION##,PARAMETERS", 23 | "java": "FILE,METHOD|FUNCTION##,^PARAMETER_LIST", 24 | "py": "FILE,FUNCTION_DECLARATION##,PARAMETER_LIST", 25 | "php": "File,Class method|function|Function##,Parameter list", 26 | "default": "FILE,DECLARATION##,PARAMETER", 27 | }, 28 | "parameter": { 29 | "_": 0, # You probably want the first parameter of the current function 30 | "+": [", space", None], 31 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,PARAMETERS,PARAMETER_DECLARATION", 32 | "java": "METHOD|FUNCTION,^PARAMETER_LIST,PARAMETER", 33 | "py": "FUNCTION_DECLARATION,PARAMETER", 34 | "php": "Class method|function|Function,Parameter list,Parameter", 35 | "default": "DECLARATION,PARAMETER", 36 | }, 37 | "parameter name": { 38 | "_": 0, # You probably want the first parameter of the current function 39 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,PARAMETERS,PARAMETER_DECLARATION##,PARAM_DEFINITION", 40 | "java": "METHOD|FUNCTION,^PARAMETER_LIST,PARAMETER##,IDENTIFIER", 41 | "py": "FUNCTION_DECLARATION,PARAMETER##,IDENTIFIER", 42 | "php": "Class method|function|Function,Parameter list,Parameter", 43 | }, 44 | "parameter type": { 45 | "_": 0, # You probably want the first parameter of the current function 46 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,PARAMETERS,PARAMETER_DECLARATION##,TYPE", 47 | "java": "METHOD|FUNCTION,^PARAMETER_LIST,PARAMETER##,TYPE,IDENTIFIER", 48 | "py": "FUNCTION_DECLARATION,PARAMETER##,ANNOTATION,EXPRESSION", 49 | }, 50 | "import": { 51 | "_": 0, # You probably want the first import of the current file 52 | "+": ["enter", None], 53 | "go": "FILE,IMPORT_LIST,IMPORT_SPEC", 54 | "java": "FILE,IMPORT_LIST,IMPORT_STATEMENT", 55 | "py": "FILE,IMPORT_STATEMENT", 56 | "php": "FILE,Use list", 57 | "default": "FILE,IMPORT_STATEMENT", 58 | }, 59 | "package": { 60 | "_": 0, # Only one package in a file 61 | "go": "FILE,PACKAGE_CLAUSE", 62 | }, 63 | "package name": { 64 | "_": 0, # Only one package in a file 65 | "go": "FILE,PACKAGE_CLAUSE,identifier", 66 | }, 67 | "comment": { 68 | "_": "next", # You probably want the next comment 69 | # "+": ["cmd-right space ", None], 70 | "php": "FILE,Comment", 71 | "default": "FILE,COMMENT", 72 | }, 73 | "method": { 74 | "_": "this", # You probably want the method containing the cursor 75 | "+": ["enter shift-tab", "method"], 76 | "go": "FILE,METHOD_DECLARATION|FUNCTION_DECLARATION", 77 | "java": "java.FILE,METHOD", 78 | "py": "FILE,Py:FUNCTION_DECLARATION", 79 | "cs": "File,DUMMY_TYPE_DECLARATION,DUMMY_BLOCK", 80 | "php": "FILE,Class method|function|Function", 81 | "default": "FILE,METHOD_DECLARATION", 82 | }, 83 | "method name": { 84 | "_": "this", # You probably want the method name of the current method 85 | "go": "FILE,METHOD_DECLARATION|FUNCTION_DECLARATION##,identifier", 86 | "py": "FILE,Py:FUNCTION_DECLARATION##,Py:IDENTIFIER", 87 | "java": "FILE,METHOD##,IDENTIFIER", 88 | "php": "FILE,Class method##,identifier", 89 | "default": "FILE,METHOD_DECLARATION##,identifier", 90 | }, 91 | "receiver": { 92 | "_": "this", # You probably want the receiver name of the current method 93 | "go": "FILE,METHOD_DECLARATION##,RECEIVER", 94 | }, 95 | "receiver name": { 96 | "_": "this", # You probably want the receiver name of the current method 97 | "go": "METHOD_DECLARATION##,RECEIVER,identifier", 98 | }, 99 | "receiver type": { 100 | "_": "this", # You probably want the receiver type of the current method 101 | "go": "FILE,METHOD_DECLARATION##,RECEIVER,TYPE", 102 | }, 103 | "results": { 104 | "_": "this", # You probably want the results of the current method 105 | "go": "FILE,METHOD_DECLARATION|FUNCTION_DECLARATION##,SIGNATURE,RESULT", 106 | }, 107 | "result": { 108 | "_": 0, # You probably want the results of the current method 109 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,SIGNATURE,RESULT,PARAMETER_DECLARATION", 110 | }, 111 | "result type": { 112 | "_": 0, # You probably want the results of the current method 113 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,SIGNATURE,RESULT,PARAMETER_DECLARATION##,TYPE", 114 | }, 115 | "result name": { 116 | "_": 0, # You probably want the result name of the current method 117 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,SIGNATURE,RESULT,PARAMETER_DECLARATION##,identifier", 118 | }, 119 | "function": { 120 | "_": "this", # You probably want the function containing the cursor 121 | "+": ["enter shift-tab", "function"], 122 | "go": "FILE,METHOD_DECLARATION|FUNCTION_DECLARATION", 123 | "py": "FILE,Py:FUNCTION_DECLARATION", 124 | "php": "FILE,Function", 125 | "default": "FILE,FUNCTION_DECLARATION", 126 | }, 127 | "function name": { 128 | "_": 0, # You probably want the function name of the current method 129 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,identifier", 130 | "py": "Py:FUNCTION_DECLARATION,Py:IDENTIFIER", 131 | "php": "Function,identifier", 132 | "default": "FUNCTION_DECLARATION,identifier", 133 | }, 134 | "class": { 135 | "_": "this", # You probably want the class containing the cursor 136 | "+": ["enter shift-tab", "class"], 137 | "py": "FILE,Py:CLASS_DECLARATION", 138 | "java": "FILE,CLASS", 139 | "php": "FILE,Class", 140 | "default": "FILE,CLASS_DECLARATION", 141 | }, 142 | "class name": { 143 | "_": 0, # You probably want the name of the current class 144 | "py": "Py:CLASS_DECLARATION,Py:IDENTIFIER", 145 | "java": "CLASS,IDENTIFIER", 146 | "php": "Class,identifier", 147 | "default": "CLASS_DECLARATION,identifier", 148 | }, 149 | "type": { 150 | "_": "this", # You probably want the type containing the cursor 151 | "+": ["\n", "type"], 152 | "go": "FILE,TYPE_DECLARATION", 153 | }, 154 | "type name": { 155 | "_": "this", # You probably want the name of the current type 156 | "go": "FILE,TYPE_DECLARATION##,identifier", 157 | }, 158 | "struct": { 159 | "_": "this", # You probably want the struct containing the cursor. 160 | "+": ["enter shift-tab", "struct"], 161 | "go": "FILE,STRUCT_TYPE", 162 | }, 163 | "if statement": { 164 | "_": "next", # You probably want the next if statement of the method 165 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,IF_STATEMENT", 166 | "java": "METHOD|FUNCTION,IF_STATEMENT", 167 | "py": "Py:FUNCTION_DECLARATION,Py:IF_STATEMENT", 168 | "php": "FILE,If", 169 | "default": "METHOD_DECLARATION|FUNCTION_DECLARATION,IF_STATEMENT", 170 | }, 171 | "while statement": { 172 | "_": "next", # You probably want the next if statement of the method 173 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,FOR_STATEMENT", 174 | "java": "METHOD|FUNCTION,WHILE_STATEMENT", 175 | "py": "Py:FUNCTION_DECLARATION,Py:WHILE_STATEMENT", 176 | "php": "FILE,While", 177 | "default": "METHOD_DECLARATION|FUNCTION_DECLARATION,WHILE_STATEMENT", 178 | }, 179 | "for statement": { 180 | "_": "next", # You probably want the next if statement of the method 181 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,FOR_STATEMENT", 182 | "java": "METHOD|FUNCTION,FOREACH_STATEMENT|FOR_STATEMENT", 183 | "py": "Py:FUNCTION_DECLARATION,Py:FOR_STATEMENT", 184 | "php": "FILE,For", 185 | "default": "METHOD_DECLARATION|FUNCTION_DECLARATION,FOR_STATEMENT", 186 | }, 187 | "statement": { 188 | "_": "this", # You probably want the current statement of the method 189 | "+": ["enter", None], 190 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,STATEMENT", 191 | "java": "METHOD|FUNCTION,STATEMENT", 192 | "py": "Py:FUNCTION_DECLARATION|FILE,STATEMENT", 193 | "cs": "File,DUMMY_BLOCK,DUMMY_NODE", 194 | "php": "FILE,Statement", 195 | "default": "METHOD_DECLARATION|FUNCTION_DECLARATION|FILE,STATEMENT", 196 | }, 197 | "literal": { 198 | "_": "next", # You probably want the next literal 199 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,LITERAL_VALUE", 200 | }, 201 | "function literal": { 202 | "_": "this", # You probably want the current composite literal 203 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,FUNCTION_LIT", 204 | }, 205 | "complex literal": { 206 | "_": "this", # You probably want the current composite literal 207 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,COMPOSITE_LIT", 208 | }, 209 | "complex literal type": { 210 | "_": "this", # You probably want the current composite literal 211 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,COMPOSITE_LIT,TYPE", 212 | }, 213 | "complex literal value": { 214 | "_": "this", # You probably want the current composite literal 215 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,COMPOSITE_LIT,LITERAL_VALUE", 216 | }, 217 | "field": { 218 | "_": "this", 219 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,ELEMENT", 220 | }, 221 | "field value": { 222 | "_": "this", 223 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,ELEMENT##,VALUE", 224 | }, 225 | "field name": { 226 | "_": "this", 227 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,ELEMENT##,KEY", 228 | }, 229 | "argument": { 230 | "_": 0, # You probably want the first / ordinal argument of the current statement 231 | "+": [", space", None], 232 | "go": "STATEMENT,ARGUMENT_LIST,_EXPR|LITERAL|_EXPRESSION", 233 | "java": "STATEMENT,EXPRESSION_LIST,_EXPRESSION", 234 | "py": "STATEMENT,ARGUMENT_LIST,CALL_EXPRESSION,ARGUMENT_LIST,EXPRESSION", 235 | "php": "Statement,Parameter list,Parameter", 236 | "default": "STATEMENT,ARGUMENT_LIST,EXPRESSION", 237 | }, 238 | "return": { 239 | "_": "next", # You probably want the next return statement of the method 240 | "+": ["enter r e t u r n space", None], 241 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,RETURN_STATEMENT", 242 | "java": "METHOD|FUNCTION,RETURN_STATEMENT", 243 | "py": "Py:FUNCTION_DECLARATION,Py:RETURN_STATEMENT", 244 | "php": "FILE,Return", 245 | "default": "METHOD_DECLARATION|FUNCTION_DECLARATION,RETURN_STATEMENT", 246 | }, 247 | "new return": { 248 | "_": -1, # You probably want to add a return statement 249 | "+": ["enter r e t u r n space", None], 250 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,STATEMENT", 251 | }, 252 | "defer": { 253 | "_": "next", # You probably want the next return statement of the method 254 | "+": ["enter d e f e r space", None], 255 | "go": "METHOD_DECLARATION|FUNCTION_DECLARATION,DEFER_STATEMENT", 256 | }, 257 | # "value": { 258 | # "_": 0, # First value of the literal? 259 | # "go": "LITERAL_VALUE,ELEMENT", 260 | # "py": "Py:DICT_LITERAL_EXPRESSION,Py:KEY_VALUE_EXPRESSION", 261 | # "default": "LITERAL_VALUE,ELEMENT", 262 | # }, 263 | "left hand": { 264 | "_": "next", # LHS of the current statement. Ordinals make sense for multi-assign. 265 | "go": "STATEMENT,LEFT_HAND_EXPR_LIST|VAR_DEFINITION", 266 | "py": "Py:FUNCTION_DECLARATION,Py:ASSIGNMENT_STATEMENT##,Py:TARGET_EXPRESSION", 267 | }, 268 | "right hand": { 269 | "_": "next", # RHS of the current statement. Ordinals make sense for multi-assign. 270 | "py": "Py:FUNCTION_DECLARATION,Py:ASSIGNMENT_STATEMENT##,EXPRESSION%231", 271 | }, 272 | "key value": { 273 | "_": "this", 274 | "+": [", enter : left", None], 275 | "py": "FILE,Py:DICT_LITERAL_EXPRESSION,Py:KEY_VALUE_EXPRESSION", 276 | }, 277 | # XXX RHS expression 278 | # "index": { 279 | # "go": "sibling INDEX_OR_SLICE_EXPR", 280 | # "py": "sibling Py:SUBSCRIPTION_EXPRESSION", 281 | # } 282 | } -------------------------------------------------------------------------------- /apps/jira.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from talon.voice import Context, ContextGroup, Key 4 | 5 | from .tridactyl import tKey 6 | from ..utils import text, delay 7 | 8 | # I don't want to interleave jira navigation with other commands, like dictation. 9 | group = ContextGroup("jira") 10 | 11 | browsers = {"Google Chrome", "Firefox", "Safari"} 12 | 13 | 14 | def isJira(app, win): 15 | return app.name in browsers and " JIRA2" in win.title 16 | 17 | 18 | ctx = Context("jira", func=isJira, group=group) 19 | ctx.vocab = ["sub-task", "Dwight"] 20 | ctx.keymap( 21 | { 22 | "go dashboard": tKey("g d"), 23 | "go boards": tKey("g a"), 24 | "go issues": tKey("g i"), 25 | "search": tKey("/"), 26 | "go create": tKey("c"), 27 | "assign [to] [over]": [tKey("a"), delay(0.6), text], 28 | "assign to me": tKey("i"), 29 | "comment": tKey("m"), 30 | "edit": tKey("e"), 31 | "(action | please) [] [over]": [tKey("."), delay(0.6), text], 32 | "submit": tKey("ctrl+return"), 33 | "copy link": tKey("cmd+l cmd+c"), 34 | "copy id": tKey("cmd+l right alt+shift+left alt+shift+left cmd+c"), 35 | } 36 | ) 37 | group.load() 38 | -------------------------------------------------------------------------------- /apps/one_password.py: -------------------------------------------------------------------------------- 1 | from talon import ctrl 2 | from talon.voice import Context, Key 3 | 4 | from ..utils import text, delay 5 | 6 | ctx = Context("1password") 7 | ctx.keymap({ 8 | "password [] [over]": [Key("shift-cmd-\\"), delay(0.2), text], 9 | }) -------------------------------------------------------------------------------- /apps/onenote.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context, Key 2 | 3 | from .. import utils 4 | 5 | ctx = Context("onenote", bundle="com.microsoft.onenote.mac") 6 | ctx.keymap( 7 | { 8 | "sync notebook": Key("cmd+s"), 9 | "sync all notebooks": Key("shift+cmd+s"), 10 | "new page": Key("cmd+n"), 11 | "new section": Key("cmd+t"), 12 | "move page": Key("shift+cmd+m"), 13 | "copy page": Key("shift+cmd+c"), 14 | "make sub page": Key("alt+cmd+]"), 15 | "promote sub page": Key("alt+cmd+["), 16 | "go back": Key("ctrl+cmd+left"), 17 | "go forward": Key("ctrl+cmd+right"), 18 | "toggle ribbon": Key("alt+cmd+r"), 19 | "focus notifications": Key("alt+cmd+o"), 20 | # Zoom 21 | "zoom out": Key("cmd+="), 22 | "zoom in": Key("cmd+-"), 23 | "zoom clear": Key("cmd+0"), 24 | # Search 25 | "search [this]": Key("cmd+f"), 26 | "search for [over]": [Key("cmd+f"), utils.delay(0.3), utils.text], 27 | "search all": Key("alt+cmd+f"), 28 | "search all for [over]": [Key("alt+cmd+f"), utils.delay(0.3), utils.text], 29 | "go next result": Key("cmd+g"), 30 | "go last result": Key("shift+cmd+g"), 31 | # The universal command 32 | "please [over]": [Key("shift+cmd+/"), utils.delay(0.3), utils.text], 33 | # TODO WYSIWYG commands 34 | 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /apps/outlook.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context, Key 2 | 3 | ctx = Context("outlook", bundle="com.microsoft.Outlook") 4 | ctx.keymap({"archive": Key("ctrl+e")}) 5 | -------------------------------------------------------------------------------- /apps/selection.zsh: -------------------------------------------------------------------------------- 1 | # http://stackoverflow.com/questions/5407916/zsh-zle-shift-selection 2 | # https://superuser.com/questions/604812/how-to-execute-a-function-in-bash-or-zsh-on-every-letter-being-typed-into-prompt 3 | 4 | # Modified to match OS X selection behavior. 5 | 6 | self-insert() { 7 | if ((REGION_ACTIVE)) then 8 | zle kill-region 9 | fi 10 | zle .self-insert 11 | } 12 | zle -N self-insert 13 | 14 | r-delregion() { 15 | if ((REGION_ACTIVE)) then 16 | if [[ $MARK -lt $CURSOR ]] 17 | then 18 | zle exchange-point-and-mark 19 | fi 20 | zle kill-region 21 | else 22 | zle $1 23 | fi 24 | } 25 | 26 | # There are left/right variants of deselect and select because OS X selections are weird. 27 | # With a selection, shift-arrows snaps the selection to the cursor, but shift-home/end will extend the selection. 28 | # So I need to manipulate the mark and cursor to emulate those behaviors, but differently depending on which keys are pressed. 29 | r-deselect() { 30 | ((REGION_ACTIVE = 0)) 31 | zle $* 32 | } 33 | 34 | r-deselect-left() { 35 | if ((REGION_ACTIVE)) then 36 | if [[ $MARK -lt $CURSOR ]] 37 | then 38 | zle exchange-point-and-mark 39 | fi 40 | ((REGION_ACTIVE = 0)) 41 | zle forward-char 42 | fi 43 | zle $* 44 | } 45 | 46 | r-deselect-right() { 47 | if ((REGION_ACTIVE)) then 48 | if [[ $MARK -gt $CURSOR ]] 49 | then 50 | zle exchange-point-and-mark 51 | fi 52 | ((REGION_ACTIVE = 0)) 53 | zle backward-char 54 | fi 55 | zle $* 56 | } 57 | 58 | r-select() { 59 | ((REGION_ACTIVE)) || zle set-mark-command 60 | zle $* 61 | } 62 | 63 | r-select-left() { 64 | if ((REGION_ACTIVE)) then 65 | if [[ $MARK -lt $CURSOR ]] 66 | then 67 | zle exchange-point-and-mark 68 | fi 69 | else 70 | zle set-mark-command 71 | fi 72 | zle $* 73 | } 74 | 75 | r-select-right() { 76 | if ((REGION_ACTIVE)) then 77 | if [[ $MARK -gt $CURSOR ]] 78 | then 79 | zle exchange-point-and-mark 80 | fi 81 | else 82 | zle set-mark-command 83 | fi 84 | zle $* 85 | } 86 | 87 | r-copy() { 88 | if ((REGION_ACTIVE)) then 89 | zle copy-region-as-kill 90 | echo -n "${CUTBUFFER}" | pbcopy 91 | zle $* 92 | # else 93 | # echo -n '' | pbcopy 94 | fi 95 | } 96 | 97 | for key kcap seq mode widget ( 98 | sleft kLFT $'\e[1;2D' select backward-char 99 | sright kRIT $'\e[1;2C' select forward-char 100 | sup kri $'\e[1;2A' select-left 'beginning-of-line exchange-point-and-mark' 101 | sdown kind $'\e[1;2B' select-right 'end-of-line exchange-point-and-mark' 102 | send kEND $'\E[1;2F' select-right end-of-line 103 | send2 x $'\E[4;2~' select-right end-of-line 104 | shome kHOM $'\E[1;2H' select-left beginning-of-line 105 | shome2 x $'\E[1;2~' select-left beginning-of-line 106 | left kcub1 $'\EOD' deselect-left backward-char 107 | right kcuf1 $'\EOC' deselect-right forward-char 108 | left2 x $'\E[D' deselect-left backward-char 109 | right2 x $'\E[C' deselect-right forward-char 110 | end kend $'\EOF' deselect-right end-of-line 111 | end2 x $'\E4~' deselect-right end-of-line 112 | end3 x $'^e' deselect-right end-of-line 113 | home khome $'\EOH' deselect-left beginning-of-line 114 | home2 x $'\E1~' deselect-left beginning-of-line 115 | home3 x $'^a' deselect-left beginning-of-line 116 | csleft x $'\E[1;10D' select backward-word 117 | csright x $'\E[1;10C' select forward-word 118 | csend x $'\E[1;10F' select end-of-line 119 | cshome x $'\E[1;10H' select beginning-of-line 120 | cleft x $'\E[1;5D' deselect-left backward-word 121 | cleft2 x $'\Eb' deselect-left backward-word 122 | cright x $'\E[1;5C' deselect-right forward-word 123 | cright2 x $'\Ef' deselect-right forward-word 124 | del kdch1 $'\E[3~' delregion delete-char 125 | bs x $'^?' delregion backward-delete-char 126 | copy x $'\Ew' copy copy-region-as-kill 127 | cut x $'^w' copy kill-region 128 | yank x $'^y' deselect yank 129 | ) { 130 | eval "key-$key() r-$mode $widget" 131 | zle -N key-$key 132 | bindkey ${terminfo[$kcap]-$seq} key-$key 133 | } 134 | 135 | zle_highlight=(region:bg=blue special:standout 136 | suffix:bold isearch:underline paste:bold) 137 | 138 | if [[ ${+ZSH_AUTOSUGGEST_ACCEPT_WIDGETS} == 1 ]] 139 | then 140 | ZSH_AUTOSUGGEST_ACCEPT_WIDGETS+=(key-right key-right2 key-end key-end2 key-end3) 141 | ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS+=(key-cright key-cright2) 142 | fi -------------------------------------------------------------------------------- /apps/slack.py: -------------------------------------------------------------------------------- 1 | import random 2 | import subprocess 3 | import time 4 | 5 | from talon import ui 6 | from talon.voice import Context, Key, Str 7 | 8 | from ..utils import text, join_words, parse_words, insert, delay 9 | 10 | alpha_to_emoji = { 11 | " ": [":white_small_square:"], 12 | "'": [":droplet:"], 13 | "a": [":amazon:", ":braves:", ":laa:", ":arch:", ":a:"], 14 | "b": [":bonusly:", ":boston:", ":bucknell:", ":b:", ":bitcoin:", ":bootstrap:"], 15 | "c": [":canvas:", ":costco:", ":copyright:"], 16 | "d": [":duo:", ":leftwards_arrow_with_hook:"], 17 | "e": [":espresa:", ":ielogo:", ":e-mail:"], 18 | "f": [":falcons:", ":payrespects:", ":flutter:"], 19 | "g": [":googleicon:", ":georgia_bulldogs:", ":grafana:", ":google:"], 20 | "h": [":hotel:", ":sa:", ":pisces:"], 21 | "i": [":illini:", ":information_source:"], 22 | "j": [":arrow_heading_up:"], 23 | "k": [":kotlin:"], 24 | "l": [":l:", ":muscle:", ":lambda:"], 25 | "m": [":michiganblock:", ":scorpius:", ":mcdonalds:", ":mini:"], 26 | "n": [":nagios:", ":nespresso:", ":nu:", ":pr:"], 27 | "o": [":okta:", ":goducks:", ":outlook:", ":portal1:", ":o:", ":o2:"], 28 | "p": [":paypal:", ":philly:", ":ptown:", ":badparking:"], 29 | "q": [":q:", ":clock430:"], 30 | "r": [":revolut:", ":registered:", ":rust:"], 31 | "s": [":skype:", ":trogdor-stomp:", ":heavy_dollar_sign:", ":sooperheroes:"], 32 | "t": [":text:", ":tesla:", ":text:", ":terraform:"], 33 | "u": [":jedi:", ":ophiuchus:", ":utah_state_aggies:"], 34 | "v": [":venmo:", ":aries:"], 35 | "w": [ 36 | ":wday:", 37 | ":googlewallet:", 38 | ":flythew:", 39 | ":wow:", 40 | ":wutang:", 41 | ":wu-tang:", 42 | ":wumbo:", 43 | ], 44 | "x": [":x:", ":heavy_multiplication_x:"], 45 | "y": [":valor:", ":yeet:", ":funnel:", ":byu:"], 46 | "z": [":zap:", ":z:"], 47 | } 48 | 49 | 50 | def emoji_formatter(m): 51 | string = join_words(parse_words(m)) 52 | insert( 53 | "".join([random.choice(alpha_to_emoji.get(c.lower(), [""])) for c in string]) 54 | ) 55 | 56 | 57 | def emoji_reaction_formatter(m): 58 | string = join_words(parse_words(m)) 59 | translated = [random.choice(alpha_to_emoji.get(c.lower(), [""])) for c in string] 60 | for t in translated: 61 | Str(f"+{t}\n")(m) 62 | time.sleep(0.7) 63 | 64 | 65 | ctx = Context("slack", bundle="com.tinyspeck.slackmacgap") 66 | ctx.keymap( 67 | { 68 | "focus next": [Key("f6")], 69 | "jump []": [Key("cmd-k"), text], 70 | "go (dm's | direct messages | messages)": Key("cmd-shift-k"), 71 | "go (and read | unread)": Key("cmd-shift-a"), 72 | "go (threads | thread)": Key("cmd-shift-t"), 73 | "go activity": Key("cmd-shift-m"), 74 | "go channel info": Key("cmd-shift-i"), 75 | "go status": Key("cmd-shift-y"), 76 | "go (star | stars | starred)": Key("cmd-shift-s"), 77 | "react []": ["+:", text], 78 | "emote []": [":", text], 79 | "toggle sidebar": Key("cmd-."), 80 | "go last unread": Key("alt-shift-up"), 81 | "go next unread": Key("alt-shift-down"), 82 | "annoying [over]": emoji_formatter, 83 | "annoying reaction [over]": emoji_reaction_formatter, 84 | } 85 | ) 86 | 87 | 88 | def toggle_slack_dnd(amount=""): 89 | def _toggle_slack_dnd(m): 90 | window = ui.active_window() 91 | # Open a slack window to yourself in a browser, scrape these values out of it. 92 | subprocess.call(["open", "slack://channel?id=D865P648K&team=T03R7LB3M"]) 93 | time.sleep(1) 94 | Str(f"/dnd {amount}\n")(None) 95 | Key("cmd-[")(None) 96 | window.focus() 97 | 98 | return _toggle_slack_dnd 99 | 100 | 101 | gctx = Context("slackglobal") 102 | gctx.keymap( 103 | { 104 | "slack do not disturb": toggle_slack_dnd("until tomorrow morning"), 105 | "slack do not disturb off": toggle_slack_dnd("off"), 106 | "slack do not disturb thirty": toggle_slack_dnd("30m"), 107 | } 108 | ) 109 | -------------------------------------------------------------------------------- /apps/spotify.py: -------------------------------------------------------------------------------- 1 | from talon import applescript 2 | from talon.voice import Context, Key 3 | 4 | 5 | def spotify(thing): 6 | def _spotify(*_): 7 | applescript.run(f'tell application "Spotify" to {thing}') 8 | 9 | return _spotify 10 | 11 | 12 | ctx = Context("spotify") 13 | ctx.keymap( 14 | { 15 | "pause song": spotify("pause"), 16 | "play song": spotify("play"), 17 | "next song": spotify("next track"), 18 | "last song": spotify("previous track"), 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /apps/ticktick.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context, Key 2 | 3 | from .. import utils 4 | 5 | globalctx = Context("ticktickGlobal") 6 | globalctx.keymap( 7 | { 8 | # Rebound, conflicted with Jetbrains action search 9 | "quick task [] [over]": [ 10 | Key("shift-alt-cmd-ctrl-a"), 11 | utils.delay(0.2), 12 | utils.text, 13 | ], 14 | # Rebound, didn't trust it 15 | "toggle tick mini": [Key("shift-alt-cmd-ctrl-o")], 16 | # Rebound, didn't trust it 17 | "toggle tick main": [Key("shift-alt-cmd-ctrl-e")], 18 | # Rebound, didn't trust it 19 | "toggle tick pomo": [Key("shift-alt-cmd-ctrl-p")], 20 | } 21 | ) 22 | 23 | ctx = Context("ticktick", bundle="com.TickTick.task.mac") 24 | ctx.keymap( 25 | { 26 | "sync task": Key("cmd-s"), 27 | "search task": Key("cmd-f"), 28 | "search task [] [over]": [Key("cmd-f"), utils.delay(0.3), utils.text], 29 | "add task": Key("cmd-n"), 30 | "add task [] [over]": [Key("cmd-n"), utils.delay(0.3), utils.text], 31 | "complete task": Key("shift-cmd-m"), 32 | "clear date": Key("cmd-0"), 33 | "set today": Key("cmd-1"), 34 | "set tomorrow": Key("cmd-2"), 35 | "set next week": Key("cmd-3"), 36 | 37 | # Rebound, existing was ctrl only, conflict with spaces 38 | "set no priority": Key("cmd-ctrl-0"), 39 | "set low priority": Key("cmd-ctrl-1"), 40 | "set medium priority": Key("cmd-ctrl-2"), 41 | "set high priority": Key("cmd-ctrl-3"), 42 | # Conflict with Alfred clipboard 43 | "go calendar": Key("shift-ctrl-cmd-c"), 44 | 45 | # Back to defaults 46 | "go today": Key("alt-cmd-t"), 47 | "go tomorrow": Key("ctrl-cmd-t"), 48 | "go next seven days": Key("alt-cmd-n"), 49 | "go all": Key("alt-cmd-a"), 50 | "go assigned": Key("ctrl-cmd-a"), 51 | "go complete": Key("alt-cmd-c"), 52 | } 53 | ) 54 | -------------------------------------------------------------------------------- /apps/tridactyl.py: -------------------------------------------------------------------------------- 1 | from talon.ui import active_app 2 | from talon.voice import Context, Key 3 | 4 | from ..utils import delay, text 5 | enabled = True 6 | 7 | 8 | def is_tridactyl(app, _): 9 | global enabled 10 | return enabled and app.name == "Firefox" 11 | 12 | 13 | # noinspection PyPep8Naming 14 | def tKey(key): 15 | """Key(), but for conflicts with Tridactyl commands""" 16 | global enabled 17 | 18 | def tKeyM(m): 19 | if is_tridactyl(active_app(), None): 20 | return Key("shift-escape " + key + " shift-escape")(m) 21 | else: 22 | return Key(key)(m) 23 | 24 | return tKeyM 25 | 26 | 27 | ctx = Context("tridactyl", func=is_tridactyl) 28 | ctx.keymap( 29 | { 30 | "(follow | go link)": "f", 31 | "go background": "F", 32 | "go back": "H", 33 | "go forward": "L", 34 | "clear tab": "d", 35 | "restore tab": "u", 36 | "jump [] [over]": ["B", delay(0.1), text], # Alltabs is more useful 37 | "open here [] [over]": ["o", delay(0.1), text], 38 | "open tab [] [over]": ["t", delay(0.1), text], 39 | "search [] [over]": ["s", delay(0.1), text], 40 | "tab search [] [over]": ["S", delay(0.1), text], 41 | "go next tab": "gt", 42 | "go last tab": "gT", 43 | "search": "/", 44 | "toggle mark": "A", 45 | "toggle ignore": Key("shift-escape"), 46 | "toggle reader": "gr", 47 | "copy URL": "yy", 48 | "copy location": "yy", 49 | "go next page": "]]", 50 | "go last page": "[[", 51 | "zoom out": "zo", 52 | "zoom in": "zi", 53 | "zoom clear": "zz", 54 | "go edit": Key("ctrl-i"), 55 | "detach tab": [":", delay(0.1), "tabdetach", delay(0.1), "\n"], 56 | } 57 | ) 58 | -------------------------------------------------------------------------------- /apps/vscode/line.js: -------------------------------------------------------------------------------- 1 | exports.GET = function(args) { 2 | // access VS Code API (s. https://code.visualstudio.com/Docs/extensionAPI/vscode-api) 3 | var vscode = require('vscode'); 4 | // current editor 5 | const editor = vscode.window.activeTextEditor; 6 | args.response.data = { 7 | "selection": editor.selection, 8 | "path": args.path, 9 | } 10 | args.statusCode = 200; 11 | } 12 | 13 | exports.POST = async function(args) { 14 | var vscode = require('vscode'); 15 | // current editor 16 | const editor = vscode.window.activeTextEditor; 17 | var body = await args.getJSON(); 18 | p1l = body["start"]["line"]; 19 | p1c = body["start"]["character"]; 20 | p2l = body["end"]["line"]; 21 | p2c = body["end"]["character"]; 22 | const start = new vscode.Position(p1l, p1c); 23 | const end = new vscode.Position(p2l, p2c); 24 | editor.selection = new vscode.Selection(start, end); 25 | editor.revealRange(new vscode.Range(start, end), 1); 26 | } -------------------------------------------------------------------------------- /apps/vscode/readme.md: -------------------------------------------------------------------------------- 1 | # VS Code via HTTP Rest API 2 | ## Install the VS Code plugin and Talon files: 3 | - Install https://marketplace.visualstudio.com/items?itemName=mkloubert.vs-rest-api 4 | - Copy this directory to your ~/.talon/user directory. 5 | ## Configure the HTTP endpoint in your Code user settings 6 | Note that you *must* replace two bits in the following config: 7 | 8 | "rest.api": { 9 | "autoStart": true, 10 | "openInBrowser": false, 11 | "port": 1781, 12 | "guest": false, 13 | "users": [ 14 | { 15 | "name": "talon", 16 | "password": "", 17 | "canExecute": true 18 | } 19 | ], 20 | "endpoints": { 21 | "talonLine": { 22 | "script": "/Users//.talon/user/apps/vscode/line.js" 23 | }, 24 | "talonSearch": { 25 | "script": "/Users//.talon/user/apps/vscode/search.js" 26 | }, 27 | "talonTerminal": { 28 | "script": "/Users//.talon/user/apps/vscode/terminal.js" 29 | } 30 | } 31 | } 32 | 33 | ### Most settings can be changed, but the user *must* be named "talon" and the endpoint *must* be named "talonLine". 34 | 35 | ## Restart VS Code. -------------------------------------------------------------------------------- /apps/vscode/search.js: -------------------------------------------------------------------------------- 1 | exports.POST = async function(args) { 2 | var vscode = require('vscode'); 3 | 4 | // current editor 5 | const editor = vscode.window.activeTextEditor; 6 | const position = editor.selection.active; 7 | var body = await args.getJSON(); 8 | direction = body["direction"]; 9 | pattern = body["string"]; 10 | const start = new vscode.Position(position.line, position.character); 11 | const startOffset = editor.document.offsetAt(start); 12 | const text = editor.document.getText(); 13 | var textToSearch; 14 | var index; 15 | var newIndex; 16 | if (direction == "forward") { 17 | textToSearch = text.substring(startOffset+1, text.length).toLowerCase(); 18 | index = textToSearch.indexOf(pattern.toLowerCase()); 19 | newIndex = startOffset + index + 1; 20 | } else { 21 | textToSearch = text.substring(0, startOffset-1).toLowerCase(); 22 | index = textToSearch.lastIndexOf(pattern.toLowerCase()); 23 | newIndex = index; 24 | } 25 | // vscode.window.showInformationMessage('('+ newIndex + ", " + pattern.length + ") " + pattern); 26 | const beginning = editor.document.positionAt(newIndex); 27 | const end = editor.document.positionAt(newIndex+pattern.length); 28 | editor.selection = new vscode.Selection(beginning, end); 29 | editor.revealRange(new vscode.Range(beginning, end), 1); 30 | } -------------------------------------------------------------------------------- /apps/vscode/terminal.js: -------------------------------------------------------------------------------- 1 | exports.GET = function(args) { 2 | // access VS Code API (s. https://code.visualstudio.com/Docs/extensionAPI/vscode-api) 3 | var vscode = require('vscode'); 4 | this._terminalPid 5 | // current editor 6 | const editor = vscode.window.activeTextEditor; 7 | if (vscode.window.activeTerminal !== undefined) { 8 | vscode.window.activeTerminal.processId.then((number) => this._terminalPid = number); 9 | } 10 | args.response.data = { 11 | "activeTerminal": vscode.window.activeTerminal, 12 | "activeTerminalPid": this._terminalPid, 13 | "activeEditorSelection": (editor != undefined)? editor.selection : undefined, 14 | } 15 | args.statusCode = 200; 16 | } -------------------------------------------------------------------------------- /apps/vscode/vscode.py_: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import threading 4 | import time 5 | 6 | import requests 7 | from talon import ctrl 8 | from talon.voice import Context 9 | 10 | from ...utils import optional_numerals, text, text_to_number, text_to_range, parse_words 11 | 12 | try: 13 | from ...misc.mouse import delayed_click 14 | except ImportError: 15 | print("Fallback mouse click logic") 16 | 17 | def delayed_click(): 18 | ctrl.mouse_click(button=0) 19 | 20 | 21 | def delay(amount=0.1): 22 | return lambda _: time.sleep(amount) 23 | 24 | 25 | def get_rest_api_settings(): 26 | vs_code_settings = None 27 | with open( 28 | os.path.expanduser("~/Library/Application Support/Code/User/settings.json") 29 | ) as fh: 30 | contents = fh.read() 31 | try: 32 | vs_code_settings = json.loads(contents) 33 | except json.JSONDecodeError: 34 | pass 35 | if vs_code_settings is None: 36 | print("Could not load VS Code Settings") 37 | return None, None, None 38 | if "rest.api" not in vs_code_settings: 39 | print("Rest API not configured.") 40 | return None, None, None 41 | rest_api_settings = vs_code_settings["rest.api"] 42 | port = rest_api_settings.get("port", None) 43 | talon_users = [ 44 | u for u in rest_api_settings.get("users", {}) if u["name"] == "talon" 45 | ] 46 | if len(talon_users) != 1: 47 | print( 48 | "Rest API needs a single user named 'talon' with write access. See docs." 49 | ) 50 | return None, None, None 51 | username = talon_users[0]["name"] 52 | password = talon_users[0]["password"] 53 | return port, username, password 54 | 55 | 56 | def send_api_command(*commands): 57 | def _send(): 58 | port, username, password = get_rest_api_settings() 59 | if port: 60 | for cmds in commands: 61 | for cmd in cmds: 62 | print("Sending {}".format(cmd)) 63 | response = requests.post( 64 | "http://localhost:{}/api/commands/{}".format(port, cmd), 65 | timeout=(0.05, 3.05), 66 | auth=(username, password), 67 | ) 68 | response.raise_for_status() 69 | else: 70 | print("Rest API does not have 'port' setting.") 71 | 72 | threading.Thread(target=_send).start() 73 | 74 | 75 | def go_to_line(drop=1): 76 | def handler(m): 77 | # noinspection PyProtectedMember 78 | line = text_to_number(m._words[drop:]) 79 | if int(line) == 0: 80 | print("Not sending, arg was 0") 81 | return 82 | port, username, password = get_rest_api_settings() 83 | if port: 84 | pos = {"line": line - 1, "character": 0} 85 | selection = {"start": pos, "end": pos} 86 | response = requests.post( 87 | "http://localhost:{}/api/talonLine".format(port), 88 | timeout=(0.05, 3.05), 89 | auth=(username, password), 90 | json=selection, 91 | ) 92 | response.raise_for_status() 93 | return response.text 94 | else: 95 | print("Rest API does not have 'port' setting.") 96 | 97 | return handler 98 | 99 | 100 | def select_lines(drop=1): 101 | def handler(m): 102 | # noinspection PyProtectedMember 103 | start, end = text_to_range(m._words[drop:]) 104 | port, username, password = get_rest_api_settings() 105 | if port: 106 | start_pos = {"line": start - 1, "character": 0} 107 | end_pos = {"line": end - 1, "character": 9999} 108 | selection = {"start": start_pos, "end": end_pos} 109 | response = requests.post( 110 | "http://localhost:{}/api/talonLine".format(port), 111 | timeout=(0.05, 3.05), 112 | auth=(username, password), 113 | json=selection, 114 | ) 115 | response.raise_for_status() 116 | return response.text 117 | else: 118 | print("Rest API does not have 'port' setting.") 119 | 120 | return handler 121 | 122 | 123 | def vscode_command(*cmds): 124 | return lambda _: send_api_command(cmds) 125 | 126 | 127 | def vscode_search(direction): 128 | def handler(m): 129 | # noinspection PyProtectedMember 130 | pattern = " ".join(parse_words(m)) 131 | port, username, password = get_rest_api_settings() 132 | if port: 133 | response = requests.post( 134 | "http://localhost:{}/api/talonSearch".format(port), 135 | timeout=(0.05, 3.05), 136 | auth=(username, password), 137 | json={"direction": direction, "string": pattern}, 138 | ) 139 | response.raise_for_status() 140 | return response.text 141 | else: 142 | print("Rest API does not have 'port' setting.") 143 | 144 | return handler 145 | 146 | 147 | # group = ContextGroup("vscode") 148 | ctx = Context("vscode", bundle="com.microsoft.VSCode") # , group=group) 149 | ctx.keymap( 150 | { 151 | "complete": vscode_command("editor.action.triggerSuggest"), 152 | # "smarter": vscode_command("action"), 153 | # "finish": vscode_command("action"), 154 | "zoom": vscode_command("workbench.action.maximizeEditor"), 155 | "find (usage | usages)": vscode_command( 156 | "editor.action.referenceSearch.trigger" 157 | ), 158 | "(refactor | reflector) []": [ 159 | vscode_command("editor.action.refactor"), 160 | text, 161 | ], 162 | "fix [this]": vscode_command("editor.action.quickFix"), 163 | "visit declaration": vscode_command("editor.action.goToDeclaration"), 164 | "visit (implementers | implementations)": vscode_command( 165 | "editor.action.goToImplementation" 166 | ), 167 | "visit type": vscode_command("editor.action.goToTypeDefinition"), 168 | "select last ++": vscode_search("backwards"), 169 | "select next ++": vscode_search("forwards"), 170 | # "search everywhere [for] []": [ 171 | # vscode_command("action"), 172 | # text, 173 | # ], 174 | "find []": [vscode_command("actions.find"), delay(), text], 175 | "find this": vscode_command("actions.findWithSelection"), 176 | "template []": [ 177 | vscode_command("editor.action.insertSnippet"), 178 | delay(), 179 | text, 180 | ], 181 | "select less": vscode_command("editor.action.smartSelect.shrink"), 182 | "select more": vscode_command("editor.action.smartSelect.grow"), 183 | f"select line {optional_numerals}": [ 184 | go_to_line(drop=2), 185 | vscode_command("cursorHome", "cursorEndSelect"), 186 | ], 187 | "select this line": [vscode_command("cursorHome", "cursorEndSelect")], 188 | f"select (lines | line) {optional_numerals} until {optional_numerals}": select_lines( 189 | drop=2 190 | ), 191 | "(clean | clear) line": [vscode_command("cursorLineStart", "deleteAllRight")], 192 | "(delete | remove) line": vscode_command("editor.action.deleteLines"), 193 | "(delete | clear) to end": vscode_command("deleteAllRight"), 194 | "(delete | clear) to start": vscode_command("deleteAllLeft"), 195 | "drag up": vscode_command("editor.action.moveLinesUpAction"), 196 | "drag down": vscode_command("editor.action.moveLinesDownAction"), 197 | "duplicate": vscode_command("editor.action.copyLinesDownAction"), 198 | "(go | jump) back": vscode_command("workbench.action.navigateBack"), 199 | "(go | jump) forward": vscode_command("workbench.action.navigateForward"), 200 | "comment": vscode_command("editor.action.commentLine"), 201 | "(action | please) []": [ 202 | vscode_command("workbench.action.showCommands"), 203 | delay(), 204 | text, 205 | ], 206 | f"(go to | jump to) {optional_numerals}": [ 207 | go_to_line(drop=2), 208 | vscode_command("cursorHome"), 209 | ], 210 | f"(go | jump) to end of {optional_numerals}": [ 211 | go_to_line(drop=4), 212 | vscode_command("cursorHome"), 213 | ], 214 | } 215 | ) 216 | # group.load() 217 | -------------------------------------------------------------------------------- /apps/vscode/vscode_terminal.py_: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | 4 | import requests 5 | from talon import cron 6 | from talon.ui import active_app 7 | from talon.voice import Context, ContextGroup, Key, Str, talon 8 | 9 | from .vscode import get_rest_api_settings 10 | 11 | terminals = ("com.apple.Terminal", "com.googlecode.iterm2") 12 | 13 | 14 | ######################################################################## 15 | # global settings 16 | ######################################################################## 17 | ENABLED = False 18 | ######################################################################## 19 | 20 | 21 | def terminal_context(app, _): 22 | global _terminalContextJob 23 | global _lastEditorSelection 24 | if app.bundle in terminals: 25 | _lastEditorSelection = "" 26 | return True 27 | elif app.bundle == "com.microsoft.VSCode": 28 | return is_vscode_terminal() 29 | return False 30 | 31 | 32 | _lastEditorSelection = "" 33 | _lastTerminalLsof = None 34 | _inTerminal = False 35 | 36 | 37 | def is_vscode_terminal(): 38 | global _lastEditorSelection 39 | global _lastTerminalLsof 40 | global _inTerminal 41 | if not ENABLED: 42 | return False 43 | port, username, password = get_rest_api_settings() 44 | if port: 45 | response = requests.get( 46 | "http://localhost:{}/api/talonTerminal".format(port), 47 | timeout=(0.05, 3.05), 48 | auth=(username, password), 49 | ) 50 | response.raise_for_status() 51 | data = response.json() 52 | # print(data) 53 | editor_selection = json.dumps( 54 | data["data"].get("activeEditorSelection", None), sort_keys=True 55 | ) 56 | if _lastEditorSelection == editor_selection: 57 | if "activeTerminal" in data["data"]: 58 | if not _inTerminal: 59 | # If we think we might be in a terminal, we know activity happened there if our tty's offsets 60 | # have changed. lsof is an easy way to check that. 61 | if data["data"].get("activeTerminalPid", 0): 62 | lsof = subprocess.check_output( 63 | ["lsof", "-p", str(data["data"]["activeTerminalPid"])] 64 | ) 65 | if _lastTerminalLsof and lsof != _lastTerminalLsof: 66 | _inTerminal = True 67 | _lastTerminalLsof = lsof 68 | else: 69 | _inTerminal = False 70 | 71 | _lastEditorSelection = editor_selection 72 | # if _inTerminal: 73 | # print("TERMINAL: {}".format(editor_selection)) 74 | return _inTerminal 75 | else: 76 | print("Rest API does not have 'port' setting.") 77 | return False 78 | 79 | 80 | ctx = Context("vscodeterminal", func=terminal_context) 81 | ctx.keymap({"testing": "vscode testing"}) 82 | 83 | 84 | def _update_scope(): 85 | global _terminalContextJob 86 | if active_app().bundle == "com.microsoft.VSCode": 87 | # print("In vscode, updating scope...") 88 | talon.update_scope() 89 | else: 90 | cron.cancel(_terminalContextJob) 91 | _terminalContextJob = None 92 | 93 | 94 | if ENABLED: 95 | _terminalContextJob = cron.interval("2s", _update_scope) 96 | -------------------------------------------------------------------------------- /apps/zoom.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context, ContextGroup, Key 2 | from talon_plugins import speech 3 | 4 | from ..utils import text 5 | 6 | dictation_group = ContextGroup("zoom") 7 | ctx = Context("zoom", bundle="us.zoom.xos", group=dictation_group) 8 | ctx.keymap( 9 | { 10 | "toggle Mike": [Key("cmd-shift-a"), lambda m: speech.set_enabled(False)], 11 | "toggle video": [Key("cmd-shift-v")], 12 | } 13 | ) 14 | dictation_group.load() 15 | -------------------------------------------------------------------------------- /debug.py: -------------------------------------------------------------------------------- 1 | from talon.engine import engine 2 | import json 3 | 4 | 5 | def listener(topic, m): 6 | cmd = m["cmd"] 7 | # Just comment these out if you want the hairy objects. 8 | if topic == "cmd" and cmd["cmd"] == "g.load" and m["success"] == True: 9 | print("[grammar reloaded]") 10 | elif topic == "cmd" and cmd["cmd"] == "g.listset": 11 | print(f'[list {m["cmd"]["list"]} updated: {m["cmd"]["items"][0:6]}...]') 12 | elif topic == "cmd" and cmd["cmd"] == "g.update": 13 | print("[user scripts updated]") 14 | elif topic == "cmd" and cmd["cmd"] == "g.unload": 15 | print("[user scripts unloaded]") 16 | elif topic == "cmd" and cmd["cmd"] in {"w.add", "w.remove"}: 17 | print("[dragon vocab updated]") 18 | elif topic == "phrase" and cmd == "p.end": 19 | if "parsed" in m: 20 | print(f'[phase: {m["parsed"]}]') 21 | else: 22 | print(topic, m) 23 | 24 | 25 | # Uncomment to enable. 26 | # engine.register('', listener) 27 | def unload(): 28 | engine.unregister("", listener) 29 | 30 | 31 | # unload() 32 | -------------------------------------------------------------------------------- /lang/language.py: -------------------------------------------------------------------------------- 1 | # Language: 2 | # Programming specific dictation, mostly that which could vary by language. 3 | # Mostly keyword commands of the form "state " so that the keywords in the syntax 4 | # can be expressed with no ambiguity. (Consider `state else` -> "else" vs `word else` -> "elves".) 5 | import os 6 | 7 | from talon.voice import Context, Key 8 | 9 | from ..text.formatters import ( 10 | GOLANG_PRIVATE, 11 | DOT_SEPARATED, 12 | DOWNSCORE_SEPARATED, 13 | SENTENCE, 14 | GOLANG_PUBLIC, 15 | LOWSMASH, 16 | JARGON, 17 | formatted_text, 18 | ) 19 | from ..utils import i, delay, text_with_leading 20 | 21 | last_filename = "" 22 | 23 | 24 | def not_extension_context(*exts): 25 | def language_match(app, win): 26 | global last_filename 27 | if win is None: 28 | return True 29 | title = win.title 30 | filename = last_filename 31 | # print("Window title:" + title) 32 | if app.bundle == "com.microsoft.VSCode": 33 | if u"\u2014" in title: 34 | filename = title.split(u" \u2014 ", 1)[0] # Unicode em dash! 35 | elif "-" in title: 36 | filename = title.split(u" - ", 1)[0] 37 | elif app.bundle == "com.apple.Terminal": 38 | parts = title.split(" \u2014 ") 39 | if len(parts) >= 2 and parts[1].startswith(("vi ", "vim ")): 40 | filename = parts[1].split(" ", 1)[1] 41 | else: 42 | return True 43 | elif str(app.bundle).startswith("com.jetbrains."): 44 | filename = title.split(" - ")[-1] 45 | filename = filename.split(" [")[0] 46 | elif win.doc: 47 | filename = win.doc 48 | else: 49 | return True 50 | filename = filename.strip() 51 | if "." in filename: 52 | last_filename = filename 53 | else: 54 | filename = last_filename 55 | _, ext = os.path.splitext(filename) 56 | # print(ext, exts, ext not in exts) 57 | return ext not in exts 58 | 59 | return language_match 60 | 61 | 62 | def extension_context(ext): 63 | def language_match(app, win): 64 | global last_filename 65 | if win is None: 66 | return True 67 | title = win.title 68 | filename = last_filename 69 | # print("Window title:" + title) 70 | if app.bundle == "com.microsoft.VSCode": 71 | if u"\u2014" in title: 72 | filename = title.split(u" \u2014 ", 1)[0] # Unicode em dash! 73 | elif "-" in title: 74 | filename = title.split(u" - ", 1)[0] 75 | elif app.bundle == "com.apple.Terminal": 76 | parts = title.split(" \u2014 ") 77 | if len(parts) >= 2 and parts[1].startswith(("vi ", "vim ")): 78 | filename = parts[1].split(" ", 1)[1] 79 | else: 80 | return False 81 | elif str(app.bundle).startswith("com.jetbrains."): 82 | filename = title.split(" - ")[-1] 83 | filename = filename.split(" [")[0] 84 | elif win.doc: 85 | filename = win.doc 86 | else: 87 | return False 88 | filename = filename.strip() 89 | if "." in filename: 90 | last_filename = filename 91 | return filename.endswith(ext) 92 | return last_filename.endswith(ext) 93 | 94 | return language_match 95 | 96 | 97 | ctx = Context("python", func=extension_context(".py")) 98 | # ctx.vocab = [ 99 | # '', 100 | # '', 101 | # ] 102 | # ctx.vocab_remove = [''] 103 | 104 | # Most of the formatted insertions are downscore_separated under the assumption 105 | # that you are operating on a locally scoped variable. 106 | ctx.keymap( 107 | { 108 | "logical and": i(" and "), 109 | "logical or": i(" or "), 110 | "state comment": i("# "), 111 | "[line] comment [over]": [ 112 | Key("cmd-right"), 113 | i("# "), 114 | formatted_text(SENTENCE), 115 | ], 116 | # "add comment [over]": [ 117 | # Key("cmd-right"), 118 | # text_with_leading(" # "), 119 | # ], 120 | "state (def | deaf | deft)": i("def "), 121 | "function [over]": [ 122 | i("def "), 123 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 124 | i("():"), 125 | Key("left left"), 126 | ], 127 | "method [over]": [ 128 | i("def "), 129 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 130 | i("(self, ):"), 131 | Key("left left"), 132 | ], 133 | "state else if": i("elif "), 134 | "state if": i("if "), 135 | "is not none": i(" is not None"), 136 | "is none": i(" is None"), 137 | "if [over]": [ 138 | i("if "), 139 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 140 | ], 141 | "state while": i("while "), 142 | "while [over]": [ 143 | i("while "), 144 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 145 | ], 146 | "state for": i("for "), 147 | "for [over]": [ 148 | i("for "), 149 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 150 | ], 151 | "body": [Key("cmd-right : enter")], 152 | "state import": i("import "), 153 | "import [over]": [ 154 | i("for "), 155 | formatted_text(DOT_SEPARATED, JARGON), 156 | ], 157 | "state class": i("class "), 158 | "class [over]": [ 159 | i("class "), 160 | formatted_text(GOLANG_PUBLIC), 161 | i(":\n"), 162 | ], 163 | "state (past | pass)": i("pass"), 164 | "state true": i("True"), 165 | "state false": i("False"), 166 | "state none": i("None"), 167 | "item [over]": [ 168 | i(", "), 169 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 170 | ], 171 | "swipe [] [over]": [ 172 | Key("right"), 173 | i(", "), 174 | formatted_text(DOWNSCORE_SEPARATED, JARGON), 175 | ], 176 | } 177 | ) 178 | 179 | ctx = Context("golang", func=extension_context(".go")) 180 | ctx.vocab = ["nil", "context", "lambda", "init"] 181 | ctx.vocab_remove = ["Linda", "Doctor", "annette"] 182 | ctx.keymap( 183 | { 184 | "empty string": i('""'), 185 | "is not empty": i('.len != 0'), 186 | "variadic": i("..."), 187 | "logical and": i(" && "), 188 | "logical or": i(" || "), 189 | # Many of these add extra terrible spacing under the assumption that 190 | # gofmt/goimports will erase it. 191 | "state comment": i("// "), 192 | "[line] comment ": [ 193 | Key("cmd-right"), 194 | i("// "), 195 | formatted_text(SENTENCE), 196 | ], 197 | # "add comment [over]": [ 198 | # Key("cmd-right"), 199 | # text_with_leading(" // "), 200 | # ], 201 | # "[state] context": i("ctx"), 202 | "CTX": i("ctx"), 203 | "state (funk | func | fun)": i("func "), 204 | "function (Annette | init) [over]": [i("func init() {\n")], 205 | "function [over]": [ 206 | i("func "), 207 | formatted_text(GOLANG_PRIVATE, JARGON), 208 | i("("), 209 | delay(0.1), 210 | ], 211 | "method [over]": [ 212 | i("meth "), 213 | formatted_text(GOLANG_PRIVATE, JARGON), 214 | delay(0.1), 215 | ], 216 | "state var": i("var "), 217 | "variable [] [over]": [ 218 | i("var "), 219 | formatted_text(GOLANG_PRIVATE, JARGON), 220 | # i(" "), 221 | delay(0.1), 222 | ], 223 | "of type [] [over]": [ 224 | i(" "), 225 | formatted_text(GOLANG_PRIVATE, JARGON), 226 | ], 227 | # "set [over]": [ 228 | # formatted_text(GOLANG_PRIVATE, JARGON), 229 | # i(" := "), 230 | # delay(0.1), 231 | # ], 232 | "state break": i("break"), 233 | "state (chan | channel)": i(" chan "), 234 | "state go": i("go "), 235 | "state if": i("if "), 236 | "if [over]": [i("if "), formatted_text(GOLANG_PRIVATE, JARGON)], 237 | "spawn [over]": [ 238 | i("go "), 239 | formatted_text(GOLANG_PRIVATE, JARGON), 240 | ], 241 | "state else if": i(" else if "), 242 | "else if [over]": [ 243 | i(" else if "), 244 | formatted_text(GOLANG_PRIVATE, JARGON), 245 | ], 246 | "state else": i(" else "), 247 | "else [over]": [ 248 | i(" else {"), 249 | Key("enter"), 250 | formatted_text(GOLANG_PRIVATE, JARGON), 251 | ], 252 | "state while": i( 253 | "while " 254 | ), # actually a live template for "for" with a single condition 255 | "while [over]": [ 256 | i("while "), 257 | formatted_text(GOLANG_PRIVATE, JARGON), 258 | ], 259 | "state for": i("for "), 260 | "for [over]": [ 261 | i("for "), 262 | formatted_text(GOLANG_PRIVATE, JARGON), 263 | ], 264 | "state for range": i("forr "), 265 | "range [over]": [ 266 | i("forr "), 267 | formatted_text(GOLANG_PRIVATE, JARGON), 268 | ], 269 | "state format": i("fmt"), 270 | "format [over]": [ 271 | i("fmt."), 272 | formatted_text(GOLANG_PUBLIC, JARGON), 273 | ], 274 | "state switch": i("switch "), 275 | "switch [over]": [ 276 | i("switch "), 277 | formatted_text(GOLANG_PRIVATE, JARGON), 278 | ], 279 | "state select": i("select "), 280 | # "select ": [i("select "), formatted_text(GOLANG_PRIVATE, JARGON)], 281 | "state (const | constant)": i(" const "), 282 | "constant [over]": [ 283 | i("const "), 284 | formatted_text(GOLANG_PUBLIC, JARGON), 285 | ], 286 | "state case": i(" case "), 287 | "state default": i(" default:"), 288 | "case [over]": [ 289 | i("case "), 290 | formatted_text(GOLANG_PRIVATE, JARGON), 291 | ], 292 | "state type": i(" type "), 293 | "type [over]": [ 294 | i("type "), 295 | formatted_text(GOLANG_PUBLIC, JARGON), 296 | ], 297 | "state true": i(" true "), 298 | "state false": i(" false "), 299 | "state (start | struct | struck)": [i(" struct {"), Key("enter")], 300 | "(struct | struck) [over]": [ 301 | i(" struct {"), 302 | Key("enter"), 303 | formatted_text(GOLANG_PUBLIC, JARGON), 304 | ], 305 | "[state] empty interface": i(" interface{} "), 306 | "state interface": [i(" interface {"), Key("enter")], 307 | "interface [over]": [ 308 | i(" interface {"), 309 | Key("enter"), 310 | formatted_text(GOLANG_PUBLIC, JARGON), 311 | ], 312 | "state string": i(" string "), 313 | "[state] (int | integer | ant)": i("int"), 314 | "state slice": i(" []"), 315 | "slice [of] ": [ 316 | i("[]"), 317 | delay(0.1), 318 | formatted_text(LOWSMASH, JARGON), 319 | ], 320 | "[state] (no | nil)": i("nil"), 321 | "state (int | integer | ant) 64": i(" int64 "), 322 | "state tag": [i(" ``"), Key("left")], 323 | "field tag [over]": [ 324 | i(" `"), 325 | delay(0.1), 326 | formatted_text(LOWSMASH, JARGON), 327 | i(" "), 328 | delay(0.1), 329 | ], 330 | "state return": i(" return "), 331 | "return [over]": [ 332 | i("return "), 333 | formatted_text(GOLANG_PRIVATE, JARGON), 334 | ], 335 | "map of string to string": i(" map[string]string "), 336 | "map of [over]": [ 337 | i("map["), 338 | formatted_text(GOLANG_PRIVATE, JARGON), 339 | Key("right"), 340 | delay(0.1), 341 | ], 342 | "receive": i(" <- "), 343 | "make": i("make("), 344 | "loggers [] [over]": [ 345 | i("logrus."), 346 | formatted_text(GOLANG_PUBLIC, JARGON), 347 | ], 348 | "length [over]": [ 349 | i("len("), 350 | formatted_text(GOLANG_PRIVATE, JARGON), 351 | ], 352 | "append [over]": [ 353 | i("append("), 354 | formatted_text(GOLANG_PRIVATE, JARGON), 355 | ], 356 | "state (air | err)": i("err"), 357 | # "error": i(" err "), 358 | "loop over [] [over]": [ 359 | i("forr "), 360 | formatted_text(GOLANG_PRIVATE, JARGON), 361 | ], 362 | "item [over]": [i(", "), formatted_text(GOLANG_PRIVATE, JARGON)], 363 | "value [over]": [ 364 | i(": "), 365 | formatted_text(GOLANG_PRIVATE, JARGON), 366 | ], 367 | "address of [] [over]": [ 368 | i("&"), 369 | formatted_text(GOLANG_PRIVATE, JARGON), 370 | ], 371 | "pointer to [] [over]": [ 372 | i("*"), 373 | formatted_text(GOLANG_PRIVATE, JARGON), 374 | ], 375 | "swipe [] [over]": [ 376 | Key("right"), 377 | i(", "), 378 | formatted_text(GOLANG_PRIVATE, JARGON), 379 | ], 380 | "index [over]": [ 381 | i("[]"), 382 | Key("left"), 383 | formatted_text(GOLANG_PRIVATE, JARGON), 384 | ], 385 | } 386 | ) 387 | 388 | 389 | def forget_last_language(_): 390 | global last_filename 391 | last_filename = "" 392 | 393 | 394 | ctx = Context("generic", func=not_extension_context(".go", ".py")) 395 | ctx.vocab = ["nil", "context", "lambda", "init"] 396 | ctx.vocab_remove = ["Linda", "Doctor", "annette"] 397 | ctx.keymap( 398 | { 399 | "logical and": i(" && "), 400 | "logical or": i(" || "), 401 | "swipe": [Key("right"), i(", ")], 402 | "clear language context": forget_last_language, 403 | } 404 | ) 405 | 406 | ctx = Context("jargon") 407 | ctx.keymap( 408 | { 409 | "state jason": i("json"), 410 | "state (oct a | okta | octa)": i("okta"), 411 | "state (a w s | aws)": i("aws"), 412 | "state bite": i("byte"), 413 | "state bites": i("bytes"), 414 | "state state": i("state"), 415 | } 416 | ) 417 | -------------------------------------------------------------------------------- /misc/basic_keys.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from talon.voice import Context, press 4 | 5 | from ..utils import insert 6 | 7 | alpha_alt = ( 8 | "air bat cap drum each fine gust harp sit jury crunch look made near " 9 | + "odd pit quench red sun trap urge vest whale plex yank zip" 10 | ).split() 11 | 12 | f_keys = {f"F {i}": f"f{i}" for i in range(1, 13)} 13 | # arrows are separated because 'up' has a high false positive rate 14 | arrows = ["left", "right", "up", "down", "pageup", "pagedown"] 15 | simple_keys = ["tab", "escape", "enter", "space", "home", "pageup", "pagedown", "end"] 16 | alternate_keys = {"delete": "backspace", "forward delete": "delete"} 17 | symbols = { 18 | "back tick": "`", 19 | "comma": ",", 20 | "dot": ".", 21 | "point": ".", 22 | "period": ".", 23 | "semi": ";", 24 | "semicolon": ";", 25 | "tick": "'", 26 | "quote": '"', 27 | "L square": "[", 28 | "left square": "[", 29 | "square": "[", 30 | "R square": "]", 31 | "right square": "]", 32 | "forward slash": "/", 33 | "slash": "/", 34 | "backslash": "\\", 35 | "minus": "-", 36 | "dash": "-", 37 | "equals": "=", 38 | } 39 | modifiers = { 40 | "command": "cmd", 41 | "control": "ctrl", 42 | "shift": "shift", 43 | "alt": "alt", 44 | "option": "alt", 45 | } 46 | 47 | alphabet = dict(zip(alpha_alt, string.ascii_lowercase)) 48 | alphabet["kush"] = "k" 49 | alphabet["nor"] = "n" 50 | alphabet["oil"] = "o" 51 | alphabet["yes"] = "y" 52 | digits = {str(i): str(i) for i in range(10)} 53 | simple_keys = {k: k for k in simple_keys} 54 | arrows = {k: k for k in arrows} 55 | keys = {} 56 | keys.update(f_keys) 57 | keys.update(simple_keys) 58 | keys.update(alternate_keys) 59 | keys.update(symbols) 60 | 61 | # map alphanumeric and keys separately so engine gives priority to letter/number repeats 62 | keymap = keys.copy() 63 | keymap.update(arrows) 64 | keymap.update(alphabet) 65 | keymap.update(digits) 66 | 67 | 68 | def get_modifiers(m): 69 | try: 70 | return [modifiers[mod] for mod in m["basic_keys.modifiers"]] 71 | except KeyError: 72 | return [] 73 | 74 | 75 | def get_keys(m): 76 | groups = [ 77 | "basic_keys.keys", 78 | "basic_keys.arrows", 79 | "basic_keys.digits", 80 | "basic_keys.alphabet", 81 | ] 82 | for group in groups: 83 | try: 84 | return [keymap[k] for k in m[group]] 85 | except KeyError: 86 | pass 87 | return [] 88 | 89 | 90 | def uppercase_letters(m): 91 | insert("".join(get_keys(m)).upper()) 92 | 93 | 94 | def letters(m): 95 | insert("".join(get_keys(m)).lower()) 96 | 97 | 98 | def press_keys(m): 99 | mods = get_modifiers(m) 100 | keys_to_press = get_keys(m) 101 | if mods: 102 | press("-".join(mods + [keys_to_press[0]])) 103 | keys_to_press = keys_to_press[1:] 104 | for k in keys_to_press: 105 | press(k) 106 | 107 | 108 | ctx = Context("basic_keys") 109 | ctx.keymap( 110 | { 111 | "(uppercase | ship) {basic_keys.alphabet}+ [(lowercase | sunk)]": uppercase_letters, 112 | "{basic_keys.modifiers}* {basic_keys.alphabet}+": press_keys, 113 | "{basic_keys.modifiers}* {basic_keys.digits}+": press_keys, 114 | "{basic_keys.modifiers}* {basic_keys.keys}+": press_keys, 115 | "(go | {basic_keys.modifiers}+) {basic_keys.arrows}+": press_keys, 116 | } 117 | ) 118 | ctx.set_list("alphabet", alphabet.keys()) 119 | ctx.set_list("digits", digits.keys()) 120 | ctx.set_list("keys", keys.keys()) 121 | ctx.set_list("modifiers", modifiers.keys()) 122 | ctx.set_list("arrows", arrows.keys()) 123 | -------------------------------------------------------------------------------- /misc/dictation.py: -------------------------------------------------------------------------------- 1 | from talon.voice import ContextGroup, Context, Key 2 | from talon import ui 3 | 4 | from ..utils import insert, parse_word 5 | 6 | # used for auto-spacing 7 | punctuation = set(".,-!?") 8 | sentence_ends = set(".!?").union({"\n", "\n\n"}) 9 | 10 | 11 | class AutoFormat: 12 | def __init__(self): 13 | self.reset() 14 | self.caps = True 15 | self.space = False 16 | ui.register("app_deactivate", lambda app: self.reset()) 17 | ui.register("win_focus", lambda win: self.reset()) 18 | 19 | def reset(self): 20 | self.caps = True 21 | self.space = False 22 | 23 | def insert_word(self, word): 24 | word = parse_word(word) 25 | 26 | if self.caps: 27 | word = word[0].upper() + word[1:] 28 | if self.space and word[0] not in punctuation and "\n" not in word: 29 | insert(" ") 30 | 31 | insert(word) 32 | 33 | self.caps = word in sentence_ends 34 | self.space = "\n" not in word 35 | 36 | def phrase(self, m): 37 | for word in m.dgndictation[0]: 38 | self.insert_word(word) 39 | 40 | 41 | dictation_group = ContextGroup("dictation") 42 | dictation = Context("dictation", group=dictation_group) 43 | dictation_group.load() 44 | dictation_group.disable() 45 | 46 | auto_format = AutoFormat() 47 | dictation.keymap({" [over]": auto_format.phrase, }) # "press enter": Key("enter") 48 | -------------------------------------------------------------------------------- /misc/eye_3mon_snap.py: -------------------------------------------------------------------------------- 1 | # Tweaked eye_mon_snap.py with assumptions: 2 | # - Three monitors, going left to right as #2 #1 #3 3 | # 4 | # Problems: 5 | # - Gaze doesn't seem to do well with the edges of the screen. 6 | # You'll have to put your monitors farther apart than you might want from an ergonomic perspective. 7 | 8 | from talon.track.geom import EyeFrame, Point2d 9 | from talon_plugins.eye_mouse import config, mouse, tracker 10 | 11 | from talon import ctrl, tap, ui 12 | 13 | main = ui.main_screen() 14 | 15 | 16 | def is_on_main(p): 17 | # Fudging this a bit around the edges 18 | return ( 19 | main.x - 10 < p.x < main.x + main.width + 10 20 | and main.y - 10 < p.y < main.y + main.height + 10 21 | ) 22 | 23 | 24 | class MonThreeSnap: 25 | def __init__(self): 26 | if len(ui.screens()) == 1: 27 | return 28 | tap.register(tap.MMOVE, self.on_move) 29 | tap.register(tap.MCLICK, self.on_click) 30 | tracker.register("gaze", self.on_gaze) 31 | self.left = None 32 | self.right = None 33 | if len(ui.screens()) >= 2: 34 | print("Have left screen") 35 | self.left = ui.screens()[1] 36 | self.saved_mouse_left = Point2d( 37 | self.left.x + self.left.width // 2, self.left.y + self.left.height // 2 38 | ) 39 | if len(ui.screens()) == 3: 40 | print("Have right screen") 41 | self.right = ui.screens()[2] 42 | self.saved_mouse_right = Point2d( 43 | self.right.x + self.right.width // 2, self.right.y + self.right.height // 2 44 | ) 45 | self.main_mouse = False 46 | self.main_gaze = False 47 | self.restore_counter = 0 48 | 49 | def on_gaze(self, b): 50 | if not config.control_mouse: 51 | return 52 | l, r = EyeFrame(b, "Left"), EyeFrame(b, "Right") 53 | p = (l.gaze + r.gaze) / 2 54 | # XXX Calculate avg. z-depth in calibration. 55 | # print(f"{(l.pos.z + r.pos.z) / 2}") 56 | main_gaze = -0.02 < p.x < 1.00 and -0.02 < p.y < 1.02 and bool(l or r) 57 | if self.main_gaze and self.main_mouse and not main_gaze: 58 | self.restore_counter += 1 59 | if self.restore_counter > 5: 60 | # print(bool(l), bool(r), p.x, p.y) 61 | 62 | self.restore() 63 | else: 64 | self.restore_counter = 0 65 | self.main_gaze = main_gaze 66 | # config.control_mouse = True 67 | 68 | def restore(self): 69 | # l, r = mouse.eye_hist[-1] 70 | # print(f"{(l.pos.z + r.pos.z) / 2}") 71 | ctrl.cursor_visible(True) 72 | pos = mouse.xy_hist[-1] 73 | if self.right is None or (pos.x < main.width / 2 and self.left is not None): 74 | # print(f"Restore left: {pos} {self.saved_mouse_left}") 75 | if self.saved_mouse_left: 76 | mouse.last_ctrl = self.saved_mouse_left 77 | ctrl.mouse(self.saved_mouse_left.x, self.saved_mouse_left.y) 78 | # self.saved_mouse_left = None 79 | self.main_gaze = False 80 | elif pos.x > main.width / 2: 81 | # print(f"Restore right: {pos} {self.saved_mouse_right}") 82 | if self.saved_mouse_right: 83 | mouse.last_ctrl = self.saved_mouse_right 84 | ctrl.mouse(self.saved_mouse_right.x, self.saved_mouse_right.y) 85 | # self.saved_mouse_right = None 86 | self.main_gaze = False 87 | # else: 88 | # print(f"Restore? {p}") 89 | 90 | def on_move(self, typ, e): 91 | if typ != tap.MMOVE: 92 | return 93 | p = Point2d(e.x, e.y) 94 | on_main = is_on_main(p) 95 | self.main_mouse = on_main 96 | 97 | def on_click(self, typ, e): 98 | # print(typ, e.flags & tap.UP) 99 | if e.flags & tap.UP: 100 | p = Point2d(e.x, e.y) 101 | # print(f"Checking for left/right saved update {p}") 102 | if self.right is None or (p.x < main.x and self.left is not None): 103 | # print("Updated left") 104 | self.saved_mouse_left = p 105 | elif p.x > main.x + 30 and self.right is not None: 106 | # print("Updated right") 107 | self.saved_mouse_right = p 108 | 109 | # snap = MonThreeSnap() 110 | -------------------------------------------------------------------------------- /misc/eye_control.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context 2 | from talon_plugins import eye_mouse 3 | 4 | ctx = Context("eye_control") 5 | 6 | 7 | def _start_calibration(): 8 | return lambda m: eye_mouse.calib_start() 9 | 10 | 11 | ctx.keymap( 12 | { 13 | # "debug overlay": lambda m: eye_mouse.on_menu( 14 | # "Eye Tracking >> Show Debug Overlay" 15 | # ), 16 | # "control mouse": lambda m: eye_mouse.on_menu("Eye Tracking >> Control Mouse"), 17 | # "camera overlay": lambda m: eye_mouse.on_menu( 18 | # "Eye Tracking >> Show Camera Overlay" 19 | # ), 20 | # "run calibration": lambda m: eye_mouse.on_menu("Eye Tracking >> Calibrate") 21 | "run calibration": _start_calibration() 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /misc/eye_hide.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from talon import ctrl, ui 4 | from talon_plugins.eye_mouse import tracker, mouse, control_mouse, Point2d 5 | 6 | main = ui.main_screen() 7 | 8 | 9 | def is_on_main(p): 10 | # Fudging this a bit around the edges 11 | return ( 12 | main.x - 10 < p.x < main.x + main.width + 10 13 | and main.y - 10 < p.y < main.y + main.height + 10 14 | ) 15 | 16 | 17 | class EyeHide: 18 | def __init__(self): 19 | self.show = False 20 | tracker.register("post:gaze", self.on_gaze) 21 | ui.register("win_focus", self.on_focus) 22 | ui.register("app_activate", self.on_focus) 23 | 24 | def on_focus(self, win): 25 | ctrl.cursor_visible(self.show or not control_mouse.enabled) 26 | 27 | def on_gaze(self, b): 28 | p = Point2d(*ctrl.mouse_pos()) 29 | on_main = is_on_main(p) 30 | if not control_mouse.enabled or not on_main or (mouse.last_ctrl and mouse.break_force > 6): 31 | self.cursor(True) 32 | ctrl.cursor_visible(True) 33 | 34 | else: 35 | try: 36 | # hides after every eye jump until a head movement 37 | origin = mouse.origin 38 | frames = [xy for xy in mouse.xy_hist if xy.ts >= origin.ts] 39 | m = max([(origin - xy).len() for xy in frames]) 40 | self.cursor(m > 5) 41 | 42 | return 43 | # this variant hides the cursor on every eye jump until it settles (can tweak radius up to 200) 44 | p, origin, radius = mouse.zone1 45 | self.cursor(radius > 20) 46 | except Exception: 47 | self.cursor(True) 48 | 49 | def cursor(self, show): 50 | now = time.time() 51 | if show: 52 | self.last_show = now 53 | elif self.show and now - self.last_show < 0.5: 54 | return 55 | 56 | if show != self.show: 57 | ctrl.cursor_visible(show) 58 | self.show = show 59 | 60 | 61 | try: 62 | hide = EyeHide() 63 | if not control_mouse.enabled: 64 | ctrl.cursor_visible(True) 65 | except AttributeError: 66 | pass 67 | -------------------------------------------------------------------------------- /misc/eye_vertical_snap.py: -------------------------------------------------------------------------------- 1 | # Tweaked eye_mon_snap.py with assumptions: 2 | # - Two monitors, going top to bottom as #2 #1 3 | # 4 | # Problems: 5 | # - Gaze doesn't seem to do well with the edges of the screen. 6 | # You'll have to put your monitors farther apart than you might want from an ergonomic perspective. 7 | 8 | from talon.track.geom import EyeFrame, Point2d 9 | from talon_plugins.eye_mouse import config, mouse, tracker 10 | 11 | from talon import ctrl, tap, ui 12 | 13 | main = ui.main_screen() 14 | 15 | 16 | def is_on_main(p): 17 | # Fudging this a bit around the edges 18 | return ( 19 | main.x < p.x < main.x + main.width 20 | and main.y - 5 < p.y < main.y + main.height + 5 21 | ) 22 | 23 | 24 | class MonTopSnap: 25 | def __init__(self): 26 | if len(ui.screens()) == 1: 27 | return 28 | tap.register(tap.MMOVE, self.on_move) 29 | tracker.register("gaze", self.on_gaze) 30 | self.top = None 31 | 32 | if len(ui.screens()) >= 2: 33 | print("Have top screen") 34 | self.top = ui.screens()[1] 35 | self.saved_mouse_top = Point2d( 36 | self.top.x + self.top.width // 2, self.top.y + self.top.height // 2 37 | ) 38 | self.main_mouse = False 39 | self.main_gaze = False 40 | self.restore_counter = 0 41 | 42 | def on_gaze(self, b): 43 | if not config.control_mouse: 44 | return 45 | l, r = EyeFrame(b, "Left"), EyeFrame(b, "Right") 46 | p = (l.gaze + r.gaze) / 2 47 | # XXX Calculate avg. z-depth in calibration. 48 | # print(f"{(l.pos.z + r.pos.z) / 2}") 49 | main_gaze = -0.02 < p.x < 1.00 and -0.02 < p.y < 1.02 and bool(l or r) 50 | if self.main_gaze and self.main_mouse and not main_gaze: 51 | self.restore_counter += 1 52 | if self.restore_counter > 5: 53 | # print(bool(l), bool(r), p.x, p.y) 54 | self.restore() 55 | else: 56 | self.restore_counter = 0 57 | self.main_gaze = main_gaze 58 | # config.control_mouse = True 59 | 60 | def restore(self): 61 | # l, r = mouse.eye_hist[-1] 62 | # print(f"{(l.pos.z + r.pos.z) / 2}") 63 | ctrl.cursor_visible(True) 64 | 65 | if self.top is not None: 66 | # print(f"Restore left: {pos} {self.saved_mouse_left}") 67 | if self.saved_mouse_top: 68 | mouse.last_ctrl = self.saved_mouse_top 69 | ctrl.mouse(self.saved_mouse_top.x, self.saved_mouse_top.y) 70 | # self.saved_mouse_left = None 71 | self.main_gaze = False 72 | 73 | # else: 74 | # print(f"Restore? {p}") 75 | 76 | def on_move(self, typ, e): 77 | if typ != tap.MMOVE: 78 | return 79 | p = Point2d(e.x, e.y) 80 | on_main = is_on_main(p) 81 | self.main_mouse = on_main 82 | if not on_main: 83 | self.saved_mouse_top = p 84 | 85 | 86 | snap = MonTopSnap() 87 | -------------------------------------------------------------------------------- /misc/help.py: -------------------------------------------------------------------------------- 1 | import string 2 | import talon 3 | from talon import voice, ui 4 | from talon.voice import Context, Key 5 | from talon.webview import Webview 6 | from . import basic_keys 7 | from ..utils import optional_numerals, numerals 8 | 9 | # reusable constant for font size (in pixels), to use in calculations 10 | FONT_SIZE = 12 11 | # border spacing, in pixels 12 | BORDER_SIZE = int(FONT_SIZE / 6) 13 | 14 | main = ui.main_screen().visible_rect 15 | # need to account for header and footer / pagination links, hence '-2' 16 | MAX_ITEMS = int(main.height // (FONT_SIZE + 2 * BORDER_SIZE) - 2) 17 | 18 | ctx = Context("help") 19 | webview_context = Context("web_view") 20 | 21 | 22 | def on_click(data): 23 | if data["id"] == "cancel": 24 | return close_webview() 25 | elif "page" in data["id"]: 26 | context, _, page = data["id"].split("-") 27 | if context == "contexts": 28 | return render_contexts_help(_, int(page)) 29 | return render_commands_webview(get_context(context), int(page)) 30 | else: 31 | return render_commands_webview(voice.talon.subs.get(data["id"])) 32 | 33 | 34 | webview = Webview() 35 | webview.register("click", on_click) 36 | 37 | css_template = ( 38 | """ 39 | 96 | """ 97 | ) 98 | 99 | templates = { 100 | "alpha": css_template 101 | + """ 102 |

alphabet

103 |
104 | 105 | {% for word, letter in kwargs['alphabet'] %} 106 | 107 | {% endfor %} 108 | 109 |
{{ letter }}{{ word }}
🔊 cancel
110 |
111 | """, 112 | "commands": css_template 113 | + """ 114 |

115 | {% if kwargs['current_page'] | int > 1 %} 116 | 117 | {% endif %} 118 | {{ kwargs['context_name'] }} commands 119 | {% if kwargs['total_pages'] | int > 1 %} 120 | - page {{ kwargs['current_page'] }} of {{ kwargs['total_pages'] }} 121 | {% endif %} 122 | {% if kwargs['current_page'] | int < kwargs['total_pages'] %} 123 | 124 | {% endif %} 125 |

126 |
127 | 128 | {% for trigger, mapped_to in kwargs['mapping'] %} 129 | 130 | {% endfor %} 131 | 132 |
🔊 {{ trigger }}{{ mapped_to|e }}
🔊 cancel
133 |
134 | """, 135 | "contexts": css_template 136 | + """ 137 |

138 | {% if kwargs['current_page'] | int > 1 %} 139 | 140 | {% endif %} 141 | contexts 142 | {% if kwargs['total_pages'] | int > 1 %} 143 | - {{ kwargs['current_page'] }} of {{ kwargs['total_pages'] }} 144 | {% endif %} 145 | {% if kwargs['current_page'] | int < kwargs['total_pages'] %} 146 | 147 | {% endif %} 148 |

149 |
150 | 151 | {% for index, context in kwargs['contexts'] %} 152 | 153 | 154 | 155 | {% endfor %} 156 | 157 |
🔊 help {{ index }}{{ context.name }}
🔊 cancel
158 |
159 | """, 160 | } 161 | 162 | 163 | def render_page(template, **kwargs): 164 | webview.render(template, kwargs=kwargs) 165 | 166 | 167 | def create_render_page(template, **kwargs): 168 | return lambda _: render_page(template, **kwargs) 169 | 170 | 171 | def build_pages(items): 172 | total_pages = int(len(items) // MAX_ITEMS) 173 | if len(items) % MAX_ITEMS > 0: 174 | total_pages += 1 175 | 176 | pages = [] 177 | 178 | # add elements to each page based on the page index 179 | for page in range(1, total_pages + 1): 180 | pages.append(items[((page - 1) * MAX_ITEMS) : (page * MAX_ITEMS)]) 181 | 182 | return pages 183 | 184 | 185 | def render_webview(template, keymap, **kwargs): 186 | keymap.update({"cancel": lambda x: close_webview()}) 187 | webview_context.keymap(keymap) 188 | webview_context.load() 189 | render_page(template, **kwargs) 190 | webview.show() 191 | 192 | 193 | def close_webview(): 194 | webview.hide() 195 | webview_context.unload() 196 | 197 | 198 | def render_alphabet_help(_): 199 | alphabet = list(zip(basic_keys.alpha_alt, string.ascii_lowercase)) 200 | render_webview(templates["alpha"], {}, alphabet=alphabet) 201 | 202 | 203 | # needed because of how closures work in Python 204 | def create_context_mapping(context): 205 | return lambda _: render_commands_webview(context) 206 | 207 | 208 | def render_contexts_help(_, target_page=1): 209 | contexts = [] 210 | keymap = {} 211 | 212 | for idx, context in enumerate(voice.talon.subs.values()): 213 | contexts.append((idx + 1, context)) 214 | number = str(idx + 1) 215 | if len(number) == 2: 216 | number = f"{number[0]}0 {number[1]}" 217 | keymap.update({"help " + number: create_context_mapping(context)}) 218 | 219 | pages = build_pages(contexts) 220 | 221 | for idx, items in enumerate(pages): 222 | keymap.update( 223 | { 224 | "page " 225 | + str(idx + 1): create_render_page( 226 | templates["contexts"], 227 | contexts=items, 228 | actives=voice.talon.active, 229 | current_page=idx + 1, 230 | total_pages=len(pages), 231 | ) 232 | } 233 | ) 234 | 235 | render_webview( 236 | templates["contexts"], 237 | keymap, 238 | contexts=pages[target_page - 1], 239 | actives=voice.talon.active, 240 | current_page=target_page, 241 | total_pages=len(pages), 242 | ) 243 | 244 | 245 | # overrides handle edge cases: 246 | # - commonly misheard contexts 247 | # - context names that are homophones 248 | # - alternative pronunciations for convenience 249 | overrides = { 250 | "pearl": "perl", 251 | "icontrol": "eyecontrol", 252 | "lack": "slack", 253 | "chrome": "googlechrome", 254 | "get": "git", 255 | "docs": "googledocs", 256 | "google docs": "googledocs", 257 | "see": "c", 258 | "adam": "atom", 259 | } 260 | 261 | 262 | def clean_word(word): 263 | # removes some extra stuff added by dragon, e.g. 'I\\pronoun' 264 | return str(word).split("\\", 1)[0] 265 | 266 | 267 | def normalize_words(words): 268 | words = [clean_word(w) for w in words] 269 | find = "".join(words).lower().replace(" ", "") 270 | return overrides.get(find, find) 271 | 272 | 273 | def normalize_context(context): 274 | return context.replace("_", "").lower() 275 | 276 | 277 | def get_context(context_name): 278 | contexts = {normalize_context(k): v for k, v in voice.talon.subs.items()} 279 | return contexts.get(normalize_words(context_name)) 280 | 281 | 282 | def format_action(action): 283 | if isinstance(action, talon.voice.Key): 284 | keys = action.data.split(" ") 285 | if len(keys) > 1 and len(set(keys)) == 1: 286 | return f"key({keys[0]}) * {len(keys)}" 287 | else: 288 | return f"key({action.data})" 289 | elif isinstance(action, talon.voice.Str): 290 | return f'"{action.data}"' 291 | elif isinstance(action, talon.voice.Rep): 292 | return f'"{action.data}"' 293 | elif isinstance(action, voice.RepPhrase): 294 | return f"repeat_phrase({action.data})" 295 | elif isinstance(action, str): 296 | return f'"{action}"' 297 | elif callable(action): 298 | return f"{action.__name__}()" 299 | else: 300 | return str(action) 301 | 302 | 303 | def format_actions(actions): 304 | actions = actions if isinstance(actions, (list, tuple)) else [actions] 305 | return ", ".join([format_action(a) for a in actions]) 306 | 307 | 308 | def render_commands_help(m): 309 | context = get_context(m.dgndictation[0]._words) 310 | if not context: 311 | return 312 | 313 | return render_commands_webview(context) 314 | 315 | 316 | def normalize_trigger(trigger): 317 | trigger = trigger.replace(numerals, "##") 318 | trigger = trigger.replace(optional_numerals, "[##]") 319 | return trigger 320 | 321 | 322 | def render_commands_webview(context, target_page=1): 323 | 324 | # what you say is stored as a trigger 325 | mapping = [] 326 | for trigger in context.triggers.keys(): 327 | actions = context.mapping[context.triggers[trigger]] 328 | mapping.append((normalize_trigger(trigger), format_actions(actions))) 329 | 330 | pages = build_pages(mapping) 331 | keymap = {} 332 | 333 | # create the commands to navigate through pages 334 | for idx, items in enumerate(pages): 335 | keymap.update( 336 | { 337 | "page " 338 | + str(idx + 1): create_render_page( 339 | templates["commands"], 340 | context_name=context.name, 341 | mapping=items, 342 | current_page=idx + 1, 343 | total_pages=len(pages), 344 | ) 345 | } 346 | ) 347 | 348 | render_webview( 349 | templates["commands"], 350 | keymap, 351 | context_name=context.name, 352 | mapping=(pages[target_page - 1] if pages else []), 353 | current_page=target_page, 354 | total_pages=len(pages), 355 | ) 356 | 357 | 358 | keymap = { 359 | "help alphabet": render_alphabet_help, 360 | "help [commands] ": render_commands_help, 361 | "help context": render_contexts_help, 362 | } 363 | 364 | ctx.keymap(keymap) 365 | -------------------------------------------------------------------------------- /misc/luxafor.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from talon import keychain, cron, applescript 4 | from talon_plugins.speech import talon as tcg 5 | 6 | import requests 7 | 8 | # https://luxafor.com/webhook-api/ 9 | # at the repl, keychain.add("luxafor", "webhookid", "ID") 10 | try: 11 | WEBHOOK_ID = keychain.find("luxafor", "webhookid") 12 | except: 13 | WEBHOOK_ID = "" 14 | 15 | USE_WEBHOOK = False 16 | 17 | # https://github.com/anonfunc/light-flag.git 18 | SHELL_SOLID = "~/bin/light-flag -solid {color} -mini {mini}" 19 | SHELL_BLINK = "~/bin/light-flag -blink {blink} -side back" 20 | COLOR = None 21 | MINI = None 22 | 23 | 24 | def solid_color(color, mini=None): 25 | global COLOR, MINI 26 | if mini is None: 27 | mini = color 28 | if COLOR == color and MINI == mini: 29 | return 30 | if USE_WEBHOOK: 31 | requests.post( 32 | "https://api.luxafor.com/webhook/v1/actions/solid_color", 33 | json={"userId": WEBHOOK_ID, "actionFields": {"color": color}}, 34 | headers={"Content-Type": "application/json"}, 35 | ).raise_for_status() 36 | else: 37 | cmd = SHELL_SOLID.format(color=color, mini=mini) 38 | p = subprocess.Popen(cmd, shell=True) 39 | 40 | COLOR = color 41 | MINI = mini 42 | 43 | 44 | def blink_color(color): 45 | if USE_WEBHOOK: 46 | requests.post( 47 | "https://api.luxafor.com/webhook/v1/actions/blink", 48 | json={"userId": WEBHOOK_ID, "actionFields": {"color": color}}, 49 | headers={"Content-Type": "application/json"}, 50 | ).raise_for_status() 51 | else: 52 | cmd = SHELL_BLINK.format(color=COLOR, blink=color) 53 | subprocess.check_call(cmd, shell=True) 54 | 55 | 56 | def set_color_for_main(): 57 | base_color = "red" if tcg.enabled else "green" 58 | if running_screensaver(): 59 | solid_color("blue", mini=base_color) 60 | else: 61 | solid_color(base_color) 62 | 63 | 64 | def running_screensaver(): 65 | return ( 66 | applescript.run( 67 | """ 68 | tell application "System Events" 69 | get running of screen saver preferences 70 | end tell 71 | """ 72 | ) 73 | == "true" 74 | ) 75 | 76 | 77 | cron.interval("2s", set_color_for_main) 78 | -------------------------------------------------------------------------------- /misc/maestro.py: -------------------------------------------------------------------------------- 1 | from talon import applescript 2 | # from talon.voice import Context 3 | 4 | 5 | def kmaestro(script_id): 6 | def _kmaestro(*_): 7 | print("Kmaestro {}".format(script_id)) 8 | 9 | applescript.run( 10 | """ 11 | tell application "Keyboard Maestro Engine" 12 | do script "{id}" 13 | end tell 14 | """.format( 15 | id=script_id 16 | ) 17 | ) 18 | 19 | return _kmaestro 20 | # 21 | # 22 | # ctx = Context("maestro") 23 | # ctx.keymap({"switcher": kmaestro("Switcher"), "window max": kmaestro("Windy Max")}) 24 | -------------------------------------------------------------------------------- /misc/misc.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import talon.clip as clip 4 | from talon.voice import Context, Key, press 5 | 6 | from talon import keychain, tap, ui 7 | from ..utils import add_vocab, delay, select_last_insert, text 8 | 9 | 10 | def learn_selection(_): 11 | with clip.capture() as s: 12 | press("cmd-c", wait=2000) 13 | words = s.get().split() 14 | add_vocab(words) 15 | print("Learned " + ",".join(words)) 16 | 17 | 18 | ctx = Context("misc") 19 | ctx.vocab = ["Jira"] 20 | ctx.keymap( 21 | { 22 | "learn selection": learn_selection, 23 | "(alfred | launch)": Key("cmd-space"), 24 | "(alfred | launch) [over]": [Key("cmd-space"), delay(0.4), text], 25 | "correct": select_last_insert, 26 | "toggle dark": lambda _: subprocess.check_call( 27 | ["open", "/System/Library/CoreServices/ScreenSaverEngine.app"] 28 | ), 29 | "terminal": lambda _: [ui.launch(bundle="com.googlecode.iterm2")], 30 | # "focus GoLand": lambda _: [ui.launch(bundle="com.jetbrains.goland")], 31 | # "focus PyCharm": lambda _: [ui.launch(bundle="com.jetbrains.pycharm")], 32 | "go toolbox": Key("cmd+shift+ctrl+f1"), 33 | "password amigo": keychain.find("login", "user"), 34 | "snippet []": [ # XXX Doesn't really go here 35 | Key("cmd-shift-j"), 36 | delay(0.1), 37 | text, 38 | ], 39 | # "under": delay(0.2), 40 | # Clipboard 41 | "clippings []": [Key("cmd+ctrl+c"), delay(0.1), text], 42 | "(kapeli | Cappelli)": Key("cmd-shift-space"), 43 | # Menubar: 44 | "menubar": Key("ctrl-f2"), 45 | "menubar [over]": [Key("cmd+shift+/"), delay(0.1), text], 46 | "menu icons": Key("ctrl-f8"), 47 | # Bartender needed for this one 48 | "menu search": Key("ctrl-shift-f8"), 49 | # Different input volume levels 50 | # "input volume high": lambda _: set_input_volume(90), 51 | # "input volume low": lambda _: set_input_volume(30), 52 | } 53 | ) 54 | 55 | ctx = Context("shortcat") 56 | ctx.keymap( 57 | { 58 | "(shortcat | short cap | shortcut) []": [ 59 | Key("cmd+shift+ctrl+alt+n"), 60 | text, 61 | ] 62 | } 63 | ) 64 | 65 | ctx = Context("login", bundle="com.apple.loginwindow") 66 | ctx.keymap({"amigo": [keychain.find("login", "user"), Key("enter")]}) 67 | 68 | 69 | # keychain.remove("login", "user"); keychain.add("login", "user", "pass") 70 | 71 | 72 | def misc_hotkey(_, e): 73 | # if e.down: 74 | # print(e) 75 | if e.down and e == "cmd-ctrl-§" or e == "ctrl-alt-shift-cmd-bksp": 76 | subprocess.check_call( 77 | ["open", "/System/Library/CoreServices/ScreenSaverEngine.app"] 78 | ), 79 | e.block() 80 | return True 81 | 82 | 83 | tap.register(tap.HOOK | tap.KEY, misc_hotkey) 84 | -------------------------------------------------------------------------------- /misc/mouse.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from talon import cron, ctrl, tap, ui 4 | from talon.voice import Context 5 | from talon_plugins import eye_mouse, eye_zoom_mouse 6 | 7 | ctx = Context("mouse") 8 | 9 | x, y = ctrl.mouse_pos() 10 | mouse_history = [(x, y, time.time())] 11 | force_move = None 12 | 13 | 14 | def on_move(_, e): 15 | time_ = (e.x, e.y, time.time()) 16 | # print(time_) 17 | mouse_history.append(time_) 18 | if force_move: 19 | e.x, e.y = force_move 20 | return True 21 | return False 22 | 23 | 24 | tap.register(tap.MMOVE, on_move) 25 | 26 | 27 | # noinspection PyProtectedMember 28 | def click_pos(m, from_end=False): 29 | word = m._words[0] 30 | if from_end: 31 | word = m._words[-1] 32 | # print(f"word is {word} {word.start} {word.end}") 33 | # start = (word.start + min((word.end - word.start) / 2, 0.100)) / 1000.0 34 | word_time = word.end / 1000.0 35 | # if from_end: 36 | # word_time = word.end / 1000.0 37 | # print(f"word start is {word_time}, now is {time.time()}") 38 | for pos in reversed(mouse_history): 39 | if pos[2] < word_time: 40 | # print(f"pos is {pos}") 41 | return pos[:2] 42 | return mouse_history[-1][:2] 43 | 44 | 45 | def delayed_click(m, button=0, times=1, from_end=False, mods=None): 46 | if mods is None: 47 | mods = [] 48 | old = eye_mouse.config.control_mouse 49 | eye_mouse.config.control_mouse = False 50 | x, y = click_pos(m, from_end=from_end) 51 | ctrl.mouse(x, y) 52 | for key in mods: 53 | ctrl.key_press(key, down=True) 54 | ctrl.mouse_click(x, y, button=button, times=times, wait=16000) 55 | for key in mods[::-1]: 56 | ctrl.key_press(key, up=True) 57 | time.sleep(0.032) 58 | eye_mouse.config.control_mouse = old 59 | 60 | 61 | def delayed_right_click(m): 62 | delayed_click(m, button=1) 63 | 64 | 65 | def delayed_dubclick(m): 66 | delayed_click(m, button=0, times=2) 67 | 68 | 69 | def delayed_tripclick(m): 70 | delayed_click(m, button=0, times=3) 71 | 72 | 73 | def mouse_drag(m): 74 | x, y = click_pos(m) 75 | ctrl.mouse_click(x, y, down=True) 76 | 77 | 78 | def mouse_release(m): 79 | x, y = click_pos(m) 80 | ctrl.mouse_click(x, y, up=True) 81 | 82 | 83 | def mouse_scroll(amount): 84 | def scroll(m): 85 | global scrollAmount 86 | # print("amount is", amount) 87 | if (scrollAmount >= 0) == (amount >= 0): 88 | scrollAmount += amount 89 | else: 90 | scrollAmount = amount 91 | ctrl.mouse_scroll(y=amount) 92 | 93 | return scroll 94 | 95 | 96 | def adv_click(button, *mods, **kwargs): 97 | def click(e): 98 | for key in mods: 99 | ctrl.key_press(key, down=True) 100 | delayed_click(e) 101 | for key in mods[::-1]: 102 | ctrl.key_press(key, up=True) 103 | 104 | return click 105 | 106 | 107 | def control_mouse(m): 108 | ctrl.mouse(10, 10) 109 | eye_mouse.control_mouse.toggle() 110 | if eye_zoom_mouse.zoom_mouse.enabled: 111 | eye_zoom_mouse.zoom_mouse.disable() 112 | 113 | 114 | def control_zoom_mouse(m): 115 | ctrl.mouse(10, 10) 116 | if eye_zoom_mouse.zoom_mouse.enabled: 117 | eye_zoom_mouse.zoom_mouse.disable() 118 | else: 119 | eye_zoom_mouse.zoom_mouse.enable() 120 | 121 | eye_zoom_mouse.zoom_mouse.toggle() 122 | if eye_mouse.control_mouse.enabled: 123 | eye_mouse.control_mouse.toggle() 124 | 125 | 126 | clickJob = None 127 | 128 | 129 | def click_me(): 130 | ctrl.mouse_click(button=0) 131 | 132 | 133 | def startClicking(m): 134 | global clickJob 135 | clickJob = cron.interval("30ms", click_me) 136 | 137 | 138 | def stopClicking(m): 139 | global clickJob 140 | cron.cancel(clickJob) 141 | 142 | 143 | def scrollMe(): 144 | global scrollAmount 145 | if scrollAmount: 146 | ctrl.mouse_scroll(by_lines=False, y=scrollAmount / 10) 147 | 148 | 149 | def startScrolling(m): 150 | global scrollJob 151 | scrollJob = cron.interval("60ms", scrollMe) 152 | 153 | 154 | def stopScrolling(m): 155 | global scrollAmount, scrollJob, gazeJob 156 | scrollAmount = 0 157 | cron.cancel(scrollJob) 158 | cron.cancel(gazeJob) 159 | 160 | 161 | def gazeScroll(): 162 | windows = ui.windows() 163 | window = None 164 | x, y = ctrl.mouse_pos() 165 | for w in windows: 166 | if w.rect.contains(x, y): 167 | window = w.rect 168 | break 169 | if window is None: 170 | return 171 | midpoint = window.y + window.height / 2 172 | amount = ((y - midpoint) / (window.height / 10)) ** 3 173 | ctrl.mouse_scroll(by_lines=False, y=amount) 174 | # print(f"gazeScroll: {midpoint} {window.height} {amount}") 175 | 176 | 177 | def startCursorScrolling(m): 178 | global gazeJob 179 | stopScrolling(m) 180 | gazeJob = cron.interval("60ms", gazeScroll) 181 | 182 | 183 | scrollAmount = 0 184 | scrollJob = None 185 | 186 | gazeJob = None 187 | 188 | 189 | def toggle_cursor(show): 190 | def _toggle(_): 191 | ctrl.cursor_visible(show) 192 | 193 | return _toggle 194 | 195 | 196 | keymap = { 197 | "hide cursor": toggle_cursor(False), 198 | "show cursor": toggle_cursor(True), 199 | # "debug overlay": lambda m: eye.on_menu("Eye Tracking >> Show Debug Overlay"), 200 | "(gaze | control mouse)": control_mouse, 201 | "zoom mouse": control_zoom_mouse, 202 | # "camera overlay": lambda m: eye.on_menu("Eye Tracking >> Show Camera Overlay"), 203 | } 204 | 205 | click_keymap = { 206 | "click": delayed_click, 207 | "right click": delayed_right_click, 208 | "double click": delayed_dubclick, 209 | "triple click": delayed_tripclick, 210 | "drag click": mouse_drag, 211 | "release click": mouse_release, 212 | # "auto click": startClicking, 213 | # "stop click": stopClicking, 214 | "wheel down": mouse_scroll(30), 215 | "wheel down continuous": [mouse_scroll(30), startScrolling], 216 | "wheel up": mouse_scroll(-30), 217 | "wheel up continuous": [mouse_scroll(-30), startScrolling], 218 | "wheel stop": stopScrolling, 219 | "wheel scroll": startCursorScrolling, 220 | "command click": adv_click(0, "cmd"), 221 | "control click": adv_click(0, "ctrl"), 222 | "(option | opt | alt) click": adv_click(0, "alt"), 223 | "shift click": adv_click(0, "shift"), 224 | "(shift alt | alt shift | shift option | option shift) click": adv_click( 225 | 0, "alt", "shift" 226 | ), 227 | "(shift double | double shift) click": adv_click(0, "shift", times=2), 228 | } 229 | keymap.update(click_keymap) 230 | 231 | ctx.keymap(keymap) 232 | 233 | ctrl.cursor_visible(True) 234 | -------------------------------------------------------------------------------- /misc/mouse_jump.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from collections import defaultdict 4 | 5 | from talon.voice import Context 6 | 7 | from talon import app, ctrl, ui, resource 8 | from .. import utils 9 | 10 | warps_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "warps.json") 11 | with resource.open(warps_file) as fh: 12 | resource_data = json.load(fh) 13 | 14 | 15 | class MouseWarp: 16 | def __init__(self): 17 | self.data = defaultdict(dict) 18 | self.data.update(resource_data) 19 | 20 | def mark(self, name): 21 | name = name.lower() 22 | window = ui.active_window() 23 | bundle = window.app.bundle 24 | x, y = ctrl.mouse_pos() 25 | rect = window.rect 26 | center_x, center_y = rect.center 27 | x_offset = x - (rect.left if x < center_x else rect.right) 28 | y_offset = y - (rect.top if y < center_y else rect.bot) 29 | app.notify(f"Marked: {name}") 30 | # self.load() 31 | self.data[bundle][name] = [int(x_offset), int(y_offset)] 32 | self.dump() 33 | 34 | def delete(self, name): 35 | window = ui.active_window() 36 | bundle = window.app.bundle 37 | print("deleting " + name) 38 | del self.data[bundle][name] 39 | self.dump() 40 | 41 | def warp(self, name): 42 | # self.load() 43 | window = ui.active_window() 44 | bundle = window.app.bundle 45 | # print(f"{window}{bundle}{self.data[bundle]}") 46 | try: 47 | x_offset, y_offset = self.data[bundle][name] 48 | except KeyError: 49 | return 50 | rect = window.rect 51 | x = rect.left + (x_offset % rect.width) 52 | y = rect.top + (y_offset % rect.height) 53 | ctrl.mouse(x, y) 54 | 55 | def warps(self): 56 | try: 57 | # self.load() 58 | window = ui.active_window() 59 | bundle = window.app.bundle 60 | keys = self.data[bundle].keys() 61 | # print(keys) 62 | return keys 63 | except Exception as e: 64 | # print(e) 65 | return [] 66 | 67 | def dump(self): 68 | resource.write(warps_file, json.dumps(self.data, indent=2)) 69 | 70 | 71 | mj = MouseWarp() 72 | ctx = Context("warp") 73 | ctx.keymap( 74 | { 75 | "mark [over]": [ 76 | lambda m: mj.mark(utils.join_words(utils.parse_words(m))), 77 | lambda _: ctx.set_list("warps", mj.warps()), 78 | ], 79 | "warp {warp.warps}": [lambda m: mj.warp(m["warp.warps"][0])], 80 | "clear warp {warp.warps} [over]": [lambda m: mj.delete(m["warp.warps"][0])], 81 | "list warps": [ 82 | lambda _: app.notify("Warps:", ", ".join(mj.warps())), 83 | lambda _: ctx.set_list("warps", mj.warps()), 84 | ], 85 | "click {warp.warps}": [ 86 | lambda m: mj.warp(m["warp.warps"][0]), 87 | lambda _: ctrl.mouse_click(button=0), 88 | ], 89 | } 90 | ) 91 | 92 | 93 | def ui_event(event, arg): 94 | if event in ("win_open", "win_closed") and arg.app.name == "Amethyst": 95 | return 96 | if event in ("app_activate", "app_launch", "app_close", "win_open", "win_close"): 97 | ctx.set_list("warps", mj.warps()) 98 | 99 | 100 | ui.register("", ui_event) 101 | -------------------------------------------------------------------------------- /misc/mouse_ocr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | import time 5 | import xml.etree.ElementTree as ElementTree 6 | from math import sqrt 7 | 8 | from talon import ctrl, ui 9 | from talon.voice import Context 10 | from ..utils import parse_word, join_words 11 | 12 | # UPSCALE = 1 13 | 14 | RETINA_SIZE = ( 15 | 1680, 16 | 1050, 17 | ) # Hack. If I'm exactly this resolution, assume I'm in retina mode. 18 | RETINA_FACTOR = 2 19 | SCALE = 1 20 | 21 | 22 | def ocr_screen(x, y, w, h, factor): 23 | subprocess.check_call( 24 | ["screencapture", "-t", "png", "-x", f"-R{x},{y},{w},{h}", "/tmp/capture.png"] 25 | ) 26 | subprocess.check_call( 27 | [ 28 | "/usr/local/bin/convert", 29 | "/tmp/capture.png", 30 | "-colorspace", 31 | "Gray", 32 | "-sharpen", 33 | "0x1", 34 | "-sample", 35 | f"{w*SCALE*factor}x{h*SCALE*factor}", 36 | "-contrast-stretch", 37 | "0", 38 | "/tmp/capture2.png", 39 | ] 40 | ) 41 | hocr = subprocess.check_output( 42 | [ 43 | "/usr/local/bin/tesseract", 44 | "-l", 45 | "eng", 46 | "--dpi", 47 | "72", 48 | "--psm", 49 | "4", 50 | "/tmp/capture2.png", 51 | "stdout", 52 | "hocr", 53 | ] 54 | ).decode() # type: str 55 | return "\n".join(hocr.splitlines()[1:]) 56 | 57 | 58 | def distance(point1, point2): 59 | return sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2) 60 | 61 | 62 | def move_to_ocr(m): 63 | old_pos = ctrl.mouse_pos() 64 | start = time.time() 65 | screen = ui.main_screen() 66 | factor = 1 67 | if (int(screen.width), int(screen.height)) == RETINA_SIZE: 68 | factor = RETINA_FACTOR 69 | midpoint = None 70 | bounds = [screen.x, screen.y, screen.width, screen.height] 71 | which = int(parse_word(m._words[1])) 72 | row = int(which - 1) // 3 73 | col = int(which - 1) % 3 74 | bounds = [ 75 | screen.x + int(col * screen.width // 3), 76 | screen.y + int(row * screen.height // 3), 77 | screen.width // 3, 78 | screen.height // 3, 79 | ] 80 | midpoint = (bounds[0] + bounds[2] // 2, bounds[1] + bounds[3] // 2) 81 | # print(which, row, col, bounds, midpoint) 82 | # noinspection PyProtectedMember 83 | search = join_words(list(map(parse_word, m.dgnwords[0]._words))).lower().strip() 84 | ctrl.mouse_move(*midpoint) 85 | # print(f"Starting teleport {which} to {search}") 86 | hocr = ocr_screen(*bounds, factor=factor) 87 | # print(f"... OCR'd screen: {time.time() - start} seconds.") 88 | tree = ElementTree.XML(hocr) # type: list[ElementTree.Element] 89 | # print(list(tree[1])) 90 | best_pos = None 91 | best_distance = screen.width + screen.height 92 | for span in tree[1].iter(): 93 | # print(span, span.attrib.get("class", "")) 94 | if span.attrib.get("class", "") == "ocrx_word": 95 | if search in span.text.lower(): 96 | # title is something like"bbox 72 3366 164 3401; x_wconf 95" 97 | title = span.attrib["title"] # type: str 98 | x, y, side, bottom = [ 99 | int(i) / (SCALE * factor) for i in title.split(";")[0].split()[1:] 100 | ] 101 | candidate = bounds[0] + (x + side) / 2, bounds[1] + (y + bottom) / 2 102 | dist = distance(candidate, midpoint) 103 | if dist < best_distance: 104 | # print(search, span.text, span.attrib["title"]) 105 | best_pos = candidate 106 | if best_pos is not None: 107 | print(f"... Found match, moving to {best_pos}. {time.time() - start} seconds.") 108 | ctrl.mouse_move(best_pos[0], best_pos[1]) 109 | return 110 | print(f"... No match. {time.time() - start} seconds.") 111 | ctrl.mouse_move(*old_pos) 112 | 113 | 114 | # ctx = Context("ocr") 115 | 116 | # ctx.keymap({"teleport (1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9) ++": [move_to_ocr]}) 117 | -------------------------------------------------------------------------------- /misc/mouse_rc.py: -------------------------------------------------------------------------------- 1 | from talon import canvas, ctrl, ui, tap 2 | from talon.audio import noise 3 | from talon.voice import Context, ContextGroup, Key 4 | from talon_plugins import speech 5 | from talon.track.geom import Point2d 6 | from talon.track.filter import Acceleration 7 | 8 | import time 9 | from math import radians, pi, sin, cos 10 | 11 | SIZE = 10 12 | SPEED = .5 13 | 14 | 15 | class MouseRcCar: 16 | def __init__(self): 17 | self.main_screen = ui.main_screen() 18 | self.offset_x = self.main_screen.x + self.main_screen.width // 2 19 | self.offset_y = self.main_screen.y + self.main_screen.height // 2 20 | self.angle = 0 21 | self.speed = 0.0 22 | self.mcanvas = canvas.Canvas.from_screen(self.main_screen) 23 | self.active = False 24 | self.last_draw = time.time() 25 | self.accel = Acceleration( 26 | cd=(.001, 100.0), 27 | v=(0.0004, 0.0025), 28 | lmb=1000.0, 29 | ratio=0.3 30 | ) 31 | self.hiss_start = time.time() 32 | noise.register("noise", self.on_noise) 33 | # tap.register(tap.MMOVE, self.on_move) 34 | 35 | # def on_move(self, typ, e): 36 | # if typ != tap.MMOVE or not self.active: 37 | # return 38 | # 39 | # # print("moved ", e, self.offset_x, self.offset_y) 40 | # if (e.x, e.y) != (self.offset_x, self.offset_y): 41 | # self.stop(None) 42 | 43 | def start(self, *_): 44 | if self.active: 45 | return 46 | self.mcanvas.register('draw', self.draw) 47 | self.active = True 48 | ctrl.cursor_visible(False) 49 | 50 | def stop(self, *_): 51 | self.mcanvas.unregister('draw', self.draw) 52 | self.active = False 53 | ctrl.cursor_visible(True) 54 | 55 | def draw(self, canvas): 56 | paint = canvas.paint 57 | paint.color = "ff00ff" 58 | paint.stroke_width = 5 59 | # print(paint.__dir__()) 60 | now = time.time() 61 | elapsed = now - self.last_draw 62 | hiss_dt = now - self.hiss_start 63 | c = cos(self.angle) 64 | s = sin(self.angle) 65 | delta = Point2d(self.speed * c, self.speed * s) 66 | # print(delta) 67 | delta = delta.apply(self.accel, hiss_dt/100) 68 | # print(delta) 69 | self.offset_x += delta.x 70 | if self.offset_x < 0: 71 | self.offset_x = 0 72 | elif self.offset_x > self.main_screen.width: 73 | self.offset_x = self.main_screen.width 74 | self.offset_y += delta.y 75 | if self.offset_y < 0: 76 | self.offset_y = 0 77 | elif self.offset_y > self.main_screen.height: 78 | self.offset_y = self.main_screen.height 79 | ctrl.mouse_move(self.offset_x, self.offset_y) 80 | line1 = self.rotate(c, s, 0, 0, -2*SIZE, SIZE) 81 | line2 = self.rotate(c, s, 0, 0, -2*SIZE, -SIZE) 82 | line3 = (line1[2], line1[3], line2[2], line2[3]) 83 | 84 | canvas.draw_line(self.offset_x + line1[0], self.offset_y+line1[1], self.offset_x+line1[2],self.offset_y+line1[3]) 85 | canvas.draw_line(self.offset_x + line2[0], self.offset_y+line2[1], self.offset_x+line2[2],self.offset_y+line2[3]) 86 | canvas.draw_line(self.offset_x + line3[0], self.offset_y+line3[1], self.offset_x+line3[2],self.offset_y+line3[3]) 87 | self.last_draw = now 88 | 89 | def rotate(self, c, s, x1, y1, x2, y2): 90 | return x1*c+y1*s, x1*s-y1*c, x2*c+y2*s, x2*s-y2*c 91 | 92 | def on_noise(self, noise): 93 | if not self.active: 94 | return 95 | # print("NOIZE", noise) 96 | if noise == "pop": 97 | self.angle -= radians(90) 98 | if self.angle <= 0: 99 | self.angle += 2*pi 100 | elif noise == "hiss_start": 101 | self.speed = SPEED 102 | self.hiss_start = time.time() 103 | elif noise == "hiss_end": 104 | self.speed = 0.0 105 | 106 | def reset(self, _): 107 | self.offset_x = self.main_screen.x + self.main_screen.width // 2 108 | self.offset_y = self.main_screen.y + self.main_screen.height // 2 109 | self.angle = 0 110 | self.speed = 0.0 111 | self.main_screen = ui.main_screen() 112 | ctrl.cursor_visible(True) 113 | 114 | # 115 | # mg = MouseRcCar() 116 | # ctx = Context("MouseRcCarStarter") 117 | # ctx.keymap({ 118 | # "start driving": [mg.reset, mg.start], 119 | # "(done | stop) driving": mg.stop, 120 | # # "snap done": [mg.stop, lambda _: ctx.unload()], 121 | # }) 122 | # 123 | # #mg.start(None) 124 | -------------------------------------------------------------------------------- /misc/mouse_snap.py: -------------------------------------------------------------------------------- 1 | from talon import canvas, ctrl, ui 2 | from talon.voice import Context, ContextGroup, Key 3 | from talon.voice import talon as talon_cg 4 | from talon_plugins import speech 5 | 6 | 7 | class MouseSnap: 8 | def __init__(self): 9 | self.main_screen = ui.main_screen() 10 | self.offset_x = self.main_screen.x 11 | self.offset_y = self.main_screen.y 12 | self.width = self.main_screen.width 13 | self.height = self.main_screen.height 14 | self.last_state = None 15 | self.save_last() 16 | self.mcanvas = canvas.Canvas.from_screen(self.main_screen) 17 | self.active = False 18 | 19 | def start(self, *_): 20 | if self.active: 21 | return 22 | self.mcanvas.register('draw', self.draw) 23 | self.active = True 24 | 25 | def stop(self, *_): 26 | if not self.active: 27 | return 28 | self.mcanvas.unregister('draw', self.draw) 29 | self.active = False 30 | 31 | def draw(self, canvas): 32 | paint = canvas.paint 33 | paint.color = "ff0000" 34 | canvas.draw_line(self.offset_x + self.width / 2, self.offset_y, self.offset_x + self.width / 2, 35 | self.offset_y + self.height) 36 | canvas.draw_line(self.offset_x, self.offset_y + self.height / 2, self.offset_x + self.width, 37 | self.offset_y + self.height / 2) 38 | 39 | def north(self, _): 40 | self.save_last() 41 | self.height /= 2 42 | 43 | def south(self, _): 44 | self.save_last() 45 | self.height /= 2 46 | self.offset_y += self.height 47 | 48 | def west(self, _): 49 | self.save_last() 50 | self.width /= 2 51 | 52 | def east(self, _): 53 | self.save_last() 54 | self.width /= 2 55 | self.offset_x += self.width 56 | 57 | def pos(self): 58 | return self.offset_x + self.width/2, self.offset_y + self.height/2 59 | 60 | def reset(self, _): 61 | self.save_last() 62 | self.offset_x = self.main_screen.x 63 | self.offset_y = self.main_screen.y 64 | self.main_screen = ui.main_screen() 65 | self.width = self.main_screen.width 66 | self.height = self.main_screen.height 67 | 68 | def save_last(self): 69 | self.last_state = (self.offset_x, self.offset_y, self.width, self.height) 70 | 71 | def go_back(self, _): 72 | self.offset_x, self.offset_y, self.width, self.height = self.last_state 73 | 74 | 75 | # mg = MouseSnap() 76 | # 77 | # ctx = Context("mouseSnap") 78 | # ctx.keymap({ 79 | # "north": mg.north, 80 | # "east": mg.east, 81 | # "south": mg.south, 82 | # "west": mg.west, 83 | # "leap": [lambda _: ctrl.mouse(*mg.pos()), mg.stop, lambda _: ctx.unload()], 84 | # "stay": [lambda _: ctrl.mouse(*mg.pos())], 85 | # "oops": mg.go_back, 86 | # "reset": mg.reset, 87 | # "done": [mg.stop, lambda _: ctx.unload()], 88 | # }) 89 | # 90 | # startCtx = Context("mouseSnapStarter") 91 | # startCtx.keymap({ 92 | # "snap": [mg.reset, mg.start, lambda _: ctx.load()], 93 | # # "snap done": [mg.stop, lambda _: ctx.unload()], 94 | # }) 95 | -------------------------------------------------------------------------------- /misc/mouse_snap9.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | from talon import canvas, ctrl, ui 5 | from talon.voice import Context, ContextGroup 6 | from talon_plugins import speech, eye_mouse, eye_zoom_mouse 7 | 8 | from .mouse import click_keymap 9 | 10 | 11 | class MouseSnapNine: 12 | def __init__(self): 13 | self.states = [] 14 | # self.screen_index = 0 15 | self.screen = ui.screens()[0] 16 | self.offset_x = self.screen.x 17 | self.offset_y = self.screen.y 18 | self.width = self.screen.width 19 | self.height = self.screen.height 20 | self.states.append((self.offset_x, self.offset_y, self.width, self.height)) 21 | self.mcanvas = canvas.Canvas.from_screen(self.screen) 22 | self.active = False 23 | self.moving = False 24 | self.count = 0 25 | self.was_eye_tracking = False 26 | 27 | # tap.register(tap.MMOVE, self.on_move) 28 | # 29 | # def on_move(self, typ, e): 30 | # if typ != tap.MMOVE or not self.active: 31 | # return 32 | # x, y = self.pos() 33 | # last_pos = self.states[-1] 34 | # x2, y2 = last_pos[0] + last_pos[2]//2, last_pos[1] + last_pos[3]//2 35 | # # print("moved ", e, x, y) 36 | # if (e.x, e.y) != (x, y) and (e.x, e.y) != (x2, y2): 37 | # self.stop(None) 38 | 39 | def start(self, *_): 40 | if self.active: 41 | return 42 | # noinspection PyUnresolvedReferences 43 | if eye_zoom_mouse.zoom_mouse.enabled: 44 | return 45 | if eye_mouse.control_mouse.enabled: 46 | self.was_eye_tracking = True 47 | eye_mouse.control_mouse.toggle() 48 | if self.mcanvas is not None: 49 | self.mcanvas.unregister("draw", self.draw) 50 | self.mcanvas.register("draw", self.draw) 51 | self.active = True 52 | 53 | def stop(self, *_): 54 | self.mcanvas.unregister("draw", self.draw) 55 | self.active = False 56 | if self.was_eye_tracking and not eye_mouse.control_mouse.enabled: 57 | eye_mouse.control_mouse.toggle() 58 | self.was_eye_tracking = False 59 | 60 | def draw(self, canvas): 61 | paint = canvas.paint 62 | paint.color = "ff0000" 63 | canvas.draw_line( 64 | self.offset_x + self.width // 3, 65 | self.offset_y, 66 | self.offset_x + self.width // 3, 67 | self.offset_y + self.height, 68 | ) 69 | canvas.draw_line( 70 | self.offset_x + 2 * self.width // 3, 71 | self.offset_y, 72 | self.offset_x + 2 * self.width // 3, 73 | self.offset_y + self.height, 74 | ) 75 | 76 | canvas.draw_line( 77 | self.offset_x, 78 | self.offset_y + self.height // 3, 79 | self.offset_x + self.width, 80 | self.offset_y + self.height // 3, 81 | ) 82 | canvas.draw_line( 83 | self.offset_x, 84 | self.offset_y + 2 * self.height // 3, 85 | self.offset_x + self.width, 86 | self.offset_y + 2 * self.height // 3, 87 | ) 88 | 89 | for row in range(3): 90 | for col in range(3): 91 | canvas.draw_text( 92 | f"{row*3+col+1}", 93 | self.offset_x + self.width / 6 + col * self.width / 3, 94 | self.offset_y + self.height / 6 + row * self.height / 3, 95 | ) 96 | 97 | def narrow(self, which, move=True): 98 | self.save_state() 99 | row = int(which - 1) // 3 100 | col = int(which - 1) % 3 101 | self.offset_x += int(col * self.width // 3) 102 | self.offset_y += int(row * self.height // 3) 103 | self.width //= 3 104 | self.height //= 3 105 | if move: 106 | ctrl.mouse_move(*self.pos()) 107 | self.count += 1 108 | if self.count >= 4: 109 | self.reset(None) 110 | 111 | def pos(self): 112 | return self.offset_x + self.width // 2, self.offset_y + self.height // 2 113 | 114 | def reset(self, pos=-1): 115 | def _reset(m): 116 | self.save_state() 117 | self.count = 0 118 | x, y = ctrl.mouse_pos() 119 | 120 | if pos >= 0: 121 | self.screen = ui.screens()[pos] 122 | else: 123 | self.screen = ui.screen_containing(x, y) 124 | 125 | # print(screens) 126 | # self.screen = screens[self.screen_index] 127 | self.offset_x = self.screen.x 128 | self.offset_y = self.screen.y 129 | self.width = self.screen.width 130 | self.height = self.screen.height 131 | if self.mcanvas is not None: 132 | self.mcanvas.unregister("draw", self.draw) 133 | self.mcanvas = canvas.Canvas.from_screen(self.screen) 134 | self.mcanvas.register("draw", self.draw) 135 | if eye_mouse.control_mouse.enabled: 136 | self.was_eye_tracking = True 137 | eye_mouse.control_mouse.toggle() 138 | if self.was_eye_tracking and self.screen == ui.screens()[0]: 139 | # if self.screen == ui.screens()[0]: 140 | self.narrow_to_pos(x, y) 141 | self.narrow_to_pos(x, y) 142 | # self.narrow_to_pos(x, y) 143 | # print(self.offset_x, self.offset_y, self.width, self.height) 144 | # print(*self.pos()) 145 | 146 | return _reset 147 | 148 | def narrow_to_pos(self, x, y): 149 | col_size = int(self.width // 3) 150 | row_size = int(self.height // 3) 151 | col = math.floor((x - self.offset_x) / col_size) 152 | row = math.floor((y - self.offset_y) / row_size) 153 | # print(f"Narrow to {row} {col} {1 + col + 3 * row}") 154 | self.narrow(1 + col + 3 * row, move=False) 155 | 156 | def save_state(self): 157 | self.states.append((self.offset_x, self.offset_y, self.width, self.height)) 158 | 159 | def go_back(self, _): 160 | last_state = self.states.pop() 161 | self.offset_x, self.offset_y, self.width, self.height = last_state 162 | self.count -= 1 163 | 164 | 165 | def narrow(m): 166 | for d in m["mouseSnapNine.digits"]: 167 | mg.narrow(int(digits[d])) 168 | time.sleep(0.1) 169 | 170 | 171 | digits = dict((str(n), n) for n in range(1, 10)) 172 | digits.update( 173 | { # Needed with built in engine? 174 | "for": 4, 175 | r"one\\number": 1, 176 | "one": 1, 177 | "two": 2, 178 | "three": 3, 179 | "four": 4, 180 | "five": 5, 181 | "six": 6, 182 | "seven": 7, 183 | "eight": 8, 184 | "nine": 9, 185 | } 186 | ) 187 | 188 | mg = MouseSnapNine() 189 | group = ContextGroup("snapNine") 190 | ctx = Context("mouseSnapNine", group=group) 191 | keymap = { 192 | "{mouseSnapNine.digits}+": narrow, 193 | "(oops | back)": mg.go_back, 194 | "(reset | clear | escape)": mg.reset(), 195 | "left": mg.reset(1), 196 | "middle": mg.reset(0), 197 | "right": mg.reset(2), 198 | "(done | grid | mouse grid | mousegrid)": [ 199 | mg.stop, 200 | lambda _: ctx.unload(), 201 | lambda _: speech.set_enabled(True), 202 | ], 203 | } 204 | keymap.update( 205 | { 206 | k: [v, mg.stop, lambda _: ctx.unload(), lambda _: speech.set_enabled(True)] 207 | for k, v in click_keymap.items() 208 | } 209 | ) 210 | ctx.keymap(keymap) 211 | ctx.set_list("digits", digits.keys()) 212 | group.load() 213 | ctx.unload() 214 | 215 | 216 | def do_start_digits(m): 217 | try: 218 | for d in m["mouseSnapNineStarter.digits"]: 219 | mg.narrow(int(digits[d])) 220 | ctrl.mouse_move(*mg.pos()) 221 | except KeyError: 222 | pass 223 | 224 | 225 | startCtx = Context("mouseSnapNineStarter") 226 | startKeymap = { 227 | "(grid | mouse grid | mousegrid) [{mouseSnapNineStarter.digits}+]": [ 228 | mg.reset(), 229 | mg.start, 230 | lambda _: ctx.load(), 231 | lambda _: speech.set_enabled(False), 232 | do_start_digits, 233 | ], 234 | # "snap done": [mg.stop, lambda _: ctx.unload()], 235 | } 236 | startKeymap.update( 237 | { 238 | "(grid | mouse grid | mousegrid) [{mouseSnapNineStarter.digits}+] click": [ 239 | mg.reset(), 240 | mg.start, 241 | do_start_digits, 242 | lambda _: ctrl.mouse_click(button=0), 243 | mg.stop, 244 | ], 245 | "(grid | mouse grid | mousegrid) [{mouseSnapNineStarter.digits}+] right click": [ 246 | mg.reset(), 247 | mg.start, 248 | do_start_digits, 249 | lambda _: ctrl.mouse_click(button=1), 250 | mg.stop, 251 | ], 252 | } 253 | ) 254 | 255 | startCtx.keymap(startKeymap) 256 | startCtx.set_list("digits", digits.keys()) 257 | # mg.start() 258 | # Hot reload while grid is active is very confusing without this. 259 | speech.set_enabled(True) 260 | -------------------------------------------------------------------------------- /misc/mouse_sonar.py: -------------------------------------------------------------------------------- 1 | from talon import canvas, ctrl, ui, tap, cron 2 | from talon.audio import noise 3 | from talon.voice import Context, ContextGroup, Key 4 | from talon_plugins import speech 5 | from talon.track.geom import Point2d 6 | from talon.track.filter import Acceleration 7 | 8 | import time 9 | from math import radians, pi, sin, cos 10 | 11 | SIZE = 10 12 | SPEED = 0.5 13 | 14 | 15 | class MouseSonar: 16 | def __init__(self): 17 | self.main_screen = ui.main_screen() 18 | self.center_x = self.main_screen.x + self.main_screen.width // 2 19 | self.center_y = self.main_screen.y + self.main_screen.height // 2 20 | self.offset_x = self.center_x 21 | self.offset_y = self.center_y 22 | self.first_hiss = True 23 | self.hiss_job = None 24 | self.angle = 0 25 | self.radius = 15 26 | self.mcanvas = canvas.Canvas.from_screen(self.main_screen) 27 | self.active = False 28 | self.last_draw = time.time() 29 | self.accel = Acceleration( 30 | cd=(0.001, 100.0), v=(0.0004, 0.0025), lmb=1000.0, ratio=0.3 31 | ) 32 | noise.register("noise", self.on_noise) 33 | # tap.register(tap.MMOVE, self.on_move) 34 | 35 | # def on_move(self, typ, e): 36 | # if typ != tap.MMOVE or not self.active: 37 | # return 38 | # 39 | # # print("moved ", e, self.offset_x, self.offset_y) 40 | # if (e.x, e.y) != (self.offset_x, self.offset_y): 41 | # self.stop(None) 42 | 43 | def start(self, *_): 44 | if self.active: 45 | return 46 | self.mcanvas.register("draw", self.draw) 47 | self.active = True 48 | ctrl.cursor_visible(False) 49 | 50 | def stop(self, *_): 51 | self.mcanvas.unregister("draw", self.draw) 52 | self.active = False 53 | ctrl.cursor_visible(True) 54 | 55 | def draw(self, canvas): 56 | paint = canvas.paint 57 | paint.color = "ff00ff" 58 | paint.stroke_width = 5 59 | # print(paint.__dir__()) 60 | now = time.time() 61 | pos = Point2d(self.radius, 0) 62 | pos.rot(self.angle) 63 | 64 | ctrl.mouse_move(pos.x, pos.y) 65 | canvas.draw_line(self.center_x, self.center_y, pos.x, pos.y) 66 | self.last_draw = now 67 | 68 | def on_noise(self, noise): 69 | if not self.active: 70 | return 71 | print("NOIZE", noise) 72 | if noise == "pop": 73 | pass 74 | elif noise == "hiss_start": 75 | print("HISSING") 76 | if self.first_hiss: 77 | self.hiss_job = cron.interval("30ms", self.spin) 78 | else: 79 | self.hiss_job = cron.interval("30ms", self.drive) 80 | 81 | self.speed = SPEED 82 | elif noise == "hiss_end": 83 | print("NO LONGER HISSING") 84 | cron.cancel(self.hiss_job) 85 | self.hiss_job = None 86 | 87 | 88 | def spin(self): 89 | self.angle += radians(1) 90 | 91 | def drive(self): 92 | self.radius += 10 93 | 94 | def reset(self, _): 95 | self.offset_x = self.center_x 96 | self.offset_y = self.center_y 97 | self.first_hiss = True 98 | ctrl.cursor_visible(True) 99 | 100 | 101 | 102 | 103 | # mg.start(None) 104 | -------------------------------------------------------------------------------- /misc/mouse_squid.py: -------------------------------------------------------------------------------- 1 | from talon import canvas, ctrl, ui, tap 2 | from talon.voice import Context, ContextGroup 3 | from talon_plugins import speech, eye_mouse, eye_zoom_mouse 4 | 5 | from .mouse import click_keymap 6 | 7 | 8 | class MouseSnapSquid: 9 | def __init__(self): 10 | self.states = [] 11 | self.main_screen = ui.main_screen() 12 | self.offset_x = self.main_screen.x 13 | self.offset_y = self.main_screen.y 14 | self.width = self.main_screen.width 15 | self.height = self.main_screen.height 16 | self.states.append((self.offset_x, self.offset_y, self.width, self.height)) 17 | self.mcanvas = canvas.Canvas.from_screen(self.main_screen) 18 | self.active = False 19 | self.moving = False 20 | self.count = 0 21 | self.rows = 60 22 | self.cols = 55 23 | 24 | def start(self, *_): 25 | if self.active: 26 | return 27 | if eye_zoom_mouse.zoom_mouse.enabled: 28 | return 29 | if eye_mouse.control_mouse.enabled: 30 | return 31 | self.mcanvas.register("draw", self.draw) 32 | self.active = True 33 | 34 | def stop(self, *_): 35 | self.mcanvas.unregister("draw", self.draw) 36 | self.active = False 37 | 38 | def draw(self, canvas): 39 | paint = canvas.paint 40 | paint.color = "ff0000" 41 | # for i in range(1, self.cols+1): 42 | # canvas.draw_line(self.offset_x + i * self.width // self.cols, self.offset_y, self.offset_x + i * self.width // self.cols, 43 | # self.offset_y + self.height) 44 | # for i in range(1, self.rows+1): 45 | # canvas.draw_line(self.offset_x, self.offset_y + i * self.height // self.rows, self.offset_x + self.width, 46 | # self.offset_y + i * self.height // self.rows) 47 | 48 | for row in range(self.rows): 49 | for col in range(self.cols): 50 | canvas.draw_text( 51 | f"{col:02d}{row:02d}", 52 | self.offset_x + (col) * self.width // (self.cols), 53 | self.offset_y + (row + 1) * self.height // (self.rows), 54 | ) 55 | 56 | def narrow(self, digits): 57 | self.save_state() 58 | # print(digits) 59 | col = int(digits[0]) * 10 + int(digits[1]) 60 | row = int(digits[2]) * 10 + int(digits[3]) 61 | # print(row, col) 62 | offset_x = self.offset_x + int(col * self.width // self.cols) 63 | offset_y = self.offset_y + int(row * self.height // self.rows) 64 | width = self.width // self.cols 65 | height = self.height // self.cols 66 | # print(offset_x + width//2, offset_y + height//2) 67 | ctrl.mouse_move(offset_x + width // 2, offset_y + height // 2) 68 | self.count += 1 69 | if self.count >= 2: 70 | self.reset(None) 71 | 72 | def reset(self, _): 73 | self.save_state() 74 | self.count = 0 75 | self.offset_x = self.main_screen.x 76 | self.offset_y = self.main_screen.y 77 | self.main_screen = ui.main_screen() 78 | self.width = self.main_screen.width 79 | self.height = self.main_screen.height 80 | 81 | def save_state(self): 82 | self.states.append((self.offset_x, self.offset_y, self.width, self.height)) 83 | 84 | def go_back(self, _): 85 | last_state = self.states.pop() 86 | self.offset_x, self.offset_y, self.width, self.height = last_state 87 | self.count -= 1 88 | 89 | 90 | def narrow(m): 91 | mg.narrow(m["mouseSnapSquid.digits"]) 92 | 93 | 94 | digits = dict((str(n), n) for n in range(0, 10)) 95 | 96 | mg = MouseSnapSquid() 97 | group = ContextGroup("squid") 98 | ctx = Context("mouseSnapSquid", group=group) 99 | keymap = { 100 | "{mouseSnapSquid.digits}+": narrow, 101 | "squid": [mg.stop, lambda _: ctx.unload(), lambda _: speech.set_enabled(True)], 102 | } 103 | keymap.update({k: [v, mg.reset] for k, v in click_keymap.items()}) 104 | ctx.keymap(keymap) 105 | ctx.set_list("digits", digits.keys()) 106 | group.load() 107 | ctx.unload() 108 | 109 | startCtx = Context("mouseSnapSquidStarter") 110 | startCtx.keymap( 111 | { 112 | "squid": [ 113 | mg.reset, 114 | mg.start, 115 | lambda _: ctx.load(), 116 | lambda _: speech.set_enabled(False), 117 | ], 118 | "squid {mouseSnapSquidStarter.digits}+": [ 119 | mg.reset, 120 | mg.start, 121 | lambda m: mg.narrow("".join(m["mouseSnapSquidStarter.digits"])), 122 | mg.stop, 123 | ] 124 | # "snap done": [mg.stop, lambda _: ctx.unload()], 125 | } 126 | ) 127 | startCtx.set_list("digits", digits.keys()) 128 | # mg.start() 129 | # Hot reload while grid is active is very confusing without this. 130 | speech.set_enabled(True) 131 | -------------------------------------------------------------------------------- /misc/noise.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from talon import ctrl, tap 4 | from talon.audio import noise 5 | from talon.track.geom import Point2d 6 | from talon_plugins import eye_mouse, eye_zoom_mouse 7 | 8 | 9 | class NoiseModel: 10 | def __init__(self): 11 | self.hiss_start = 0 12 | self.hiss_last = 0 13 | self.button = 0 14 | self.mouse_origin = Point2d(0, 0) 15 | self.mouse_last = Point2d(0, 0) 16 | self.dragging = False 17 | 18 | tap.register(tap.MMOVE, self.on_move) 19 | noise.register("noise", self.on_noise) 20 | 21 | def on_move(self, typ, e): 22 | if typ != tap.MMOVE: 23 | return 24 | self.mouse_last = pos = Point2d(e.x, e.y) 25 | if self.hiss_start and not self.dragging: 26 | if (pos - self.mouse_origin).len() > 10: 27 | self.dragging = True 28 | self.button = 0 29 | x, y = self.mouse_origin.x, self.mouse_origin.y 30 | ctrl.mouse(x, y) 31 | ctrl.mouse_click(x, y, button=0, down=True) 32 | 33 | def on_noise(self, noise): 34 | if eye_zoom_mouse.zoom_mouse.enabled: 35 | return 36 | if not eye_mouse.control_mouse.enabled: 37 | return 38 | now = time.time() 39 | # print("{} {}".format(noise, now - self.hiss_last)) 40 | 41 | if noise == "pop": 42 | ctrl.mouse_click(button=0, hold=16000) 43 | # elif noise == "hiss_start": 44 | # if now - self.hiss_last < 0.4: 45 | # self.button = 0 46 | # ctrl.mouse_click(button=self.button, down=True) 47 | # self.hiss_last = now 48 | # self.dragging = True 49 | # else: 50 | # self.mouse_origin = self.mouse_last 51 | # self.hiss_start = now 52 | # elif noise == "hiss_end": 53 | # duration = time.time() - self.hiss_start 54 | # print("Hiss duration: {}".format(duration)) 55 | # if self.dragging: 56 | # ctrl.mouse_click(button=self.button, up=True) 57 | # self.dragging = False 58 | # else: 59 | # if duration > 0.7: 60 | # self.button = 2 61 | # ctrl.mouse_click(button=2) 62 | # elif duration > 0.3: 63 | # self.button = 1 64 | # ctrl.mouse_click(button=1) 65 | # self.hiss_last = now 66 | # self.hiss_start = 0 67 | 68 | 69 | model = NoiseModel() 70 | -------------------------------------------------------------------------------- /misc/phrase_frequency.py: -------------------------------------------------------------------------------- 1 | from talon import ui, webview 2 | from talon.engine import engine 3 | from talon.voice import Context 4 | 5 | from collections import defaultdict 6 | 7 | template = """ 8 | 38 | 39 |

Counts

40 | 41 | {% for phrase, text in phrases %} 42 | 43 | {% endfor %} 44 | 45 | 46 | """ 47 | 48 | webview = webview.Webview() 49 | webview.render(template, phrases=[("command", "")]) 50 | webview.move(0, ui.main_screen().height) 51 | 52 | webview_shown = False 53 | 54 | 55 | def toggle_webview(m): 56 | global webview_shown 57 | if webview_shown: 58 | webview.hide() 59 | else: 60 | webview.show() 61 | webview_shown = not webview_shown 62 | 63 | 64 | class History: 65 | def __init__(self): 66 | self.history = defaultdict(int) 67 | engine.register("post:phrase", self.on_phrase_post) 68 | 69 | def parse_phrase(self, phrase): 70 | return " ".join(word.split("\\")[0] for word in phrase) 71 | 72 | def on_phrase_post(self, j): 73 | phrase = self.parse_phrase(j.get("phrase", [])) 74 | if phrase in ("toggle frequency", "pa"): 75 | return 76 | cmd = j["cmd"] 77 | if cmd == "p.end" and phrase: 78 | self.history[phrase] += 1 79 | by_count = sorted(self.history.items(), reverse=True, key=lambda v: v[1])[:50] 80 | # print(by_count) 81 | webview.render(template, phrases=by_count) 82 | 83 | 84 | history = History() 85 | ctx = Context("phrase_frequency") 86 | ctx.keymap({"toggle frequency": toggle_webview}) 87 | # webview.show() 88 | -------------------------------------------------------------------------------- /misc/phrase_history.py: -------------------------------------------------------------------------------- 1 | from talon import ui, webview 2 | from talon.engine import engine 3 | from talon.voice import Context 4 | 5 | hist_len = 6 6 | 7 | template = """ 8 | 38 | 39 |

History

40 |
{{ phrase }}{{ text }}
{{ hypothesis }}
41 | {% for phrase, text in phrases %} 42 | 43 | {% endfor %} 44 | 45 | 46 | """ 47 | 48 | webview = webview.Webview() 49 | webview.render(template, phrases=[(" ", "")]) 50 | webview.move(0, ui.main_screen().height) 51 | 52 | webview_shown = False 53 | 54 | 55 | def toggle_webview(m): 56 | global webview_shown 57 | if webview_shown: 58 | webview.hide() 59 | else: 60 | webview.show() 61 | webview_shown = not webview_shown 62 | 63 | 64 | class History: 65 | def __init__(self): 66 | self.history = [] 67 | engine.register("post:phrase", self.on_phrase_post) 68 | 69 | def clear(self, _): 70 | self.history = [] 71 | 72 | def parse_phrase(self, phrase): 73 | return " ".join(word.split("\\")[0] for word in phrase) 74 | 75 | def on_phrase_post(self, j): 76 | phrase = self.parse_phrase(j.get("phrase", [])) 77 | if phrase in ("toggle history"): 78 | return 79 | cmd = j["cmd"] 80 | if cmd == "p.end" and phrase: 81 | if phrase != "clear history": 82 | self.history.append((phrase, "")) 83 | self.history = self.history[-hist_len:] 84 | webview.render(template, phrases=self.history) 85 | with open("last_phrase.txt", "w") as fh: 86 | fh.write(phrase) 87 | 88 | 89 | history = History() 90 | ctx = Context("phrase_history") 91 | ctx.keymap({"toggle history": toggle_webview, "clear history": history.clear}) 92 | # webview.show() 93 | -------------------------------------------------------------------------------- /misc/picker.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context, Key 2 | 3 | from .. import utils 4 | 5 | 6 | def pick(m): 7 | try: 8 | index = utils.ordinal_indexes[m["picker.ordinal"][0]] 9 | Key("down " * index + "enter")(m) 10 | except: 11 | pass 12 | 13 | 14 | ctx = Context("picker") 15 | ctx.keymap({"pick {picker.ordinal}": pick}) 16 | 17 | ctx.set_list("ordinal", utils.ordinal_indexes.keys()) 18 | -------------------------------------------------------------------------------- /misc/popups.py: -------------------------------------------------------------------------------- 1 | popup_template = """ 2 | 44 | """ 45 | 46 | 47 | def list_template(list_name): 48 | return f""" 49 |
50 |

{list_name}

51 |
{{ phrase }}{{ text }}
{{ hypothesis }}
52 | {{% for word in {list_name} %}} 53 | 54 | {{% endfor %}} 55 | 56 |
🔊 pick {{{{ word }}}}
🔊 cancel
57 | 58 | """ 59 | 60 | 61 | def dict_to_html(title, data): 62 | return f""" 63 |
64 |

{title}

65 | 66 | {"".join(['' for k,v in data.items()])} 67 | 68 |
🔊 ' + k + '' + v + '
🔊 cancel
69 |
70 | """ 71 | 72 | 73 | def quickref_template(title, col1, col2, col3): 74 | return """ 75 | 89 | """ + f""" 90 |

{title}

91 |
92 |
{col1}
93 |
{col2}
94 |
{col3}
95 |
96 | """ -------------------------------------------------------------------------------- /misc/repeat.py: -------------------------------------------------------------------------------- 1 | from talon.voice import Context, Rep, talon 2 | 3 | from ..utils import optional_numerals, text_to_number 4 | 5 | def repeat(m): 6 | 7 | # noinspection PyProtectedMember 8 | words = m._words 9 | 10 | repeat_count = text_to_number(words[1:]) 11 | print("Repeat! {} {}".format([str(w) for w in words], repeat_count)) 12 | if not repeat_count: 13 | repeat_count = 1 14 | repeater = Rep(repeat_count) 15 | repeater.ctx = talon 16 | result = repeater(None) 17 | print(f"Result: {result}") 18 | return result 19 | 20 | 21 | ctx = Context("repeaters") 22 | ctx.keymap({"repeat" + optional_numerals: repeat}) 23 | -------------------------------------------------------------------------------- /misc/speech_toggle.py: -------------------------------------------------------------------------------- 1 | from talon import app, tap, ui 2 | from talon.engine import engine 3 | from talon.voice import Context, ContextGroup, talon 4 | from talon_plugins import speech, microphone 5 | 6 | from .. import utils 7 | from ..misc.dictation import dictation_group 8 | 9 | sleep_group = ContextGroup("sleepy") 10 | sleepy = Context("sleepy", group=sleep_group) 11 | sleepy.keymap( 12 | { 13 | "talon sleep": lambda m: speech.set_enabled(False), 14 | "talon wake": lambda m: speech.set_enabled(True), 15 | "dragon mode": [ 16 | lambda m: speech.set_enabled(False), 17 | lambda _: app.notify("Dragon mode"), 18 | lambda m: dictation_group.disable(), 19 | lambda m: _mimic("wake up".split()), 20 | ], 21 | "dictation mode": [ 22 | # lambda m: speech.set_enabled(False), 23 | lambda _: app.notify("Dictation mode"), 24 | lambda m: _mimic("go to sleep".split()), 25 | lambda m: dictation_group.enable(), 26 | ], 27 | "talon mode": [ 28 | lambda m: speech.set_enabled(True), 29 | lambda _: app.notify("Talon mode"), 30 | lambda m: dictation_group.disable(), 31 | lambda m: _mimic("go to sleep".split()), 32 | ], 33 | "full sleep mode": [ 34 | lambda m: speech.set_enabled(False), 35 | lambda m: dictation_group.disable(), 36 | lambda m: _mimic("go to sleep".split()), 37 | ], 38 | } 39 | ) 40 | sleep_group.load() 41 | 42 | 43 | def _mimic(words): 44 | if engine.endpoint: 45 | engine.mimic(words) 46 | 47 | 48 | def sleep_hotkey(typ, e): 49 | # print(e) 50 | if e == 'cmd-alt-ctrl-shift-tab' and e.down: 51 | speech.set_enabled(not speech.talon.enabled) 52 | if speech.talon.enabled: 53 | if microphone.manager.active_mic() is None: 54 | utils.use_mic("krisp microphone") 55 | utils.mic_uses_volume({ 56 | "Plantronics Blackwire 435": 100, 57 | "ATR2USB": 80, 58 | "AndreaMA": 35, 59 | }) 60 | if not engine.endpoint: 61 | ui.launch(bundle="com.dragon.dictate") 62 | else: 63 | utils.set_input_volume(0) # Fallback, only override with present mic. 64 | e.block() 65 | return True 66 | 67 | 68 | tap.register(tap.HOOK | tap.KEY, sleep_hotkey) 69 | 70 | speech.set_enabled(False) 71 | 72 | 73 | # Default to krisp on startup, if present. 74 | def _use_krisp(): 75 | utils.use_mic("krisp microphone") 76 | 77 | 78 | # _use_krisp() 79 | # Start at login, but off. 80 | utils.use_mic("None") 81 | -------------------------------------------------------------------------------- /misc/switcher.py: -------------------------------------------------------------------------------- 1 | from talon import ui 2 | from talon.voice import Context, Key, Rep, Str, Word, press 3 | 4 | from ..utils import parse_word 5 | 6 | apps = {} 7 | 8 | 9 | def switch_app(m): 10 | # noinspection PyProtectedMember 11 | name = parse_word(m._words[1]) 12 | full = apps.get(name) 13 | if not full: 14 | return 15 | for app in ui.apps(): 16 | if app.name == full: 17 | app.focus() 18 | break 19 | 20 | 21 | ctx = Context("switcher") 22 | ctx.keymap({"focus {switcher.apps}": switch_app}) 23 | 24 | 25 | def update_lists(): 26 | global apps 27 | new = {} 28 | for app in ui.apps(): 29 | if not app.windows(): 30 | continue 31 | words = app.name.split(" ") 32 | for word in words: 33 | if word and word not in new and len(word) > 1: 34 | new[word] = app.name 35 | new[app.name] = app.name 36 | if set(new.keys()) == set(apps.keys()): 37 | return 38 | # print(new) 39 | ctx.set_list("apps", new.keys()) 40 | apps = new 41 | 42 | 43 | def ui_event(event, arg): 44 | if event in ('win_open', 'win_closed') and arg.app.name == 'Amethyst': 45 | return 46 | if event in ("app_activate", "app_launch", "app_close", "win_open", "win_close"): 47 | update_lists() 48 | 49 | 50 | ui.register("", ui_event) 51 | update_lists() 52 | -------------------------------------------------------------------------------- /stubs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | from inspect import signature 5 | from textwrap import dedent 6 | 7 | INCLUDED_PRIVATE_MEMBERS = ["__init__", "__call__", "_words"] 8 | 9 | talon_modules = [ 10 | m 11 | for m in sys.modules.keys() 12 | if m.startswith("talon.") or m.startswith("talon_plugins.") 13 | ] 14 | # talon_modules = ["talon.clip"] 15 | # talon_plugin_modules = [m for m in sys.modules.keys() if m.startswith("talon_plugins.")] 16 | # print(talon_plugin_modules) 17 | 18 | 19 | def dump_stubs(mod, indent=""): 20 | output = [] 21 | for identifier in dir(mod): 22 | # print(mod, id) 23 | if identifier.startswith("_") and identifier not in INCLUDED_PRIVATE_MEMBERS: 24 | continue 25 | # noinspection PyBroadException 26 | try: 27 | thing = getattr(mod, identifier) 28 | except: 29 | continue 30 | if callable(thing): 31 | output.append(stub_callable(identifier, thing, indent)) 32 | else: 33 | type_name = type(thing).__name__ 34 | if type_name == "module" or identifier.startswith("_"): 35 | continue 36 | # noinspection PyBroadException 37 | try: 38 | output.append(f"{identifier}: {type_name} = ...") 39 | except: 40 | # print(e) 41 | output.append(f"{identifier} = ...") 42 | if len(output) == 0: 43 | return "..." 44 | return f"\n{indent}" + f"\n{indent}".join( 45 | [ 46 | o.replace("", '"unknown value"') 47 | .replace("NoneType", "Any") 48 | .replace("", "'") 50 | for o in output 51 | ] 52 | ) 53 | 54 | 55 | def stub_callable(identifier, thing, indent): 56 | if callable(thing): 57 | if not isinstance(thing, type): 58 | # noinspection PyBroadException 59 | try: 60 | return f"def {identifier}{str(signature(thing))}: ..." 61 | except: 62 | return f"def {identifier}(*args, **kwargs) -> Any: ..." 63 | else: 64 | return f"class {identifier}: " + dump_stubs(thing, indent=indent + " ") 65 | 66 | 67 | STUBS_DIR = os.path.expanduser("~/.talon/user") 68 | 69 | 70 | def dump_all_stubs(): 71 | os.makedirs(STUBS_DIR, exist_ok=True) 72 | super_import = ( 73 | "\n".join( 74 | [ 75 | f"from {t} import *" 76 | for t in talon_modules + ["talon.stubbed"] 77 | if t != "talon.voice" 78 | ] 79 | ) 80 | + "\n" 81 | ) 82 | for m in talon_modules: 83 | name = ".".join(m.split(".")) 84 | if "." in name: 85 | path_join = os.path.join(STUBS_DIR, "/".join(name.split(".")[:-1])) 86 | os.makedirs(path_join, exist_ok=True) 87 | output_path = os.path.join(STUBS_DIR, name.replace(".", "/")) 88 | with open(output_path + ".pyi", "w") as fh: 89 | stubs = dump_stubs(sys.modules[m]) 90 | fh.write("from typing import *\n") 91 | # fh.write(super_import) 92 | fh.write(stubs) 93 | with open(os.path.join(STUBS_DIR, "talon", "stubbed.pyi"), "w") as fh: 94 | stub = dedent("""\ 95 | from typing import * 96 | CompiledLib: Any = ... 97 | CompiledFFI: Any = ... 98 | class _cffi_backend: 99 | CData: Any = ... 100 | getset_descriptor: Any = ... 101 | """) 102 | fh.write( 103 | stub 104 | ) 105 | with open(os.path.join(STUBS_DIR, "talon", "__init__.pyi"), "w") as fh: 106 | fh.write( 107 | dedent( 108 | """\ 109 | from typing import * 110 | """ 111 | ) 112 | ) 113 | print("Done stubbing.") 114 | 115 | 116 | # if time.time() - os.path.getmtime(os.path.join(STUBS_DIR, "talon", "__init__.pyi")) > 3600: 117 | # dump_all_stubs() 118 | # dump_all_stubs() 119 | -------------------------------------------------------------------------------- /text/formatters.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | import re 4 | 5 | from talon import resource, app 6 | import talon.clip as clip 7 | from talon.voice import Context, Word, press 8 | 9 | from ..utils import parse_word, surround, vocab, parse_words, insert 10 | 11 | jargon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "jargon.json") 12 | jargon_substitutions = {} 13 | with resource.open(jargon_path) as fh: 14 | jargon_substitutions.update(json.load(fh)) 15 | 16 | ACRONYM = (True, lambda i, word, _: word[0:1].upper()) 17 | FIRST_THREE = (True, lambda i, word, _: word[0:3]) 18 | FIRST_FOUR = (True, lambda i, word, _: word[0:4]) 19 | DUNDER = ( 20 | True, 21 | lambda i, word, last: ("__%s" % word if i == 0 else word) + ("__" if last else ""), 22 | ) 23 | CAMELCASE = (True, lambda i, word, _: word if i == 0 else word.capitalize()) 24 | SLASH_SEPARATED = (True, lambda i, word, _: "/" + word) 25 | DOT_SEPARATED = (True, lambda i, word, _: word if i == 0 else "." + word) 26 | GOLANG_PRIVATE = ( 27 | True, 28 | lambda i, word, _: word.lower() 29 | if i == 0 30 | else word 31 | if word.upper() == word 32 | else word.capitalize(), 33 | ) 34 | GOLANG_PUBLIC = ( 35 | True, 36 | lambda i, word, _: word if word.upper() == word else word.capitalize(), 37 | ) 38 | DOT_STUB = (True, lambda i, word, _: "." + word[:1] if i == 0 else word.capitalize()[:1]) 39 | SLICE = (True, lambda i, word, _: " []" + word if i == 0 else word) 40 | NO_SPACES = (True, lambda i, word, _: word.replace("-", "")) 41 | DASH_SEPARATED = (True, lambda i, word, _: word if i == 0 else "-" + word) 42 | DOWNSCORE_SEPARATED = (True, lambda i, word, _: word if i == 0 else "_" + word) 43 | LOWSMASH = (True, lambda i, word, _: word.lower()) 44 | SENTENCE = (False, lambda i, word, _: word.capitalize() if i == 0 else word) 45 | JARGON = (False, lambda i, word, _: jargon_substitutions.get(word.lower(), word)) 46 | 47 | formatters = { 48 | # Smashed 49 | "acronym": ACRONYM, 50 | "tree": FIRST_THREE, 51 | "quad": FIRST_FOUR, 52 | "dunder": DUNDER, 53 | "camel": GOLANG_PRIVATE, 54 | "slashed": SLASH_SEPARATED, 55 | # Golang private/public conventions prefer SendHTML to SendHtml sendHtml 56 | # TODO: Consider making these the "camel" impl, pep8 prefers it as well. 57 | # "private": GOLANG_PRIVATE, 58 | "upper": GOLANG_PUBLIC, 59 | # "slice": SLICE, 60 | # Call method: for driving jetbrains style fuzzy Complete -> .fuzCom 61 | "invoke": DOT_STUB, 62 | "snake": DOWNSCORE_SEPARATED, 63 | "smash": NO_SPACES, 64 | "spine": DASH_SEPARATED, 65 | # Spaced 66 | # "sentence": SENTENCE, 67 | "jargon": JARGON, 68 | "title": (False, lambda i, word, _: word.capitalize()), 69 | "allcaps": (False, lambda i, word, _: word.upper()), 70 | "lowcaps": (False, lambda i, word, _: word.lower()), 71 | "phrase": (False, lambda i, word, _: word), 72 | "bold": (False, surround("*")), 73 | "quoted": (False, surround('"')), 74 | "ticked": (False, surround("'")), 75 | "glitched": (False, surround("`")), 76 | "padded": (False, surround(" ")), 77 | "pad": (False, lambda i, word, _: " " + word if i == 0 else word), 78 | "parens": ( 79 | False, 80 | lambda i, word, last: ("(%s" % word if i == 0 else word) 81 | + (")" if last else ""), 82 | ), 83 | } 84 | 85 | 86 | def normalize(identifier): 87 | # https://stackoverflow.com/questions/29916065/how-to-do-camelcase-split-in-python 88 | return re.sub( 89 | r"[-_]", " ", re.sub("(?!^| )([A-Z0-9][a-z0-9]+)", r" \1", identifier) 90 | ) 91 | 92 | 93 | # TODO: Can I make this part of format_text? Or reuse extract_formatter_and_words? 94 | def formatted_text(*formatters): 95 | def _fmt(m): 96 | # noinspection PyProtectedMember 97 | words = parse_words(m) 98 | tmp = [] 99 | spaces = True 100 | for i, word in enumerate(words): 101 | word = parse_word(word) 102 | for formatter in formatters: 103 | smash, func = formatter 104 | word = func(i, word, i == len(words) - 1) 105 | spaces = spaces and not smash 106 | tmp.append(word) 107 | words = tmp 108 | 109 | sep = " " 110 | if not spaces: 111 | sep = "" 112 | insert(sep.join(words)) 113 | 114 | return _fmt 115 | 116 | 117 | def format_text(m): 118 | fmt, words = extract_formatter_and_words(m) 119 | tmp = [] 120 | spaces = True 121 | for i, word in enumerate(words): 122 | word = parse_word(word) 123 | for name in reversed(fmt): 124 | smash, func = formatters[name] 125 | word = func(i, word, i == len(words) - 1) 126 | spaces = spaces and not smash 127 | tmp.append(word) 128 | words = tmp 129 | 130 | sep = " " 131 | if not spaces: 132 | sep = "" 133 | 134 | insert(sep.join(words)) 135 | 136 | 137 | def extract_formatter_and_words(m): 138 | fmt = [] 139 | # noinspection PyProtectedMember 140 | for w in m._words: 141 | # noinspection PyUnresolvedReferences 142 | if isinstance(w, Word) and parse_word(w.word) != "over": 143 | # noinspection PyUnresolvedReferences 144 | fmt.append(w.word) 145 | words = [a for w in parse_words(m) for a in normalize(w).split()] 146 | # print(words) 147 | if not words: 148 | with clip.capture() as s: 149 | press("cmd-c", wait=2000) 150 | try: 151 | words = normalize(s.get()).split() 152 | except clip.NoChange: 153 | words = [] 154 | if not words: 155 | words = [""] 156 | return fmt, words 157 | 158 | 159 | def sponge_format(m): 160 | _, words = extract_formatter_and_words(m) 161 | dictation = " ".join(words) 162 | result = [] 163 | caps = True 164 | for c in dictation: 165 | if c == " ": 166 | result.append(c) 167 | continue 168 | result.append(c.upper() if caps else c.lower()) 169 | caps = not caps 170 | insert("".join(result)) 171 | 172 | 173 | def add_jargon(key, meaning): 174 | global jargon_substitutions, ctx 175 | jargon_substitutions[key] = meaning 176 | resource.write(jargon_path, json.dumps(jargon_substitutions, indent=2)) 177 | v = list(ctx.vocab) 178 | v.append(key) 179 | ctx.vocab = v 180 | 181 | 182 | def learn_jargon(m): 183 | with clip.capture() as s: 184 | press("cmd-c", wait=2000) 185 | meaning = s.get() # type: str 186 | if meaning: 187 | meaning = meaning.strip() 188 | key = " ".join(parse_words(m)) 189 | app.notify(f"learned {key}={meaning}") 190 | add_jargon(key, meaning) 191 | 192 | 193 | ctx = Context("formatters") 194 | ctx.vocab = vocab + list(jargon_substitutions.keys()) 195 | ctx.keymap( 196 | { 197 | f"({' | '.join(formatters)})+ [] [over]": format_text, 198 | "sponge [] [over]": sponge_format, 199 | "create jargon [over]": learn_jargon, 200 | } 201 | ) 202 | -------------------------------------------------------------------------------- /text/homophones.csv: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/dwiel/talon_community/master/text/homophones.csv 2 | Abel,able 3 | Adam,atom 4 | Cain,cane 5 | Chile,chilly,chili 6 | Czech,check 7 | Dane,deign 8 | Finnish,finish 9 | Gail,gale 10 | Hugh,hew,hue 11 | I,aye,eye 12 | Jim,gym 13 | Lapps,laps,lapse 14 | Lou,lieu 15 | Nice,niece 16 | Paul,pall 17 | Pete,peat 18 | Sioux,sue 19 | Wayne,wane,wain 20 | acclamation,acclimation 21 | ad,add 22 | addition,edition 23 | adds,adz,ads 24 | adherents,adherence 25 | ado,adieu 26 | aerial,ariel 27 | affected,effected 28 | afterward,afterword 29 | aid,aide 30 | ale,ail 31 | all,awl 32 | alluded,eluded 33 | allusion,illusion 34 | aloud,allowed 35 | alter,altar 36 | annalist,analyst 37 | apatite,appetite 38 | apprize,apprise 39 | ark,arc 40 | assent,ascent 41 | assistants,assistance 42 | auger,augur 43 | aunt,ant 44 | aural,oral 45 | aureole,oriole 46 | aweigh,away 47 | ax,acts 48 | axis,axes 49 | axle,axel 50 | ayes,eyes 51 | baa,bah 52 | babble,Babel 53 | bade,bad 54 | bait,bate 55 | bald,balled,bawled 56 | bale,bail,baal 57 | band,banned 58 | barred,bard 59 | barren,baron 60 | basal,basil 61 | base,bass 62 | basis,bases 63 | basque,bask 64 | baste,based 65 | bated,baited 66 | bawl,ball 67 | bazaar,bizarre 68 | bear,bare 69 | beat,beet 70 | beau,bow 71 | bee,be 72 | beech,beach 73 | been,bin 74 | beer,bier 75 | bell,belle 76 | bettor,better 77 | bib,bibb 78 | bight,byte,bite 79 | bird,burred 80 | birth,berth 81 | blew,blue 82 | block,bloc 83 | boar,bore 84 | bolder,boulder 85 | bomb,balm,bombe 86 | bootie,booty 87 | border,boarder 88 | bored,board 89 | born,borne 90 | bough,bow 91 | bowed,bode 92 | bowled,bold 93 | brayed,braid 94 | brays,braise 95 | breach,breech 96 | break,brake 97 | bred,bread 98 | brews,bruise 99 | bridal,bridle 100 | brooch,broach 101 | brood,brewed 102 | brows,browse 103 | brut,brute 104 | build,billed 105 | bullion,bouillon 106 | buoy,boy 107 | burger,burgher 108 | burrow,borough,burro 109 | bury,berry 110 | bust,bussed 111 | but,butt 112 | bye,by,buy 113 | caddy,caddie 114 | calender,calendar 115 | callous,callus 116 | canon,cannon 117 | canter,cantor 118 | canvass,canvas 119 | capital,capitol 120 | carrel,carol 121 | carrot,carat,karat,caret 122 | cash,cache 123 | cashed,cached 124 | cast,caste 125 | castor,caster 126 | caws,cause 127 | cede,seed 128 | ceiling,sealing 129 | cellar,seller 130 | censor,sensor 131 | cent,scent,sent 132 | cereal,serial 133 | chance,chants 134 | chaste,chased 135 | chauffeur,shofar 136 | cheap,cheep 137 | chic,sheik 138 | choose,chews 139 | cited,sighted,sided 140 | clack,claque 141 | clammer,clamor,clamber 142 | clause,claws 143 | clew,clue 144 | click,clique 145 | climb,clime 146 | cloze,close,clothes 147 | coal,cole 148 | coarse,course 149 | coat,cote 150 | coax,cokes 151 | collared,collard 152 | complaisant,complacent 153 | complement,compliment 154 | conceited,conceded 155 | consonants,consonance 156 | continence,continents 157 | coo,coup 158 | coolie,coulee 159 | cops,copse 160 | coral,choral 161 | cord,cored,chord 162 | core,corps 163 | coughers,coffers 164 | council,counsel 165 | coupe,coop 166 | courser,coarser 167 | cousin,cozen 168 | cowered,coward 169 | craft,kraft 170 | creek,creak 171 | crepe,crape 172 | crewel,cruel 173 | cruise,crews 174 | cue,queue 175 | current,currant 176 | curser,cursor 177 | cygnet,signet 178 | cymbal,symbol 179 | cypress,Cyprus 180 | dam,damn 181 | days,daze 182 | deer,dear 183 | dense,dents 184 | diffused,defused 185 | discrete,discreet 186 | disperse,disburse 187 | dissent,descent 188 | do,dew,due 189 | doc,dock 190 | doe,do,dough 191 | doze,does 192 | draft,draught 193 | duct,ducked 194 | ducts,ducks 195 | duel,dual 196 | dun,done 197 | dye,die 198 | dyeing,dying 199 | educe,adduce 200 | eek,eke 201 | effect,affect 202 | effects,affects 203 | eight,ate,8 204 | elicit,illicit 205 | elude,allude 206 | elusive,allusive,illusive 207 | emend,amend 208 | enumerable,innumerable 209 | errant,arrant 210 | eve,eave 211 | exceed,accede 212 | except,accept 213 | exercise,exorcise 214 | eyed,I'd 215 | faint,feint 216 | fair,fare 217 | fairy,ferry 218 | faux,foe 219 | fawn,faun 220 | fax,facts 221 | faze,phase 222 | feet,feat 223 | fens,fends 224 | fete,fate 225 | few,phew 226 | fill,Phil 227 | fined,find 228 | fisher,fissure 229 | five,5 230 | flare,flair 231 | flea,flee 232 | floe,flow 233 | flour,flower 234 | flue,flew,flu 235 | flyer,flier 236 | fold,foaled 237 | fore,for,four,4 238 | foregone,forgone 239 | foreword,forward 240 | forte,fort 241 | fourth,forth 242 | fowl,foul 243 | franc,frank 244 | frays,phrase 245 | freeze,frees,frieze 246 | friar,fryer 247 | fur,fir 248 | gaff,gaffe 249 | gait,gate 250 | gambol,gamble 251 | gel,jell 252 | gene,Jean 253 | gibe,jibe 254 | gild,guild 255 | gilder,guilder 256 | gilt,guilt 257 | git,get 258 | gnome,Nome 259 | gofer,gopher 260 | gored,gourd 261 | gorilla,guerilla 262 | gram,graham 263 | graphed,graft 264 | grate,great 265 | grater,greater 266 | grayed,grade 267 | grays,graze 268 | grease,Greece 269 | grill,grille 270 | grizzly,grisly 271 | grown,groan 272 | guest,guessed 273 | hale,hail 274 | hall,haul 275 | halve,have 276 | handmade,handmaid 277 | hanger,hangar 278 | hansom,handsome 279 | hare,hair 280 | hay,hey 281 | haze,hays 282 | heart,hart 283 | heel,heal,he'll 284 | heir,err,air 285 | herd,heard 286 | here,hear 287 | heroin,heroine 288 | he'd,heed 289 | high,hi 290 | hire,higher 291 | hoe,ho 292 | hold,holed 293 | hole,whole 294 | holy,holey,wholly 295 | horde,hoard 296 | horse,hoarse 297 | hose,hoes 298 | hostel,hostile 299 | humerus,humorous 300 | hurts,hertz 301 | hymn,him 302 | idle,idol,idyll 303 | imminent,immanent 304 | impassible,impassable 305 | in,inn 306 | innocence,innocents 307 | insight,incite 308 | instants,instance 309 | intense,intents 310 | isle,I'll,aisle 311 | islet,eyelet 312 | it's,its 313 | jamb,jam 314 | jinks,jinx 315 | kernel,colonel 316 | knap,nap 317 | knave,nave 318 | knew,new,gnu 319 | knit,nit 320 | knock,nock 321 | know,no 322 | koi,coy 323 | kraal,crawl 324 | lacks,lax 325 | ladder,latter 326 | laid,lade 327 | lama,llama 328 | lane,lain 329 | lea,lee 330 | lead,led 331 | leak,leek 332 | lean,lien 333 | leased,least 334 | leech,leach 335 | lei,lay 336 | leis,laze,lays 337 | lens,lends 338 | lesson,lessen 339 | lets,let's 340 | levee,levy 341 | liken,lichen 342 | links,lynx 343 | littoral,literal 344 | loathe,loath 345 | lock,loch 346 | lode,lowed,load 347 | lone,loan 348 | loot,lute 349 | low,lo 350 | lox,lochs,locks 351 | lumber,lumbar 352 | lye,lie 353 | lyre,liar,lier 354 | madder,matter 355 | made,maid 356 | maize,maze 357 | male,mail 358 | mall,maul 359 | mane,Maine,main 360 | manor,manner 361 | mantle,mantel 362 | mare,mayor 363 | mark,marc 364 | martial,marshal 365 | martin,marten 366 | mast,massed 367 | matte,mat 368 | meat,meet,mete 369 | meatier,meteor 370 | medal,metal,mettle 371 | medal,mettle,meddle 372 | merry,marry,Mary 373 | metal,mettle,meddle 374 | mewl,mule 375 | mien,mean 376 | mil,mill 377 | mince,mints 378 | mined,mind 379 | minor,miner 380 | misses,Mrs. 381 | missile,missal 382 | mist,missed 383 | mite,might 384 | mode,mowed 385 | mooed,mood 386 | morn,mourn 387 | mote,moat 388 | mourning,morning 389 | mousse,moose 390 | mown,moan 391 | mucous,mucus 392 | muscle,mussel 393 | muse,mews 394 | must,mussed 395 | mustard,mustered 396 | naval,navel 397 | need,kneed,knead 398 | neigh,nay 399 | nice,gneiss 400 | nickers,knickers 401 | night,knight 402 | nine,9 403 | none,nun 404 | nose,knows 405 | not,knot 406 | oar,or,ore 407 | ode,owed 408 | oh,owe 409 | one,1 410 | our,hour 411 | outcaste,outcast 412 | overdue,overdo 413 | oversees,overseas 414 | paced,paste 415 | pact,packed 416 | pail,pale 417 | pain,pane 418 | palate,palette,pallet 419 | pare,pair,pear 420 | parley,parlay 421 | passed,past 422 | patients,patience 423 | patted,padded 424 | pause,paws 425 | peace,piece 426 | peas,pees 427 | pedal,peddle,petal 428 | pee,pea 429 | peel,peal 430 | peer,pier 431 | pennants,penance 432 | peon,paean,paeon 433 | per,purr 434 | perish,parish 435 | petrol,petrel 436 | pew,pugh 437 | phlox,flocks 438 | pi,pie 439 | picot,pekoe 440 | pigeon,pidgin 441 | pilot,Pilate 442 | pique,peak,peek 443 | pistol,pistil 444 | plait,plate 445 | plane,plain 446 | pleas,please 447 | plum,plumb 448 | pole,poll 449 | pool,pull 450 | pour,pore 451 | praise,prays,preys 452 | precedents,precedence 453 | premier,premiere 454 | presence,presents 455 | prey,pray 456 | pride,pried 457 | primmer,primer 458 | principal,principle 459 | prints,prince 460 | prophet,profit 461 | prose,pros 462 | purl,pearl 463 | purveyed,pervade 464 | quartz,quarts 465 | quints,quince 466 | quire,choir 467 | rabbet,rabbit 468 | rain,reign,rein 469 | rap,wrap 470 | rapped,wrapped,rapt 471 | rays,raise,raze 472 | read,red 473 | read,reed 474 | real,reel 475 | reek,wreak 476 | residence,residents 477 | rest,wrest 478 | retch,wretch 479 | review,revue 480 | rigor,rigger 481 | ring,wring 482 | rite,right,write 483 | roads,rhodes 484 | roam,Rome 485 | roe,row 486 | roll,role 487 | room,rheum 488 | rose,rows 489 | rough,ruff 490 | route,root 491 | roux,rue 492 | rowed,rode,road 493 | rude,rued 494 | rumor,roomer 495 | rung,wrung 496 | rustle,Russell 497 | sac,sack 498 | sacks,sax 499 | sail,sale 500 | sane,seine 501 | sashay,sachet 502 | saver,savor 503 | seller,cellar 504 | scene,seen 505 | scents,cents,sense 506 | scull,skull 507 | sear,seer,sere 508 | see,sea 509 | seeder,cedar 510 | seem,seam 511 | sees,seize,seas 512 | sell,cell 513 | series,Ceres 514 | session,cession 515 | seven,7 516 | sheer,shear 517 | shoo,shoe 518 | shoot,chute 519 | shown,shone 520 | sic,sick 521 | side,sighed 522 | sighs,size 523 | sight,cite,site 524 | sink,sync 525 | six,6 526 | sleigh,slay 527 | sleight,slight 528 | slow,sloe 529 | slue,slough,slew 530 | soared,sword 531 | soled,sold 532 | son,sun 533 | sore,soar 534 | sorry,sari 535 | soul,sole 536 | sow,sew,so 537 | spayed,spade 538 | staid,stayed 539 | stare,stair 540 | stationary,stationery 541 | steak,stake 542 | steal,steel 543 | step,steppe 544 | stile,style 545 | straightened,straitened 546 | strait,straight 547 | suede,swayed 548 | suit,soot 549 | sum,some 550 | summary,summery 551 | sundae,Sunday 552 | surf,serf 553 | surge,serge 554 | sweet,suite 555 | tacks,tax 556 | tact,tacked 557 | tail,tale 558 | taper,tapir 559 | tarry,terry 560 | taupe,tope 561 | taut,taught 562 | tea,tee 563 | team,teem 564 | teas,tease,tees 565 | tear,tare 566 | tens,tends 567 | tents,tense 568 | the,thee 569 | theirs,there's 570 | there,they're,their 571 | three,3 572 | threw,through 573 | throne,thrown 574 | throws,throes 575 | thyme,time 576 | tic,tick 577 | tie,Thai 578 | tied,tide 579 | tier,tear 580 | tigress,Tigris 581 | timbre,timber 582 | to,two,too,2 583 | toe,tow 584 | tolled,told 585 | tool,tulle 586 | torte,tort 587 | tortuous,torturous 588 | towed,toed,toad 589 | tract,tracked 590 | trader,traitor 591 | troop,troupe 592 | trust,trussed 593 | turban,turbine 594 | turn,tern 595 | tutor,Tudor,tooter 596 | tux,tucks 597 | udder,utter 598 | undue,undo 599 | urn,earn 600 | use,yews,ewes 601 | vale,veil 602 | valence,valance 603 | vein,vain,vane 604 | vein,vane 605 | venous,Venus 606 | verses,versus 607 | very,vary 608 | vial,vile 609 | vise,vice 610 | waist,waste 611 | wait,weight 612 | waiter,wader 613 | wale,wail,whale 614 | wares,wears,where's 615 | wave,waive 616 | wax,whacks 617 | way,weigh,whey 618 | we,wee 619 | wear,ware,where 620 | weather,whether,wether 621 | weed,we'd 622 | week,weak 623 | weighed,wade 624 | weighs,ways 625 | wet,whet 626 | we've,weave 627 | whales,wails,Wales 628 | wheel,we'll 629 | which,witch 630 | while,wile 631 | whir,were,we're 632 | whirred,word 633 | whoop,hoop 634 | who's,whose 635 | wine,whine 636 | wined,whined,wind 637 | woe,whoa 638 | wok,walk 639 | won,one 640 | wont,want 641 | wood,would 642 | wore,war 643 | world,whirled,whorled 644 | worn,warn 645 | wrack,rack 646 | wrapper,rapper 647 | wrote,rote 648 | wry,rye 649 | yew,ewe,you 650 | yolk,yoke 651 | yore,your,you're 652 | you'll,Yule 653 | -------------------------------------------------------------------------------- /text/homophones.py: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/dwiel/talon_community/master/text/homophones.py 2 | 3 | import os 4 | 5 | from talon import app, clip, cron 6 | from talon.voice import Context, press 7 | from talon.webview import Webview 8 | 9 | from .. import utils 10 | from ..misc.popups import popup_template, list_template, dict_to_html 11 | 12 | ######################################################################## 13 | # global settings 14 | ######################################################################## 15 | 16 | # a list of homophones where each line is a comma separated list 17 | # e.g. where,wear,ware 18 | # a suitable one can be found here: 19 | # https://github.com/pimentel/homophones 20 | cwd = os.path.dirname(os.path.realpath(__file__)) 21 | homophones_file = os.path.join(cwd, "homophones.csv") 22 | # if quick_replace, then when a word is selected and only one homophone exists, 23 | # replace it without bringing up the options 24 | quick_replace = True 25 | ######################################################################## 26 | 27 | context = Context("homophones") 28 | pick_context = Context("pick") 29 | 30 | phones = {} 31 | canonical = [] 32 | with open(homophones_file, "r") as f: 33 | for h in f: 34 | # Skip comments and empty lines. 35 | if h.startswith("#") or not h.strip(): 36 | continue 37 | h = h.rstrip() 38 | h = h.split(",") 39 | canonical.append(max(h, key=len)) 40 | for w in h: 41 | w = w.lower() 42 | others = phones.get(w, None) 43 | if others is None: 44 | phones[w] = sorted(h) 45 | else: 46 | # if there are multiple hits, collapse them into one list 47 | others += h 48 | others = set(others) 49 | others = sorted(others) 50 | phones[w] = others 51 | 52 | all_homophones = phones 53 | active_word_list = None 54 | is_selection = False 55 | 56 | webview = Webview() 57 | 58 | phones_template = popup_template + list_template("homophones") 59 | 60 | 61 | def close_homophones(): 62 | webview.hide() 63 | pick_context.unload() 64 | 65 | 66 | def make_selection(m, is_selection, transform=lambda x: x): 67 | cron.after("0s", close_homophones) 68 | words = m._words 69 | d = None 70 | if len(words) == 1: 71 | d = int(utils.parse_word(words[0])) 72 | else: 73 | d = int(utils.parse_word(words[1])) 74 | w = active_word_list[d - 1] 75 | if len(words) > 1: 76 | w = transform(w) 77 | if is_selection: 78 | clip.set(w) 79 | press("cmd-v", wait=0) 80 | else: 81 | utils.insert(w) 82 | 83 | 84 | def get_selection(): 85 | with clip.capture() as s: 86 | press("cmd-c", wait=0) 87 | return s.get() 88 | 89 | 90 | def raise_homophones(m, force_raise=False, is_selection=False): 91 | global pick_context 92 | global active_word_list 93 | 94 | if is_selection: 95 | word = get_selection() 96 | word = word.strip() 97 | # elif hasattr(m, "dgndictation"): 98 | # # this mode is currently disabled... 99 | # # experimenting with using a canonical representation and not using 100 | # # dgndictation 101 | # word = str(m.dgndictation[0]._words[0]) 102 | # word = parse_word(word) 103 | elif len(m._words) >= 2: 104 | word = str(m._words[len(m._words) - 1]) 105 | word = utils.parse_word(word) 106 | 107 | word = word.lower() 108 | 109 | if word not in all_homophones: 110 | app.notify("homophones.py", '"%s" not in homophones list' % word) 111 | return 112 | 113 | active_word_list = all_homophones[word] 114 | if ( 115 | is_selection 116 | and len(active_word_list) == 2 117 | and quick_replace 118 | and not force_raise 119 | ): 120 | if word == active_word_list[0].lower(): 121 | new = active_word_list[1] 122 | else: 123 | new = active_word_list[0] 124 | clip.set(new) 125 | press("cmd-v", wait=0) 126 | return 127 | 128 | valid_indices = range(len(active_word_list)) 129 | 130 | webview.render(phones_template, homophones=active_word_list) 131 | webview.show() 132 | 133 | keymap = {"(cancel | 0)": lambda x: close_homophones()} 134 | 135 | def capitalize(x): 136 | return x[0].upper() + x[1:] 137 | 138 | def uppercase(x): 139 | return x.upper() 140 | 141 | def lowercase(x): 142 | return x.lower() 143 | 144 | keymap.update( 145 | { 146 | "[pick] %s" % (i + 1): lambda m: make_selection(m, is_selection) 147 | for i in valid_indices 148 | } 149 | ) 150 | keymap.update( 151 | { 152 | "(ship | title) %s" 153 | % (i + 1): lambda m: make_selection(m, is_selection, capitalize) 154 | for i in valid_indices 155 | } 156 | ) 157 | keymap.update( 158 | { 159 | "(yeller | upper | uppercase) %s" 160 | % (i + 1): lambda m: make_selection(m, is_selection, uppercase) 161 | for i in valid_indices 162 | } 163 | ) 164 | keymap.update( 165 | { 166 | "(lower | lowercase) %s" 167 | % (i + 1): lambda m: make_selection(m, is_selection, lowercase) 168 | for i in valid_indices 169 | } 170 | ) 171 | pick_context.keymap(keymap) 172 | pick_context.load() 173 | 174 | 175 | help_data = { 176 | "phones": "look up homophones for selected text", 177 | "phones [word]": "look up homophones for a given word", 178 | "pick [number]": "make a selection from the homophone list", 179 | "ship [number]": "make a selection and capitalize it", 180 | "yeller [number]": "make a selection and uppercase it", 181 | "lower [number]": "make a selection and lowercase it", 182 | } 183 | 184 | help_template = popup_template + dict_to_html("homophones help", help_data) 185 | 186 | 187 | def homophones_help(m): 188 | webview.render(help_template) 189 | webview.show() 190 | 191 | keymap = {"(cancel | exit)": lambda x: close_homophones()} 192 | pick_context.keymap(keymap) 193 | pick_context.load() 194 | 195 | 196 | context.keymap( 197 | { 198 | "(phones | homophones) help": homophones_help, 199 | "phones {homophones.canonical}": raise_homophones, 200 | "phones": lambda m: raise_homophones(m, is_selection=True), 201 | "force phones {homophones.canonical}": lambda m: raise_homophones( 202 | m, force_raise=True 203 | ), 204 | "force phones": lambda m: raise_homophones( 205 | m, force_raise=True, is_selection=True 206 | ), 207 | } 208 | ) 209 | context.set_list("canonical", canonical) 210 | -------------------------------------------------------------------------------- /text/jargon.json: -------------------------------------------------------------------------------- 1 | { 2 | "integer": "int", 3 | "bite": "byte", 4 | "bites": "bytes", 5 | "constant": "const", 6 | "context": "ctx", 7 | "define": "def", 8 | "dictation": "dgndictation", 9 | "format": "fmt", 10 | "funk": "func", 11 | "jason": "json", 12 | "no": "nil", 13 | "octa": "okta", 14 | "struck": "struct", 15 | "command": "cmd", 16 | "temp": "tmp", 17 | "module": "mod", 18 | "initialized": "init", 19 | "initialize": "init", 20 | "Annette": "init", 21 | "cloud": "cfn", 22 | "llama": "yaml", 23 | "unsigned": "u", 24 | "weight": "wait", 25 | "strength": "string", 26 | "air": "err", 27 | "the attacks": "mutex" 28 | } -------------------------------------------------------------------------------- /text/std.py: -------------------------------------------------------------------------------- 1 | from talon import app, clip, ui 2 | from talon.voice import Context, Key 3 | 4 | from ..utils import delay, sentence_text, text, vocab, word, i, numerals, parse_word, text_to_number, insert, text_with_leading 5 | 6 | 7 | def copy_bundle(_): 8 | bundle = ui.active_app().bundle 9 | clip.set(bundle) 10 | app.notify("Copied app bundle", body="{}".format(bundle)) 11 | 12 | 13 | def type_number(m): 14 | # noinspection PyProtectedMember 15 | count = text_to_number([parse_word(w) for w in m._words[1:]]) 16 | insert(str(count)) 17 | 18 | 19 | ctx = Context("input") 20 | ctx.vocab = vocab 21 | ctx.keymap( 22 | { 23 | # "over": delay(0.3), 24 | "literal ++": text, 25 | "say [over]": text, 26 | "sentence [over]": sentence_text, 27 | # "sentence [over]": sentence_text, # Formatters. 28 | # "comma [over]": [", ", text], 29 | # "period [over]": [". ", text], 30 | # "more [over]": [" ", text], 31 | "more [over]": text_with_leading(" "), 32 | "word ": word, 33 | f"numeral {numerals}": type_number, 34 | "slap": [Key("cmd-right enter")], 35 | "slappy": [Key("cmd-right space")], 36 | "cape": [Key("escape")], 37 | "pa": [Key("space")], 38 | "question [mark]": i("?"), 39 | "tilde": i("~"), 40 | "(bang | exclamation point)": i("!"), 41 | "dollar [sign]": i("$"), 42 | "downscore": i("_"), 43 | "colon": i(":"), 44 | "(paren | left paren)": i("("), 45 | "(rparen | are paren | right paren)": i(")"), 46 | "(brace | left brace)": i("{"), 47 | "(rbrace | are brace | right brace)": i("}"), 48 | # Square Brackets are in basic_keys.py! 49 | # "(square | left square)": "[", 50 | # "(rsquare | are square | right square)": "]", 51 | "(angle | left angle | less than)": i("<"), 52 | "(rangle | are angle | right angle | greater than)": i(">"), 53 | "(star | asterisk)": i("*"), 54 | "(pound | hash [sign] | octo | thorpe | number sign)": i("#"), 55 | "(percent [sign] | modulo)": i("%"), 56 | "caret": i("^"), 57 | "(at sign | arobase)": i("@"), 58 | "ampersand": i("&"), 59 | "pipe": i("|"), 60 | "(dubquote | double quote)": i('"'), 61 | "triple tick": i("'''"), 62 | "triple quote": i('"""'), 63 | # Swipe moved to language.py 64 | #"swipe": [Key("right"), i(", ")], 65 | "item": i(", "), 66 | "value": i(": "), 67 | # "space": " ", # basic_keys.py 68 | "(args | arguments)": ["()", Key("left")], 69 | # "index": ["[]", Key("left")], 70 | "block": [" {}", Key("left enter")], 71 | "empty array": i("[]"), 72 | "(empty dict | empty dictionary)": i("{}"), 73 | "plus": i("+"), 74 | "arrow": i("->"), 75 | # "call": "()", 76 | "indirect": i("&"), 77 | "dereference": i("*"), 78 | "assign": i(" = "), 79 | "[op] set to": i(" := "), 80 | "(minus | subtract)": i(" - "), 81 | "add": i(" + "), 82 | "(times | multiply)": i(" * "), 83 | "(divide | divided by)": i(" / "), 84 | "modulo": i(" % "), 85 | "(minus | subtract) equals": i(" -= "), 86 | "(plus | add) equals": i(" += "), 87 | "(times | multiply) equals": i(" *= "), 88 | "divide equals": i(" /= "), 89 | "(mod | modulo) equals": i(" %= "), 90 | "is greater [than]": i(" > "), 91 | "is less [than]": i(" < "), 92 | "is equal [to]": i(" == "), 93 | "is not [equal] [to]": i(" != "), 94 | "is greater [than] or equal [to]": i(" >= "), 95 | "is less [than] or equal [to] ": i(" <= "), 96 | "to the power of": i(" ** "), 97 | ## Language specific, moved to language.py. 98 | # "logical and": i(" && "), 99 | # "logical or": i(" || "), 100 | "bitwise and": i(" & "), 101 | "bitwise or": i(" | "), 102 | "(piped | alternate)": i(" | "), 103 | "bitwise exclusive or": i(" ^ "), 104 | "[bitwise] left shift": i(" << "), 105 | "[bitwise] right shift": i(" >> "), 106 | "bitwise and equals": i(" &= "), 107 | "bitwise or equals": i(" |= "), 108 | "bitwise exclusive or equals": i(" ^= "), 109 | "[bitwise] left shift equals": i(" <<= "), 110 | "[bitwise] right shift equals": i(" >>= "), 111 | 112 | "[focus] next window": Key("cmd-`"), 113 | "[focus] last window": Key("cmd-shift-`"), 114 | "[focus] next app": Key("cmd-tab"), 115 | "[focus] last app": Key("cmd-shift-tab"), 116 | "[focus] next tab": Key("ctrl-tab"), 117 | "[focus] last tab": Key("ctrl-shift-tab"), 118 | "create tab": Key("cmd-t"), 119 | "create window": Key("cmd-n"), 120 | "undo": Key("cmd-z"), 121 | 122 | # Moved to amethyst.py 123 | # "next space": Key("cmd-alt-ctrl-right"), 124 | # "last space": Key("cmd-alt-ctrl-left"), 125 | "copy active bundle": copy_bundle, 126 | } 127 | ) 128 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import string 5 | import time 6 | import typing as t 7 | 8 | from talon import applescript, resource, cron, ctrl 9 | from talon.voice import Str, press 10 | from talon_plugins import eye_mouse, eye_zoom_mouse, microphone 11 | 12 | ordinal_indexes = { 13 | "first": 0, 14 | "second": 1, 15 | "third": 2, 16 | "fourth": 3, 17 | "fifth": 4, 18 | "sixth": 5, 19 | "seventh": 6, 20 | "eighth": 7, 21 | "ninth": 8, 22 | "tenth": 9, 23 | "final": -1, 24 | "next": "next", # Yeah, yeah, not a number. 25 | "last": "last", 26 | "this": "this", 27 | } 28 | 29 | mapping = { 30 | "semicolon": ";", 31 | "new-line": "\n", 32 | "new-paragraph": "\n\n", 33 | "dot": ".", 34 | "…": "...", 35 | "comma": ",", 36 | "question": "?", 37 | "exclamation": "!", 38 | "dash": "-", 39 | } 40 | punctuation = set(".,-!?") 41 | 42 | try: 43 | vocab_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vocab.json") 44 | with resource.open(vocab_path) as fh: 45 | vocab = json.load(fh) 46 | except FileNotFoundError: 47 | vocab = [] 48 | 49 | 50 | def add_vocab(words): 51 | global vocab 52 | vocab += [re.sub("[^a-zA-Z0-9]+", "", w) for w in words] 53 | vocab = sorted(list(set(vocab))) 54 | with open(vocab_path, "w") as f: 55 | json.dump(vocab, f, indent=0) 56 | 57 | 58 | def parse_word(w): 59 | w = str(w).lstrip("\\").split("\\", 1)[0] 60 | w = mapping.get(w, w) 61 | # w = w.replace("-", "") # hate dragon hyphenation. 62 | return w 63 | 64 | 65 | def parse_words(m): 66 | try: 67 | # noinspection PyProtectedMember 68 | return list(map(parse_word, m.dgndictation[0]._words)) 69 | except AttributeError: 70 | return [] 71 | 72 | 73 | def join_words(words, sep=" "): 74 | out = "" 75 | for i, w in enumerate(words): 76 | if i > 0 and w not in punctuation: 77 | out += sep 78 | out += w 79 | return out 80 | 81 | 82 | last_insert = "" 83 | reenable_job = None 84 | 85 | 86 | def debounce_enable_job(): 87 | global reenable_job 88 | if reenable_job is not None: 89 | cron.cancel(reenable_job) 90 | reenable_job = cron.after("3s", enable_tracking) 91 | 92 | 93 | def enable_tracking(): 94 | if not eye_mouse.control_mouse.enabled: 95 | eye_mouse.control_mouse.toggle() 96 | 97 | 98 | def insert(s): 99 | global last_insert, reenable_job 100 | 101 | last_insert = s 102 | # if eye_zoom_mouse.zoom_mouse.enabled: 103 | # eye_zoom_mouse.zoom_mouse.toggle() 104 | # if eye_mouse.control_mouse.enabled: 105 | # eye_mouse.control_mouse.toggle() 106 | # ctrl.cursor_visible(True) 107 | # if reenable_job is None: 108 | # reenable_job = cron.after("3s", enable_tracking) 109 | # elif reenable_job is not None: 110 | # debounce_enable_job() 111 | Str(s)(None) 112 | 113 | 114 | def i(s): 115 | return lambda _: insert(s) 116 | 117 | 118 | def select_last_insert(_): 119 | for _ in range(len(last_insert)): 120 | press("left") 121 | for _ in range(len(last_insert)): 122 | press("shift-right") 123 | 124 | 125 | def text(m): 126 | insert(join_words(parse_words(m))) 127 | # Add a universal fudge factor. 128 | time.sleep(0.2) 129 | 130 | def sentence_text(m): 131 | words = parse_words(m) 132 | words[0] = str(words[0]).capitalize() 133 | insert(join_words(words)) 134 | # Add a universal fudge factor. 135 | time.sleep(0.2) 136 | 137 | 138 | 139 | def list_value(l, index=0): 140 | def _val(m): 141 | insert(m[l][index]) 142 | 143 | return _val 144 | 145 | 146 | def text_with_trailing_space(m): 147 | insert(join_words(parse_words(m)) + " ") 148 | 149 | 150 | def text_with_leading_space(m): 151 | insert(" " + join_words(parse_words(m))) 152 | 153 | 154 | def text_with_leading(leading): 155 | return lambda m: insert(leading + join_words(parse_words(m))) 156 | 157 | 158 | def word(m): 159 | try: 160 | # noinspection PyProtectedMember 161 | insert(join_words(list(map(parse_word, m.dgnwords[0]._words)))) 162 | # Universal fudge factor. 163 | time.sleep(0.2) 164 | except AttributeError: 165 | pass 166 | 167 | 168 | def surround(by): 169 | def func(i, w, last): 170 | if i == 0: 171 | w = by + w 172 | if last: 173 | w += by 174 | return w 175 | 176 | return func 177 | 178 | 179 | def rot13(_, w, __): 180 | out = "" 181 | for c in w.lower(): 182 | if c in string.ascii_lowercase: 183 | c = chr((((ord(c) - ord("a")) + 13) % 26) + ord("a")) 184 | out += c 185 | return out 186 | 187 | 188 | numeral_map = dict((str(n), n) for n in range(0, 20)) 189 | for n in range(20, 101, 10): 190 | numeral_map[str(n)] = n 191 | for n in range(100, 1001, 100): 192 | numeral_map[str(n)] = n 193 | for n in range(1000, 10001, 1000): 194 | numeral_map[str(n)] = n 195 | numeral_map["oh"] = 0 # synonym for zero 196 | numeral_map["and"] = None # drop me 197 | 198 | numerals = " (" + " | ".join(sorted(numeral_map.keys())) + ")+" 199 | optional_numerals = " (" + " | ".join(sorted(numeral_map.keys())) + ")*" 200 | 201 | 202 | def text_to_number(words): 203 | words = [parse_word(w).lower() for w in words] 204 | 205 | result = 0 206 | factor = 1 207 | for i, w in enumerate(reversed(words)): 208 | print(f"{i} {result} {factor} {w}") 209 | if w not in numerals: 210 | raise Exception("not a number: {}".format(words)) 211 | 212 | number = numeral_map[w] 213 | if number is None: 214 | continue 215 | number = int(number) 216 | print(f"{i} {result} {factor} {w} {number}") 217 | if number > factor and number % factor == 0: 218 | result = result + number 219 | else: 220 | result = result + factor * number 221 | if i != 0: 222 | factor = (10 ** max(1, len(str(number).rstrip("0")))) * factor 223 | else: 224 | factor = (10 ** max(1, len(str(number)))) * factor 225 | return result 226 | 227 | 228 | def text_to_range(words, delimiter="until"): 229 | tmp = [str(s).lower() for s in words] 230 | split = tmp.index(delimiter) 231 | start = text_to_number(words[:split]) 232 | end = text_to_number(words[split + 1:]) 233 | return start, end 234 | 235 | 236 | def delay(amount): 237 | return lambda _: time.sleep(amount) 238 | 239 | 240 | def use_mic(mic_name): 241 | mic = microphone.manager.active_mic() 242 | if mic is not None and mic.name == mic_name: 243 | return 244 | # noinspection PyUnresolvedReferences 245 | mics = {i.name: i for i in list(microphone.manager.menu.items)} 246 | if mic_name in mics: 247 | microphone.manager.menu_click(mics[mic_name]) 248 | 249 | def mic_uses_volume(settings: t.Dict[str, int]): 250 | mics = {i.name: i for i in list(microphone.manager.menu.items)} 251 | for m in settings: 252 | if m in mics: 253 | set_input_volume(settings[m]) 254 | 255 | 256 | def set_input_volume(m: int): 257 | applescript.run(f"set volume input volume {m}") --------------------------------------------------------------------------------