dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

commit 2be655570d88cc41c7974224a05cc6aa94e420a8
parent 3e2607914a6ab32ac85a65fc5c84b66b9a4f8a80
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Tue,  8 Jun 2021 14:10:54 +0200

talon: more configuration

Diffstat:
Atalon-user/amethyst.talon | 13+++++++++++++
Dtalon-user/caffeinate.talon | 2--
Atalon-user/code/Resources/HiddenCursor.cur | 0
Atalon-user/code/abbreviate.py | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/app_names/app_name_overrides.linux.csv | 3+++
Atalon-user/code/app_names/app_name_overrides.mac.csv | 4++++
Atalon-user/code/app_names/app_name_overrides.windows.csv | 9+++++++++
Atalon-user/code/app_running.py | 12++++++++++++
Atalon-user/code/application_matches.py | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/code.py | 510+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/debugger.py | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/delayed_speech_off.py | 30++++++++++++++++++++++++++++++
Atalon-user/code/dictation.py | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/edit.py | 28++++++++++++++++++++++++++++
Atalon-user/code/engine.py | 19+++++++++++++++++++
Atalon-user/code/exec.py | 21+++++++++++++++++++++
Atalon-user/code/file_manager.py | 427+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/find_and_replace.py | 47+++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/formatters.py | 298+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rtalon-user/help.py -> talon-user/code/help.py | 0
Rtalon-user/history.py -> talon-user/code/history.py | 0
Atalon-user/code/homophones.csv | 648+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/homophones.py | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/keys.py | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/line_commands.py | 46++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/macro.py | 44++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/messaging.py | 42++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/microphone_selection.py | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/mouse.py | 392+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/multiple_cursors.py | 32++++++++++++++++++++++++++++++++
Rtalon-user/numbers.py -> talon-user/code/numbers.py | 0
Atalon-user/code/ordinals.py | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/phrase_history.py | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/screenshot.py | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/search_engines.py | 34++++++++++++++++++++++++++++++++++
Atalon-user/code/snippet_watcher.py | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/snippets.py | 41+++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/splits.py | 46++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/sql.py | 0
Atalon-user/code/switcher.py | 383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/tabs.py | 11+++++++++++
Atalon-user/code/tags.py | 17+++++++++++++++++
Atalon-user/code/talon_helpers.py | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/user_settings.py | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/vocabulary.py | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/code/window_snap.py | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtalon-user/exec.py | 17-----------------
Atalon-user/eye_tracking_settings.py | 11+++++++++++
Atalon-user/mac-edit.talon | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/mac.talon | 40++++++++++++++++++++++++++++++++++++++++
Atalon-user/misc/abbreviate.talon | 2++
Atalon-user/misc/extensions.talon | 15+++++++++++++++
Atalon-user/misc/formatters.talon | 16++++++++++++++++
Atalon-user/misc/git.talon | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/misc/git_add_patch.talon | 19+++++++++++++++++++
Rtalon-user/help.talon -> talon-user/misc/help.talon | 0
Atalon-user/misc/help_open.talon | 8++++++++
Rtalon-user/history.talon -> talon-user/misc/history.talon | 0
Atalon-user/misc/keys.talon | 9+++++++++
Atalon-user/misc/macro.talon | 4++++
Atalon-user/misc/media.talon | 7+++++++
Atalon-user/misc/messaging.talon | 17+++++++++++++++++
Atalon-user/misc/microphone_selection.talon | 3+++
Atalon-user/misc/mouse.talon | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/misc/multiple_cursors.talon | 10++++++++++
Atalon-user/misc/repeater.talon | 4++++
Atalon-user/misc/screenshot.talon | 6++++++
Atalon-user/misc/search_engines.talon | 5+++++
Atalon-user/misc/splits.talon | 15+++++++++++++++
Atalon-user/misc/standard.talon | 31+++++++++++++++++++++++++++++++
Atalon-user/misc/tabs.talon | 9+++++++++
Atalon-user/misc/talon_helpers.talon | 14++++++++++++++
Atalon-user/misc/toggles.talon | 2++
Atalon-user/misc/window_management.talon | 16++++++++++++++++
Atalon-user/modes/dictation_mode.talon | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/modes/dragon_modes.talon | 9+++++++++
Atalon-user/modes/language_modes.talon | 17+++++++++++++++++
Atalon-user/modes/modes.py | 47+++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/modes/modes.talon | 13+++++++++++++
Atalon-user/modes/sleep_mode.talon | 8++++++++
Atalon-user/modes/sleep_mode_wav2letter.talon | 7+++++++
Atalon-user/modes/wake_up.talon | 22++++++++++++++++++++++
Atalon-user/mouse_grid/mouse_grid.py | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/mouse_grid/mouse_grid.talon | 17+++++++++++++++++
Atalon-user/mouse_grid/mouse_grid_open.talon | 12++++++++++++
Mtalon-user/music.talon | 2+-
Atalon-user/power.talon | 7+++++++
Mtalon-user/settings.talon | 30++++++++++++++++++++++++++----
Atalon-user/shared_settings_module.py | 9+++++++++
Atalon-user/talon_draft_window/LICENSE | 21+++++++++++++++++++++
Atalon-user/talon_draft_window/README.md | 33+++++++++++++++++++++++++++++++++
Atalon-user/talon_draft_window/doc/talon-draft-demo.gif | 0
Atalon-user/talon_draft_window/draft_global.talon | 38++++++++++++++++++++++++++++++++++++++
Atalon-user/talon_draft_window/draft_talon_helpers.py | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/talon_draft_window/draft_ui.py | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/talon_draft_window/draft_window.talon | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/talon_draft_window/draft_window_open.talon | 12++++++++++++
Atalon-user/talon_draft_window/settings.talon.example | 8++++++++
Atalon-user/talon_draft_window/test_draft_ui.py | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/termapps.talon | 1+
Atalon-user/text/find_and_replace.talon | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/text/homophones.talon | 5+++++
Atalon-user/text/homophones_open.talon | 11+++++++++++
Atalon-user/text/line_commands.talon | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/text/numbers.talon | 3+++
Atalon-user/text/symbols.talon | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/text/text_navigation.py | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/text/text_navigation.talon | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atalon-user/weather.talon | 5+++++
109 files changed, 7812 insertions(+), 24 deletions(-)

diff --git a/talon-user/amethyst.talon b/talon-user/amethyst.talon @@ -0,0 +1,12 @@ +user.running: amethyst +- +window next: key("alt-shift-j") +window previous: key("alt-shift-k") +# window move desk: key("ctrl-alt-shift-h") +window full: key("alt-shift-f") +window tall: key("alt-shift-a") +window wide: key("alt-shift-s") +window move main: key("alt-shift-enter") +window grow: key("alt-shift-l") +window shrink: key("alt-shift-h") +window reevaluate: key("alt-shift-z")+ \ No newline at end of file diff --git a/talon-user/caffeinate.talon b/talon-user/caffeinate.talon @@ -1,2 +0,0 @@ -caffeinate start: user.system_command('killall caffeinate; setsid -f caffeinate -d && notify "Caffeinated" "Sleep disabled" talon') -caffeinate stop: user.system_command('killall caffeinate && notify "Sleepy" "Will sleep again" talon') diff --git a/talon-user/code/Resources/HiddenCursor.cur b/talon-user/code/Resources/HiddenCursor.cur Binary files differ. diff --git a/talon-user/code/abbreviate.py b/talon-user/code/abbreviate.py @@ -0,0 +1,217 @@ +# XXX - would be nice to be able pipe these through formatters + +from talon import Context, Module + +mod = Module() +mod.list("abbreviation", desc="Common abbreviation") + + +ctx = Context() +ctx.lists["user.abbreviation"] = { + "address": "addr", + "administrator": "admin", + "administrators": "admins", + "advance": "adv", + "advanced": "adv", + "alberta": "ab", + "alternative": "alt", + "application": "app", + "applications": "apps", + "argument": "arg", + "arguments": "args", + "as far as i can tell": "afaict", + "as far as i know": "afaik", + "assembly": "asm", + "at the moment": "atm", + "attribute": "attr", + "attributes": "attrs", + "authenticate": "auth", + "authentication": "auth", + "away from keyboard": "afk", + "binary": "bin", + "boolean": "bool", + "british columbia": "bc", + "button": "btn", + "canada": "ca", + "centimeter": "cm", + "char": "chr", + "character": "char", + "class": "cls", + "client": "cli", + "command": "cmd", + "comment": "cmt", + "compare": "cmp", + "conference": "conf", + "config": "cfg", + "configuration": "cfg", + "context": "ctx", + "control": "ctrl", + "constant": "const", + "coordinate": "coord", + "coordinates": "coords", + "copy": "cpy", + "count": "cnt", + "counter": "ctr", + "database": "db", + "declare": "decl", + "declaration": "decl", + "decode": "dec", + "decrement": "dec", + "debug": "dbg", + "define": "def", + "definition": "def", + "description": "desc", + "develop": "dev", + "development": "dev", + "device": "dev", + "dictation": "dict", + "dictionary": "dict", + "direction": "dir", + "directory": "dir", + "distribution": "dist", + "document": "doc", + "documents": "docs", + "double": "dbl", + "dupe": "dup", + "duplicate": "dup", + "dynamic": "dyn", + "encode": "enc", + "entry": "ent", + "enumerate": "enum", + "environment": "env", + "escape": "esc", + "etcetera": "etc", + "example": "ex", + "exception": "exc", + "execute": "exec", + "expression": "exp", + "extend": "ext", + "extension": "ext", + "file system": "fs", + "framework": "fw", + "function": "func", + "funny": "lol", + "generic": "gen", + "generate": "gen", + "hypertext": "http", + "history": "hist", + "image": "img", + "import table": "iat", + "import address table": "iat", + "increment": "inc", + "information": "info", + "initialize": "init", + "initializer": "init", + "in real life": "irl", + "instance": "inst", + "integer": "int", + "interrupt": "int", + "iterate": "iter", + "java archive": "jar", + "javascript": "js", + "jason": "json", + "jump": "jmp", + "keyboard": "kbd", + "keyword arguments": "kwargs", + "keyword": "kw", + "kilogram": "kg", + "kilometer": "km", + "language": "lng", + "length": "len", + "library": "lib", + "manitoba": "mb", + "markdown": "md", + "message": "msg", + "meta sploit": "msf", + "meta sploit framework": "msf", + "microphone": "mic", + "milligram": "mg", + "millisecond": "ms", + "miscellaneous": "misc", + "module": "mod", + "mount": "mnt", + "nano second": "ns", + "neo vim": "nvim", + "new brunswick": "nb", + "nova scotia": "ns", + "number": "num", + "object": "obj", + "okay": "ok", + "ontario": "on", + "option": "opt", + "operating system": "os", + "original": "orig", + "package": "pkg", + "parameter": "param", + "parameters": "params", + "pico second": "ps", + "pixel": "px", + "point": "pt", + "pointer": "ptr", + "position": "pos", + "position independent code": "pic", + "position independent executable": "pie", + "previous": "prev", + "property": "prop", + "public": "pub", + "python": "py", + "quebec": "qc", + "query string": "qs", + "random": "rnd", + "receipt": "rcpt", + "reference": "ref", + "references": "refs", + "register": "reg", + "registery": "reg", + "regular expression": "regex", + "regular expressions": "regex", + "repel": "repl", + "represent": "repr", + "representation": "repr", + "request": "req", + "return": "ret", + "revision": "rev", + "ruby": "rb", + "saskatchewan": "sk", + "service pack": "sp", + "session id": "sid", + "shell": "sh", + "shellcode": "sc", + "source": "src", + "special": "spec", + "specific": "spec", + "specification": "spec", + "specify": "spec", + "standard in": "stdin", + "standard out": "stdout", + "standard": "std", + "string": "str", + "structure": "struct", + "synchronize": "sync", + "synchronous": "sync", + "system": "sys", + "table of contents": "toc", + "table": "tbl", + "taiwan": "tw", + "technology": "tech", + "temperature": "temp", + "temporary": "tmp", + "temp": "tmp", + "text": "txt", + "time of check time of use": "toctou", + "token": "tok", + "ultimate": "ulti", + "unique id": "uuid", + "user": "usr", + "utilities": "utils", + "utility": "util", + "value": "val", + "variable": "var", + "verify": "vrfy", + "versus": "vs", + "visual": "vis", + "visual studio": "msvc", + "web": "www", + "what the fuck": "wtf", + "window": "win", +} diff --git a/talon-user/code/app_names/app_name_overrides.linux.csv b/talon-user/code/app_names/app_name_overrides.linux.csv @@ -0,0 +1,2 @@ +grip, DataGrip +py, jetbrains-pycharm-ce+ \ No newline at end of file diff --git a/talon-user/code/app_names/app_name_overrides.mac.csv b/talon-user/code/app_names/app_name_overrides.mac.csv @@ -0,0 +1,3 @@ +grip, DataGrip +term, iTerm2 +one note, ONENOTE+ \ No newline at end of file diff --git a/talon-user/code/app_names/app_name_overrides.windows.csv b/talon-user/code/app_names/app_name_overrides.windows.csv @@ -0,0 +1,8 @@ +grip, DataGrip +term, iTerm2 +one note, ONENOTE +lock, slack.exe +app, slack.exe +lockapp, slack.exe +pycharm, pycharm64.exe +webstorm, webstorm64.exe+ \ No newline at end of file diff --git a/talon-user/code/app_running.py b/talon-user/code/app_running.py @@ -0,0 +1,12 @@ +from talon import Module, ui + +mod = Module() + + +@mod.scope +def scope(): + return {"running": {app.name.lower() for app in ui.apps()}} + + +ui.register("app_launch", scope.update) +ui.register("app_close", scope.update) diff --git a/talon-user/code/application_matches.py b/talon-user/code/application_matches.py @@ -0,0 +1,78 @@ +from talon import Context, Module + +mod = Module() + + +apps = mod.apps + +# apple specific apps +apps.datagrip = """ +os: mac +and app.name: DataGrip +""" + +apps.finder = """ +os: mac +and app.bundle: com.apple.finder +""" + +apps.rstudio = """ +os: mac +and app.name: RStudio +""" + +apps.apple_terminal = """ +os: mac +and app.bundle: com.apple.Terminal +""" + +apps.iterm2 = """ +os: mac +and app.bundle: com.googlecode.iterm2 +""" + +# linux specific apps +apps.keepass = """ +os: linux +and app.name: KeePassX2 +os: linux +and app.name: KeePassXC +os: linux +and app.name: KeepassX2 +os: linux +and app.name: keepassx2 +os: linux +and app.name: keepassxc +os: linux +and app.name: Keepassxc""" + +apps.signal = """ +os: linux +and app.name: Signal + +os: linux +and app.name: signal +""" + +apps.termite = """ +os: linux +and app.name: /termite/ +""" + +apps.windows_command_processor = """ +os: windows +and app.name: Windows Command Processor +os: windows +and app.exe: cmd.exe +""" + +apps.windows_terminal = """ +os: windows +and app.exe: WindowsTerminal.exe +""" + +mod.apps.windows_power_shell = """ +os: windows +and app.exe: powershell.exe +""" + diff --git a/talon-user/code/code.py b/talon-user/code/code.py @@ -0,0 +1,510 @@ +from talon import Context, Module, actions, app, imgui, registry, settings + +ctx = Context() +mod = Module() +mod.list("code_functions", desc="List of functions for active language") +mod.list("code_types", desc="List of types for active language") +mod.list("code_libraries", desc="List of libraries for active language") + +setting_private_function_formatter = mod.setting("code_private_function_formatter", str) +setting_protected_function_formatter = mod.setting( + "code_protected_function_formatter", str +) +setting_public_function_formatter = mod.setting("code_public_function_formatter", str) +setting_private_variable_formatter = mod.setting("code_private_variable_formatter", str) +setting_protected_variable_formatter = mod.setting( + "code_protected_variable_formatter", str +) +setting_public_variable_formatter = mod.setting("code_public_variable_formatter", str) + +mod.tag("code_comment", desc="Tag for enabling generic comment commands") +mod.tag("code_block_comment", desc="Tag for enabling generic block comment commands") +mod.tag("code_operators", desc="Tag for enabling generic operator commands") +mod.tag( + "code_generic", + desc="Tag for enabling other basic programming commands (loops, functions, etc)", +) + +key = actions.key +function_list = [] +library_list = [] +extension_lang_map = { + ".asm": "assembly", + ".bat": "batch", + ".c": "c", + ".cmake": "cmake", + ".cpp": "cplusplus", + ".cs": "csharp", + ".gdb": "gdb", + ".go": "go", + ".h": "c", + ".hpp": "cplusplus", + ".java": "java", + ".js": "javascript", + ".jsx": "javascript", + ".json": "json", + ".lua": "lua", + ".md": "markdown", + ".pl": "perl", + ".ps1": "powershell", + ".py": "python", + ".r": "r", + ".rb": "ruby", + ".s": "assembly", + ".sh": "bash", + ".snippets": "snippets", + ".talon": "talon", + ".ts": "typescript", + ".tsx": "typescript", + ".vba": "vba", + ".vim": "vimscript", + ".vimrc": "vimscript", +} + +# flag indicates whether or not the title tracking is enabled +forced_language = False + + +@mod.capture(rule="{user.code_functions}") +def code_functions(m) -> str: + """Returns a function name""" + return m.code_functions + + +@mod.capture(rule="{user.code_types}") +def code_types(m) -> str: + """Returns a type""" + return m.code_types + + +@mod.capture(rule="{user.code_libraries}") +def code_libraries(m) -> str: + """Returns a type""" + return m.code_libraries + + +@ctx.action_class("code") +class code_actions: + def language(): + result = "" + if not forced_language: + file_extension = actions.win.file_ext() + + if file_extension and file_extension in extension_lang_map: + result = extension_lang_map[file_extension] + + # print("code.language: " + result) + return result + + +# create a mode for each defined language +for __, lang in extension_lang_map.items(): + mod.mode(lang) + + +@mod.action_class +class Actions: + def code_set_language_mode(language: str): + """Sets the active language mode, and disables extension matching""" + global forced_language + actions.user.code_clear_language_mode() + actions.mode.enable("user.{}".format(language)) + # app.notify("Enabled {} mode".format(language)) + forced_language = True + + def code_clear_language_mode(): + """Clears the active language mode, and re-enables code.language: extension matching""" + global forced_language + forced_language = False + + for __, lang in extension_lang_map.items(): + actions.mode.disable("user.{}".format(lang)) + # app.notify("Cleared language modes") + + def code_operator_indirection(): + """code_operator_indirection""" + + def code_operator_address_of(): + """code_operator_address_of (e.g., C++ & op)""" + + def code_operator_structure_dereference(): + """code_operator_structure_dereference (e.g., C++ -> op)""" + + def code_operator_lambda(): + """code_operator_lambda""" + + def code_operator_subscript(): + """code_operator_subscript (e.g., C++ [])""" + + def code_operator_assignment(): + """code_operator_assignment""" + + def code_operator_subtraction(): + """code_operator_subtraction""" + + def code_operator_subtraction_assignment(): + """code_operator_subtraction_equals""" + + def code_operator_addition(): + """code_operator_addition""" + + def code_operator_addition_assignment(): + """code_operator_addition_assignment""" + + def code_operator_multiplication(): + """code_operator_multiplication""" + + def code_operator_multiplication_assignment(): + """code_operator_multiplication_assignment""" + + def code_operator_exponent(): + """code_operator_exponent""" + + def code_operator_division(): + """code_operator_division""" + + def code_operator_division_assignment(): + """code_operator_division_assignment""" + + def code_operator_modulo(): + """code_operator_modulo""" + + def code_operator_modulo_assignment(): + """code_operator_modulo_assignment""" + + def code_operator_equal(): + """code_operator_equal""" + + def code_operator_not_equal(): + """code_operator_not_equal""" + + def code_operator_greater_than(): + """code_operator_greater_than""" + + def code_operator_greater_than_or_equal_to(): + """code_operator_greater_than_or_equal_to""" + + def code_operator_less_than(): + """code_operator_less_than""" + + def code_operator_less_than_or_equal_to(): + """code_operator_less_than_or_equal_to""" + + def code_operator_in(): + """code_operator_less_than_or_equal_to""" + + def code_operator_and(): + """codee_operator_and""" + + def code_operator_or(): + """code_operator_or""" + + def code_operator_bitwise_and(): + """code_operator_bitwise_and""" + + def code_operator_bitwise_and_assignment(): + """code_operator_and""" + + def code_operator_bitwise_or(): + """code_operator_bitwise_or""" + + def code_operator_bitwise_or_assignment(): + """code_operator_or_assignment""" + + def code_operator_bitwise_exclusive_or(): + """code_operator_bitwise_exclusive_or""" + + def code_operator_bitwise_exclusive_or_assignment(): + """code_operator_bitwise_exclusive_or_assignment""" + + def code_operator_bitwise_left_shift(): + """code_operator_bitwise_left_shift""" + + def code_operator_bitwise_left_shift_assignment(): + """code_operator_bitwise_left_shift_assigment""" + + def code_operator_bitwise_right_shift(): + """code_operator_bitwise_right_shift""" + + def code_operator_bitwise_right_shift_assignment(): + """code_operator_bitwise_right_shift_assignment""" + + def code_block(): + """Inserts equivalent of {\n} for the active language, and places the cursor appropriately""" + + def code_self(): + """Inserts the equivalent of "this" in C++ or self in python""" + + def code_null(): + """inserts null equivalent""" + + def code_is_null(): + """inserts check for == null""" + + def code_is_not_null(): + """inserts check for == null""" + + def code_state_in(): + """Inserts python "in" equivalent""" + + def code_state_if(): + """Inserts if statement""" + + def code_state_else_if(): + """Inserts else if statement""" + + def code_state_else(): + """Inserts else statement""" + + def code_state_do(): + """Inserts do statement""" + + def code_state_switch(): + """Inserts switch statement""" + + def code_state_case(): + """Inserts case statement""" + + def code_state_for(): + """Inserts for statement""" + + def code_state_for_each(): + """Inserts for each equivalent statement""" + + def code_state_go_to(): + """inserts go-to statement""" + + def code_state_while(): + """Inserts while statement""" + + def code_state_return(): + """Inserts return statement""" + + def code_break(): + """Inserts break statement""" + + def code_next(): + """Inserts next statement""" + + def code_true(): + """Insert True value""" + + def code_false(): + """Insert False value""" + + def code_try_catch(): + """Inserts try/catch. If selection is true, does so around the selecion""" + + def code_default_function(text: str): + """Inserts function declaration""" + actions.user.code_private_function(text) + + def code_private_function(text: str): + """Inserts private function declaration""" + + def code_private_static_function(text: str): + """Inserts private static function""" + + def code_protected_function(text: str): + """Inserts protected function declaration""" + + def code_protected_static_function(text: str): + """Inserts public function""" + + def code_public_function(text: str): + """Inserts public function""" + + def code_public_static_function(text: str): + """Inserts public function""" + + def code_private_function_formatter(name: str): + """Inserts private function name with formatter""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_private_function_formatter") + ) + ) + + def code_protected_function_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_protected_function_formatter") + ) + ) + + def code_public_function_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_public_function_formatter") + ) + ) + + def code_private_variable_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_private_variable_formatter") + ) + ) + + def code_protected_variable_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_protected_variable_formatter") + ) + ) + + def code_public_variable_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_public_variable_formatter") + ) + ) + + def code_comment(): + """Inserts comment at current cursor location""" + + def code_block_comment(): + """Block comment""" + + def code_block_comment_prefix(): + """Block comment start syntax""" + + def code_block_comment_suffix(): + """Block comment end syntax""" + + def code_type_definition(): + """code_type_definition (typedef)""" + + def code_typedef_struct(): + """code_typedef_struct (typedef)""" + + def code_type_class(): + """code_type_class""" + + def code_type_struct(): + """code_type_struct""" + + def code_include(): + """code_include""" + + def code_include_system(): + """code_include_system""" + + def code_include_local(): + """code_include_local""" + + def code_import(): + """import/using equivalent""" + + def code_from_import(): + """from import python equivalent""" + + def code_toggle_functions(): + """GUI: List functions for active language""" + global function_list + if gui_libraries.showing: + gui_libraries.hide() + if gui_functions.showing: + function_list = [] + gui_functions.hide() + else: + update_function_list_and_freeze() + + def code_select_function(number: int, selection: str): + """Inserts the selected function when the imgui is open""" + if gui_functions.showing and number < len(function_list): + actions.user.code_insert_function( + registry.lists["user.code_functions"][0][function_list[number]], + selection, + ) + + def code_insert_function(text: str, selection: str): + """Inserts a function and positions the cursor appropriately""" + + def code_toggle_libraries(): + """GUI: List libraries for active language""" + global library_list + if gui_functions.showing: + gui_functions.hide() + if gui_libraries.showing: + library_list = [] + gui_libraries.hide() + else: + update_library_list_and_freeze() + + def code_select_library(number: int, selection: str): + """Inserts the selected library when the imgui is open""" + if gui_libraries.showing and number < len(library_list): + actions.user.code_insert_library( + registry.lists["user.code_libraries"][0][library_list[number]], + selection, + ) + + def code_insert_library(text: str, selection: str): + """Inserts a library and positions the cursor appropriately""" + + def code_document_string(): + """Inserts a document string and positions the cursor appropriately""" + + +def update_library_list_and_freeze(): + global library_list + if "user.code_libraries" in registry.lists: + library_list = sorted(registry.lists["user.code_libraries"][0].keys()) + else: + library_list = [] + + gui_libraries.show() + + +def update_function_list_and_freeze(): + global function_list + if "user.code_functions" in registry.lists: + function_list = sorted(registry.lists["user.code_functions"][0].keys()) + else: + function_list = [] + + gui_functions.show() + + +@imgui.open() +def gui_functions(gui: imgui.GUI): + gui.text("Functions") + gui.line() + + # print(str(registry.lists["user.code_functions"])) + for i, entry in enumerate(function_list, 1): + if entry in registry.lists["user.code_functions"][0]: + gui.text( + "{}. {}: {}".format( + i, entry, registry.lists["user.code_functions"][0][entry] + ) + ) + + +@imgui.open() +def gui_libraries(gui: imgui.GUI): + gui.text("Libraries") + gui.line() + + for i, entry in enumerate(library_list, 1): + gui.text( + "{}. {}: {}".format( + i, entry, registry.lists["user.code_libraries"][0][entry] + ) + ) + + +def commands_updated(_): + if gui_functions.showing: + update_function_list_and_freeze() + if gui_libraries.showing: + update_library_list_and_freeze() + + +registry.register("update_commands", commands_updated) diff --git a/talon-user/code/debugger.py b/talon-user/code/debugger.py @@ -0,0 +1,198 @@ +# XXX - execute until line number/cursor +# XXX - more memory printing he thumping + +from talon import Context, Module + +mod = Module() +mod.tag("debugger", desc="Tag for enabling generic debugger commands") + +ctx = Context() +ctx.matches = r""" +tag: debugger +""" + +x86_registers = { + "air": "eax", + "bat": "ebx", + "cap": "ecx", + "drum": "edx", + "source": "esi", + "dest": "edi", + "stack": "esp", + "frame": "ebp", + "instruction": "eip", +} + +x64_registers = { + # general purpose + "air": "rax", + "racks": "rax", + "bat": "rbx", + "cap": "rcx", + "drum": "rdx", + "source": "rsi", + "dest": "rdi", + "stack": "rsp", + "stack pointer": "rsp", + "frame": "rbp", + "frame pointer": "rbp", + "base": "rbp", + "base pointer": "rbp", + "eight": "r8", + "nine": "r9", + "ten": "r10", + "eleven": "r11", + "twelve": "r12", + "thirteen": "r13", + "fourteen": "r14", + "fifteen": "r15", + # pointers + "instruction": "rip", + "rip": "rip", + # segment +} + +# XXX - pass by windbg to dump +windows_x64_register_parameters = ["rcx", "rdx", "r8", "r9"] + +# XXX - make this dynamic +ctx.lists["self.registers"] = x64_registers + +# assembly_languages = { +# "x86": x86_registers, +# "x64": x64_registers, +# } + +mod.list("registers", desc="Main architecture register set") + + +@mod.capture(rule="{self.registers}") +def registers(m) -> str: + "Returns a register" + return m.registers + + +@mod.action_class +class Actions: + def debugger_step_into(): + """Step into an instruction in the debugger""" + + def debugger_step_over(): + """Step over an instruction in the debugger""" + + def debugger_step_line(): + """Step into a source line in the debugger""" + + def debugger_step_over_line(): + """Step over a source line in the debugger""" + + def debugger_step_out(): + """Step until function exit in the debugger""" + + def debugger_continue(): + """Continue execution in the debugger""" + + def debugger_restart(): + """Restart execution in the debugger""" + + def debugger_start(): + """Start debugging""" + + def debugger_stop(): + """Stop the debugger""" + + def debugger_exit(): + """Exit the debugger""" + + def debugger_detach(): + """Detach the debugger""" + + def debugger_backtrace(): + """Print a back trace in the debugger""" + + def debugger_get_register(): + """Print specific register in the debugger""" + + def debugger_set_register(): + """Set specific register in the debugger""" + + def debugger_show_registers(): + """Print the current registers in the debugger""" + + def debugger_break_now(): + """Break into the debugger""" + + def debugger_break_here(): + """Set a break on the current line""" + + def debugger_show_breakpoints(): + """Print the current breakpoints in the debugger""" + + def debugger_add_sw_breakpoint(): + """Add one software breakpoint in the debugger""" + + def debugger_add_hw_breakpoint(): + """Add one hardware breakpoint in the debugger""" + + def debugger_clear_all_breakpoints(): + """Clear all breakpoints in the debugger""" + + def debugger_clear_breakpoint(): + """Clear one breakpoint in the debugger""" + + def debugger_clear_breakpoint_id(number_small: int): + """Clear one breakpoint id in the debugger""" + + def debugger_disable_breakpoint_id(number_small: int): + """Disable one breakpoint id in the debugger""" + + def debugger_disable_breakpoint(): + """Disable one breakpoint in the debugger""" + + def debugger_disable_all_breakpoints(): + """Disable all breakpoints in the debugger""" + + def debugger_enable_breakpoint(): + """Enable one breakpoint in the debugger""" + + def debugger_enable_breakpoint_id(number_small: int): + """Enable one breakpoint id in the debugger""" + + def debugger_enable_all_breakpoints(): + """Enable all breakpoints in the debugger""" + + def debugger_disassemble(): + """Preps the disassemble command in the debugger""" + + def debugger_disassemble_here(): + """Disassembles instructions at the current instruction pointer""" + + def debugger_disassemble_clipboard(): + """Disassemble instructions at an address in the clipboard""" + + def debugger_goto_address(): + """Jump to a specific address in the debugger""" + + def debugger_goto_clipboard(): + """Jump to a specific address stored in the clipboard""" + + def debugger_goto_highlighted(): + """Jump to a specific highlighted address in the debugger""" + + def debugger_dump_ascii_string(): + """Display as specific address as an ascii string in the debugger""" + + def debugger_dump_unicode_string(): + """Display as specific address as an unicode string in the debugger""" + + def debugger_dump_pointers(): + """Display as specific address as a list of pointers in the debugger""" + + def debugger_inspect_type(): + """Inspect a specific data type in the debugger""" + + def debugger_clear_line(): + """Clear unwanted data from the command line""" + + def debugger_list_modules(): + """List the loaded modules in the debuggee memory space""" diff --git a/talon-user/code/delayed_speech_off.py b/talon-user/code/delayed_speech_off.py @@ -0,0 +1,30 @@ +from talon import Context, Module, actions, app, speech_system + +delay_mod = Module() + +delayed_enabled = False + + +def do_disable(e): + speech_system.unregister("post:phrase", do_disable) + actions.speech.disable() + + +@delay_mod.action_class +class DelayedSpeechOffActions: + def delayed_speech_on(): + """Activates a "temporary speech" mode that can be disabled lazily, + so that the actual disable command happens after whatever phrase + finishes next.""" + global delayed_enabled + if not actions.speech.enabled(): + delayed_enabled = True + actions.speech.enable() + + def delayed_speech_off(): + """Disables "temporary speech" mode lazily, meaning that the next + phrase that finishes will turn speech off.""" + global delayed_enabled + if delayed_enabled: + delayed_enabled = False + speech_system.register("post:phrase", do_disable) diff --git a/talon-user/code/dictation.py b/talon-user/code/dictation.py @@ -0,0 +1,284 @@ +# Descended from https://github.com/dwiel/talon_community/blob/master/misc/dictation.py +from talon import Module, Context, ui, actions, clip, app, grammar +from typing import Optional, Tuple, Literal +import re + +mod = Module() + +setting_context_sensitive_dictation = mod.setting( + "context_sensitive_dictation", + type=bool, + default=False, + desc="Look at surrounding text to improve auto-capitalization/spacing in dictation mode. By default, this works by selecting that text & copying it to the clipboard, so it may be slow or fail in some applications.", +) + +@mod.capture(rule="({user.vocabulary} | <word>)") +def word(m) -> str: + """A single word, including user-defined vocabulary.""" + try: + return m.vocabulary + except AttributeError: + return " ".join(actions.dictate.replace_words(actions.dictate.parse_words(m.word))) + +@mod.capture(rule="({user.vocabulary} | <phrase>)+") +def text(m) -> str: + """A sequence of words, including user-defined vocabulary.""" + return format_phrase(m) + +@mod.capture(rule="({user.vocabulary} | {user.punctuation} | <phrase>)+") +def prose(m) -> str: + """Mixed words and punctuation, auto-spaced & capitalized.""" + text, _state = auto_capitalize(format_phrase(m)) + return text + + +# ---------- FORMATTING ---------- # +def format_phrase(m): + words = capture_to_words(m) + result = "" + for i, word in enumerate(words): + if i > 0 and needs_space_between(words[i-1], word): + result += " " + result += word + return result + +def capture_to_words(m): + words = [] + for item in m: + words.extend( + actions.dictate.replace_words(actions.dictate.parse_words(item)) + if isinstance(item, grammar.vm.Phrase) else + item.split(" ")) + return words + +# There must be a simpler way to do this, but I don't see it right now. +no_space_after = re.compile(r""" + (?: + [\s\-_/#@([{‘“] # characters that never need space after them + | (?<!\w)[$£€¥₩₽₹] # currency symbols not preceded by a word character + # quotes preceded by beginning of string, space, opening braces, dash, or other quotes + | (?: ^ | [\s([{\-'"] ) ['"] + )$""", re.VERBOSE) +no_space_before = re.compile(r""" + ^(?: + [\s\-_.,!?;:/%)\]}’”] # characters that never need space before them + | [$£€¥₩₽₹](?!\w) # currency symbols not followed by a word character + # quotes followed by end of string, space, closing braces, dash, other quotes, or some punctuation. + | ['"] (?: $ | [\s)\]}\-'".,!?;:/] ) + )""", re.VERBOSE) + +# no_space_before = set("\n .,!?;:-_/%)]}") +# no_space_after = set("\n -_/#@([{") +def needs_space_between(before: str, after: str) -> bool: + return (before and after + and not no_space_after.search(before) + and not no_space_before.search(after)) + # return (before != "" and after != "" + # and before[-1] not in no_space_after + # and after[0] not in no_space_before) + +# # TESTS, uncomment to enable +# assert needs_space_between("a", "break") +# assert needs_space_between("break", "a") +# assert needs_space_between(".", "a") +# assert needs_space_between("said", "'hello") +# assert needs_space_between("hello'", "said") +# assert needs_space_between("hello.", "'John") +# assert needs_space_between("John.'", "They") +# assert needs_space_between("paid", "$50") +# assert needs_space_between("50$", "payment") +# assert not needs_space_between("", "") +# assert not needs_space_between("a", "") +# assert not needs_space_between("a", " ") +# assert not needs_space_between("", "a") +# assert not needs_space_between(" ", "a") +# assert not needs_space_between("a", ",") +# assert not needs_space_between("'", "a") +# assert not needs_space_between("a", "'") +# assert not needs_space_between("and-", "or") +# assert not needs_space_between("mary", "-kate") +# assert not needs_space_between("$", "50") +# assert not needs_space_between("US", "$") +# assert not needs_space_between("(", ")") +# assert not needs_space_between("(", "e.g.") +# assert not needs_space_between("example", ")") +# assert not needs_space_between("example", '".') +# assert not needs_space_between("example", '."') +# assert not needs_space_between("hello'", ".") +# assert not needs_space_between("hello.", "'") + +def auto_capitalize(text, state = None): + """ + Auto-capitalizes text. `state` argument means: + + - None: Don't capitalize initial word. + - "sentence start": Capitalize initial word. + - "after newline": Don't capitalize initial word, but we're after a newline. + Used for double-newline detection. + + Returns (capitalized text, updated state). + """ + output = "" + # Imagine a metaphorical "capitalization charge" travelling through the + # string left-to-right. + charge = state == "sentence start" + newline = state == "after newline" + for c in text: + # Sentence endings & double newlines create a charge. + if c in ".!?" or (newline and c == "\n"): + charge = True + # Alphanumeric characters and commas/colons absorb charge & try to + # capitalize (for numbers & punctuation this does nothing, which is what + # we want). + elif charge and (c.isalnum() or c in ",:"): + charge = False + c = c.capitalize() + # Otherwise the charge just passes through. + output += c + newline = c == "\n" + return output, ("sentence start" if charge else + "after newline" if newline else None) + + +# ---------- DICTATION AUTO FORMATTING ---------- # +class DictationFormat: + def __init__(self): + self.reset() + + def reset(self): + self.before = "" + self.state = "sentence start" + + def update_context(self, before): + if before is None: return + self.reset() + self.pass_through(before) + + def pass_through(self, text): + _, self.state = auto_capitalize(text, self.state) + self.before = text or self.before + + def format(self, text): + if needs_space_between(self.before, text): + text = " " + text + text, self.state = auto_capitalize(text, self.state) + self.before = text or self.before + return text + +dictation_formatter = DictationFormat() +ui.register("app_deactivate", lambda app: dictation_formatter.reset()) +ui.register("win_focus", lambda win: dictation_formatter.reset()) + +@mod.action_class +class Actions: + def dictation_format_reset(): + """Resets the dictation formatter""" + return dictation_formatter.reset() + + def dictation_insert_raw(text: str): + """Inserts text as-is, without invoking the dictation formatter.""" + dictation_formatter.pass_through(text) + actions.insert(text) + + def dictation_insert(text: str) -> str: + """Inserts dictated text, formatted appropriately.""" + # do_the_dance = whether we should try to be context-sensitive. Since + # whitespace is not affected by formatter state, if text.isspace() is + # True we don't need context-sensitivity. + do_the_dance = (setting_context_sensitive_dictation.get() + and not text.isspace()) + if do_the_dance: + dictation_formatter.update_context( + actions.user.dictation_peek_left(clobber=True)) + text = dictation_formatter.format(text) + actions.user.add_phrase_to_history(text) + actions.insert(text) + # Add a space after cursor if necessary. + if not do_the_dance or not text or no_space_after.search(text): + return + char = actions.user.dictation_peek_right() + if char is not None and needs_space_between(text, char): + actions.insert(" ") + actions.edit.left() + + def dictation_peek_left(clobber: bool = False) -> Optional[str]: + """ + Tries to get some text before the cursor, ideally a word or two, for the + purpose of auto-spacing & -capitalization. Results are not guaranteed; + dictation_peek_left() may return None to indicate no information. (Note + that returning the empty string "" indicates there is nothing before + cursor, ie. we are at the beginning of the document.) + + If there is currently a selection, dictation_peek_left() must leave it + unchanged unless `clobber` is true, in which case it may clobber it. + """ + # Get rid of the selection if it exists. + if clobber: actions.user.clobber_selection_if_exists() + # Otherwise, if there's a selection, fail. + elif "" != actions.edit.selected_text(): return None + + # In principle the previous word should suffice, but some applications + # have a funny concept of what the previous word is (for example, they + # may only take the "`" at the end of "`foo`"). To be double sure we + # take two words left. I also tried taking a line up + a word left, but + # edit.extend_up() = key(shift-up) doesn't work consistently in the + # Slack webapp (sometimes escapes the text box). + actions.edit.extend_word_left() + actions.edit.extend_word_left() + text = actions.edit.selected_text() + # if we're at the beginning of the document/text box, we may not have + # selected any text, in which case we shouldn't move the cursor. + if text: + # Unfortunately, in web Slack, if our selection ends at newline, + # this will go right over the newline. Argh. + actions.edit.right() + return text + + def clobber_selection_if_exists(): + """Deletes the currently selected text if it exists; otherwise does nothing.""" + actions.key("space backspace") + # This space-backspace trick is fast and reliable but has the + # side-effect of cluttering the undo history. Other options: + # + # 1. Call edit.cut() inside a clip.revert() block. This assumes + # edit.cut() is supported AND will be a no-op if there's no + # selection. Unfortunately, sometimes one or both of these is false, + # eg. the notion webapp makes ctrl-x cut the current block by default + # if nothing is selected. + # + # 2. Test whether a selection exists by asking whether + # edit.selected_text() is empty; if it does, use edit.delete(). This + # usually uses the clipboard, which can be quite slow. Also, not sure + # how this would interact with switching edit.selected_text() to use + # the selection clipboard on linux, which can be nonempty even if no + # text is selected in the current application. + # + # Perhaps this ought to be configurable by a setting. + + def dictation_peek_right() -> Optional[str]: + """ + Tries to get a few characters after the cursor for auto-spacing. + Results are not guaranteed; dictation_peek_right() may return None to + indicate no information. (Note that returning the empty string "" + indicates there is nothing after cursor, ie. we are at the end of the + document.) + """ + # We grab two characters because I think that's what no_space_before + # needs in the worst case. An example where the second character matters + # is inserting before (1) "' hello" vs (2) "'hello". In case (1) we + # don't want to add space, in case (2) we do. + actions.edit.extend_right() + actions.edit.extend_right() + after = actions.edit.selected_text() + if after: actions.edit.left() + return after + +# Use the dictation formatter in dictation mode. +dictation_ctx = Context() +dictation_ctx.matches = r""" +mode: dictation +""" + +@dictation_ctx.action_class("main") +class main_action: + def auto_insert(text): actions.user.dictation_insert(text) diff --git a/talon-user/code/edit.py b/talon-user/code/edit.py @@ -0,0 +1,28 @@ +import time +from talon import Context, Module, actions, clip, ui + +ctx = Context() +mod = Module() + + +@ctx.action_class("edit") +class edit_actions: + def selected_text() -> str: + with clip.capture() as s: + actions.edit.copy() + try: + return s.get() + except clip.NoChange: + return "" + + +@mod.action_class +class Actions: + def paste(text: str): + """Pastes text and preserves clipboard""" + + with clip.revert(): + clip.set_text(text) + actions.edit.paste() + # sleep here so that clip.revert doesn't revert the clipboard too soon + actions.sleep("150ms") diff --git a/talon-user/code/engine.py b/talon-user/code/engine.py @@ -0,0 +1,19 @@ +from talon import Context, Module +from talon import speech_system + +mod = Module() + + +@mod.action_class +class Actions: + def engine_sleep(): + """Sleep the engine""" + speech_system.engine_mimic("go to sleep"), + + def engine_wake(): + """Wake the engine""" + speech_system.engine_mimic("wake up"), + + def engine_mimic(cmd: str): + """Sends phrase to engine""" + speech_system.engine_mimic(cmd) diff --git a/talon-user/code/exec.py b/talon-user/code/exec.py @@ -0,0 +1,21 @@ +import os +import subprocess + +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def system_command(cmd: str): + """execute a command on the system""" + os.system(cmd) + + def system_command_nb(cmd: str): + """execute a command on the system without blocking""" + subprocess.Popen(cmd, shell=True) + + def system_path_command(cmd: str): + """execute a command on the system with PATH set""" + os.system('. ~/.dotfiles/shell/env; . ~/.dotfiles/shell/paths;' + cmd) diff --git a/talon-user/code/file_manager.py b/talon-user/code/file_manager.py @@ -0,0 +1,427 @@ +from talon import app, Module, Context, actions, ui, imgui, settings, app, registry +from os.path import expanduser +from subprocess import Popen +from pathlib import Path +from typing import List, Union +import os +import math +import re +from itertools import islice + +mod = Module() +ctx = Context() + +mod.tag("file_manager", desc="Tag for enabling generic file management commands") +mod.list("file_manager_directories", desc="List of subdirectories for the current path") +mod.list("file_manager_files", desc="List of files at the root of the current path") + + +setting_auto_show_pickers = mod.setting( + "file_manager_auto_show_pickers", + type=int, + default=0, + desc="Enable to show the file/directories pickers automatically", +) +setting_folder_limit = mod.setting( + "file_manager_folder_limit", + type=int, + default=1000, + desc="Maximum number of files/folders to iterate", +) +setting_file_limit = mod.setting( + "file_manager_file_limit", + type=int, + default=1000, + desc="Maximum number of files to iterate", +) +setting_imgui_limit = mod.setting( + "file_manager_imgui_limit", + type=int, + default=20, + desc="Maximum number of files/folders to display in the imgui", +) +setting_imgui_string_limit = mod.setting( + "file_manager_string_limit", + type=int, + default=20, + desc="Maximum like of string to display in the imgui", +) +cached_path = None +file_selections = folder_selections = [] +current_file_page = current_folder_page = 1 + +ctx.lists["self.file_manager_directories"] = [] +ctx.lists["self.file_manager_files"] = [] + +directories_to_remap = {} +user_path = os.path.expanduser("~") +if app.platform == "windows": + is_windows = True + import ctypes + + GetUserNameEx = ctypes.windll.secur32.GetUserNameExW + NameDisplay = 3 + + size = ctypes.pointer(ctypes.c_ulong(0)) + GetUserNameEx(NameDisplay, None, size) + + nameBuffer = ctypes.create_unicode_buffer(size.contents.value) + GetUserNameEx(NameDisplay, nameBuffer, size) + one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) + + # this is probably not the correct way to check for onedrive, quick and dirty + if os.path.isdir(os.path.expanduser(os.path.join("~", r"OneDrive\Desktop"))): + default_folder = os.path.join("~", "Desktop") + + directories_to_remap = { + "Desktop": os.path.join(one_drive_path, "Desktop"), + "Documents": os.path.join(one_drive_path, "Documents"), + "Downloads": os.path.join(user_path, "Downloads"), + "Music": os.path.join(user_path, "Music"), + "OneDrive": one_drive_path, + "Pictures": os.path.join(one_drive_path, "Pictures"), + "Videos": os.path.join(user_path, "Videos"), + } + else: + # todo use expanduser for cross platform support + directories_to_remap = { + "Desktop": os.path.join(user_path, "Desktop"), + "Documents": os.path.join(user_path, "Documents"), + "Downloads": os.path.join(user_path, "Downloads"), + "Music": os.path.join(user_path, "Music"), + "OneDrive": one_drive_path, + "Pictures": os.path.join(user_path, "Pictures"), + "Videos": os.path.join(user_path, "Videos"), + } + + +@mod.action_class +class Actions: + def file_manager_current_path() -> str: + """Returns the current path for the active file manager.""" + return "" + + def file_manager_open_parent(): + """file_manager_open_parent""" + return + + def file_manager_go_forward(): + """file_manager_go_forward_directory""" + + def file_manager_go_back(): + """file_manager_go_forward_directory""" + + def file_manager_open_volume(volume: str): + """file_manager_open_volume""" + + def file_manager_open_directory(path: str): + """opens the directory that's already visible in the view""" + + def file_manager_select_directory(path: str): + """selects the directory""" + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + + def file_manager_show_properties(): + """Shows the properties for the file""" + + def file_manager_terminal_here(): + """Opens terminal at current location""" + + def file_manager_open_file(path: str): + """opens the file""" + + def file_manager_select_file(path: str): + """selects the file""" + + def file_manager_refresh_title(): + """Refreshes the title to match current directory. this is for e.g. windows command prompt that will need to do some magic. """ + return + + def file_manager_update_lists(): + """Forces an update of the lists (e.g., when file or folder created)""" + update_lists() + + def file_manager_toggle_pickers(): + """Shows the pickers""" + if gui_files.showing: + gui_files.hide() + gui_folders.hide() + else: + gui_files.show() + gui_folders.show() + + def file_manager_hide_pickers(): + """Hides the pickers""" + if gui_files.showing: + gui_files.hide() + gui_folders.hide() + + def file_manager_open_user_directory(path: str): + """expands and opens the user directory""" + # this functionality exists mostly for windows. + # since OneDrive does strange stuff... + if path in directories_to_remap: + path = directories_to_remap[path] + + path = os.path.expanduser(os.path.join("~", path)) + actions.user.file_manager_open_directory(path) + + def file_manager_get_directory_by_index(index: int) -> str: + """Returns the requested directory for the imgui display by index""" + index = (current_folder_page - 1) * setting_imgui_limit.get() + index + assert index < len(folder_selections) + return folder_selections[index] + + def file_manager_get_file_by_index(index: int) -> str: + """Returns the requested directory for the imgui display by index""" + index = (current_file_page - 1) * setting_imgui_limit.get() + index + assert index < len(file_selections) + return file_selections[index] + + def file_manager_next_file_page(): + """next_file_page""" + global current_file_page + if gui_files.showing: + if current_file_page != total_file_pages: + current_file_page += 1 + else: + current_file_page = 1 + gui_files.show() + + def file_manager_previous_file_page(): + """previous_file_page""" + global current_file_page + if gui_files.showing: + if current_file_page != 1: + current_file_page -= 1 + else: + current_file_page = total_file_pages + + gui_files.show() + + def file_manager_next_folder_page(): + """next_folder_page""" + global current_folder_page + if gui_folders.showing: + if current_folder_page != total_folder_pages: + current_folder_page += 1 + else: + current_folder_page = 1 + + gui_folders.show() + + def file_manager_previous_folder_page(): + """previous_folder_page""" + global current_folder_page + if gui_folders.showing: + if current_folder_page != 1: + current_folder_page -= 1 + else: + current_folder_page = total_folder_pages + + gui_folders.show() + + +pattern = re.compile(r"[A-Z][a-z]*|[a-z]+|\d") + + +def create_spoken_forms(symbols, max_len=30): + return [" ".join(list(islice(pattern.findall(s), max_len))) for s in symbols] + + +def is_dir(f): + try: + return f.is_dir() + except: + return False + + +def is_file(f): + try: + return f.is_file() + except: + return False + + +def get_directory_map(current_path): + directories = [ + f.name + for f in islice( + current_path.iterdir(), settings.get("user.file_manager_folder_limit", 1000) + ) + if is_dir(f) + ] + # print(len(directories)) + spoken_forms = create_spoken_forms(directories) + return dict(zip(spoken_forms, directories)) + + +def get_file_map(current_path): + files = [ + f.name + for f in islice( + current_path.iterdir(), settings.get("user.file_manager_file_limit", 1000) + ) + if is_file(f) + ] + # print(str(files)) + spoken_forms = create_spoken_forms([p for p in files]) + return dict(zip(spoken_forms, [f for f in files])) + + +@imgui.open(y=10, x=900) +def gui_folders(gui: imgui.GUI): + global current_folder_page, total_folder_pages + total_folder_pages = math.ceil( + len(ctx.lists["self.file_manager_directories"]) / setting_imgui_limit.get() + ) + gui.text( + "Select a directory ({}/{})".format(current_folder_page, total_folder_pages) + ) + gui.line() + + index = 1 + current_index = (current_folder_page - 1) * setting_imgui_limit.get() + + while index <= setting_imgui_limit.get() and current_index < len(folder_selections): + name = ( + ( + folder_selections[current_index][: setting_imgui_string_limit.get()] + + ".." + ) + if len(folder_selections[current_index]) > setting_imgui_string_limit.get() + else folder_selections[current_index] + ) + gui.text("{}: {} ".format(index, name)) + current_index += 1 + index = index + 1 + + # if total_folder_pages > 1: + # gui.spacer() + + # if gui.button('Next...'): + # actions.user.file_manager_next_folder_page() + + # if gui.button("Previous..."): + # actions.user.file_manager_previous_folder_page() + + +@imgui.open(y=10, x=1300) +def gui_files(gui: imgui.GUI): + global file_selections, current_file_page, total_file_pages + total_file_pages = math.ceil(len(file_selections) / setting_imgui_limit.get()) + + gui.text("Select a file ({}/{})".format(current_file_page, total_file_pages)) + gui.line() + index = 1 + current_index = (current_file_page - 1) * setting_imgui_limit.get() + + while index <= setting_imgui_limit.get() and current_index < len(file_selections): + name = ( + (file_selections[current_index][: setting_imgui_string_limit.get()] + "..") + if len(file_selections[current_index]) > setting_imgui_string_limit.get() + else file_selections[current_index] + ) + + gui.text("{}: {} ".format(index, name)) + current_index = current_index + 1 + index = index + 1 + + # if total_file_pages > 1: + # gui.spacer() + + # if gui.button('Next...'): + # actions.user.file_manager_next_file_page() + + # if gui.button("Previous..."): + # actions.user.file_manager_previous_file_page() + + +def clear_lists(): + global folder_selections, file_selections + if ( + len(ctx.lists["self.file_manager_directories"]) > 0 + or len(ctx.lists["self.file_manager_files"]) > 0 + ): + current_folder_page = current_file_page = 1 + ctx.lists["self.file_manager_directories"] = [] + ctx.lists["self.file_manager_files"] = [] + folder_selections = [] + file_selections = [] + + +def update_gui(): + if gui_folders.showing or setting_auto_show_pickers.get() >= 1: + gui_folders.show() + gui_files.show() + + +def update_lists(): + global folder_selections, file_selections, current_folder_page, current_file_page + is_valid_path = False + path = actions.user.file_manager_current_path() + directories = {} + files = {} + folder_selections = [] + file_selections = [] + # print(path) + try: + current_path = Path(path) + is_valid_path = current_path.is_dir() + except: + is_valid_path = False + + if is_valid_path: + # print("valid..." + str(current_path)) + try: + directories = get_directory_map(current_path) + files = get_file_map(current_path) + except: + # print("invalid path...") + + directories = {} + files = {} + + current_folder_page = current_file_page = 1 + ctx.lists["self.file_manager_directories"] = directories + ctx.lists["self.file_manager_files"] = files + folder_selections = sorted(directories.values(), key=str.casefold) + file_selections = sorted(files.values(), key=str.casefold) + + update_gui() + + +def win_event_handler(window): + global cached_path + + # on windows, we get events from the clock + # and such, so this check is important + if not window.app.exe or window != ui.active_window(): + return + + path = actions.user.file_manager_current_path() + + if not "user.file_manager" in registry.tags: + actions.user.file_manager_hide_pickers() + clear_lists() + elif path: + if cached_path != path: + update_lists() + elif cached_path: + clear_lists() + actions.user.file_manager_hide_pickers() + + cached_path = path + + +def register_events(): + ui.register("win_title", win_event_handler) + ui.register("win_focus", win_event_handler) + + +# prevent scary errors in the log by waiting for talon to be fully loaded +# before registering the events +app.register("ready", register_events) + diff --git a/talon-user/code/find_and_replace.py b/talon-user/code/find_and_replace.py @@ -0,0 +1,47 @@ +from talon import Context, actions, ui, Module, app +from typing import Union + +mod = Module() +mod.tag("find_and_replace", desc="Tag for enabling generic find and replace commands") + + +@mod.action_class +class Actions: + def find(text: str): + """Finds text in current editor""" + + def find_next(): + """Navigates to the next occurrence""" + + def find_previous(): + """Navigates to the previous occurrence""" + + def find_everywhere(text: str): + """Finds text across project""" + + def find_toggle_match_by_case(): + """Toggles find match by case sensitivity""" + + def find_toggle_match_by_word(): + """Toggles find match by whole words""" + + def find_toggle_match_by_regex(): + """Toggles find match by regex""" + + def replace(text: str): + """Search and replace for text in the active editor""" + + def replace_everywhere(text: str): + """Search and replaces for text in the entire project""" + + def replace_confirm(): + """Confirm replace at current position""" + + def replace_confirm_all(): + """Confirm replace all""" + + def select_previous_occurrence(text: str): + """Selects the previous occurrence of the text, and suppresses any find/replace dialogs.""" + + def select_next_occurrence(text: str): + """Selects the next occurrence of the text, and suppresses any find/replace dialogs.""" diff --git a/talon-user/code/formatters.py b/talon-user/code/formatters.py @@ -0,0 +1,298 @@ +from talon import Module, Context, actions, ui, imgui, app +from talon.grammar import Phrase +from typing import List, Union +import logging +import re + +ctx = Context() +key = actions.key +edit = actions.edit + +words_to_keep_lowercase = "a,an,the,at,by,for,in,is,of,on,to,up,and,as,but,or,nor".split( + "," +) + +# The last phrase spoken, without & with formatting. Used for reformatting. +last_phrase = "" +last_phrase_formatted = "" + + +def surround(by): + def func(i, word, last): + if i == 0: + word = by + word + if last: + word += by + return word + + return func + + +def format_phrase(m: Union[str, Phrase], fmtrs: str): + global last_phrase, last_phrase_formatted + last_phrase = m + words = [] + if isinstance(m, str): + words = m.split(" ") + else: + # TODO: is this still necessary, and if so why? + if m.words[-1] == "over": + m.words = m.words[:-1] + + words = actions.dictate.parse_words(m) + words = actions.dictate.replace_words(words) + + result = last_phrase_formatted = format_phrase_no_history(words, fmtrs) + actions.user.add_phrase_to_history(result) + # Arguably, we shouldn't be dealing with history here, but somewhere later + # down the line. But we have a bunch of code that relies on doing it this + # way and I don't feel like rewriting it just now. -rntz, 2020-11-04 + return result + + +def format_phrase_no_history(word_list, fmtrs: str): + fmtr_list = fmtrs.split(",") + words = [] + spaces = True + for i, w in enumerate(word_list): + for name in reversed(fmtr_list): + smash, func = all_formatters[name] + w = func(i, w, i == len(word_list) - 1) + spaces = spaces and not smash + words.append(w) + sep = " " if spaces else "" + return sep.join(words) + + +NOSEP = True +SEP = False + + +def words_with_joiner(joiner): + """Pass through words unchanged, but add a separator between them.""" + + def formatter_function(i, word, _): + return word if i == 0 else joiner + word + + return (NOSEP, formatter_function) + + +def first_vs_rest(first_func, rest_func=lambda w: w): + """Supply one or two transformer functions for the first and rest of + words respectively. + + Leave second argument out if you want all but the first word to be passed + through unchanged. + Set first argument to None if you want the first word to be passed + through unchanged.""" + if first_func is None: + first_func = lambda w: w + + def formatter_function(i, word, _): + return first_func(word) if i == 0 else rest_func(word) + + return formatter_function + + +def every_word(word_func): + """Apply one function to every word.""" + + def formatter_function(i, word, _): + return word_func(word) + + return formatter_function + + +formatters_dict = { + "NOOP": (SEP, lambda i, word, _: word), + "DOUBLE_UNDERSCORE": (NOSEP, first_vs_rest(lambda w: "__%s__" % w)), + "PRIVATE_CAMEL_CASE": (NOSEP, first_vs_rest(lambda w: w, lambda w: w.capitalize())), + "PROTECTED_CAMEL_CASE": ( + NOSEP, + first_vs_rest(lambda w: w, lambda w: w.capitalize()), + ), + "PUBLIC_CAMEL_CASE": (NOSEP, every_word(lambda w: w.capitalize())), + "SNAKE_CASE": ( + NOSEP, + first_vs_rest(lambda w: w.lower(), lambda w: "_" + w.lower()), + ), + "NO_SPACES": (NOSEP, every_word(lambda w: w)), + "DASH_SEPARATED": words_with_joiner("-"), + "TERMINAL_DASH_SEPARATED": ( + NOSEP, + first_vs_rest(lambda w: " --" + w.lower(), lambda w: "-" + w.lower()), + ), + "DOUBLE_COLON_SEPARATED": words_with_joiner("::"), + "ALL_CAPS": (SEP, every_word(lambda w: w.upper())), + "ALL_LOWERCASE": (SEP, every_word(lambda w: w.lower())), + "DOUBLE_QUOTED_STRING": (SEP, surround('"')), + "SINGLE_QUOTED_STRING": (SEP, surround("'")), + "SPACE_SURROUNDED_STRING": (SEP, surround(" ")), + "DOT_SEPARATED": words_with_joiner("."), + "DOT_SNAKE": (NOSEP, lambda i, word, _: "." + word if i == 0 else "_" + word), + "SLASH_SEPARATED": (NOSEP, every_word(lambda w: "/" + w)), + "CAPITALIZE_FIRST_WORD": (SEP, first_vs_rest(lambda w: w.capitalize())), + "CAPITALIZE_ALL_WORDS": ( + SEP, + lambda i, word, _: word.capitalize() + if i == 0 or word not in words_to_keep_lowercase + else word, + ), + "FIRST_THREE": (NOSEP, lambda i, word, _: word[0:3]), + "FIRST_FOUR": (NOSEP, lambda i, word, _: word[0:4]), + "FIRST_FIVE": (NOSEP, lambda i, word, _: word[0:5]), +} + +# This is the mapping from spoken phrases to formatters +formatters_words = { + "allcaps": formatters_dict["ALL_CAPS"], + "alldown": formatters_dict["ALL_LOWERCASE"], + "camel": formatters_dict["PRIVATE_CAMEL_CASE"], + "dotted": formatters_dict["DOT_SEPARATED"], + "dubstring": formatters_dict["DOUBLE_QUOTED_STRING"], + "dunder": formatters_dict["DOUBLE_UNDERSCORE"], + "hammer": formatters_dict["PUBLIC_CAMEL_CASE"], + "kebab": formatters_dict["DASH_SEPARATED"], + "packed": formatters_dict["DOUBLE_COLON_SEPARATED"], + "padded": formatters_dict["SPACE_SURROUNDED_STRING"], + # "say": formatters_dict["NOOP"], + # "sentence": formatters_dict["CAPITALIZE_FIRST_WORD"], + "slasher": formatters_dict["SLASH_SEPARATED"], + "smash": formatters_dict["NO_SPACES"], + "snake": formatters_dict["SNAKE_CASE"], + # "speak": formatters_dict["NOOP"], + "string": formatters_dict["SINGLE_QUOTED_STRING"], + "title": formatters_dict["CAPITALIZE_ALL_WORDS"], + # disable a few formatters for now + # "tree": formatters_dict["FIRST_THREE"], + # "quad": formatters_dict["FIRST_FOUR"], + # "fiver": formatters_dict["FIRST_FIVE"], +} + +all_formatters = {} +all_formatters.update(formatters_dict) +all_formatters.update(formatters_words) + +mod = Module() +mod.list("formatters", desc="list of formatters") +mod.list( + "prose_formatter", + desc="words to start dictating prose, and the formatter they apply", +) + + +@mod.capture(rule="{self.formatters}+") +def formatters(m) -> str: + "Returns a comma-separated string of formatters e.g. 'SNAKE,DUBSTRING'" + return ",".join(m.formatters_list) + + +@mod.capture( + # Note that if the user speaks something like "snake dot", it will + # insert "dot" - otherwise, they wouldn't be able to insert punctuation + # words directly. + rule="<self.formatters> <user.text> (<user.text> | <user.formatter_immune>)*" +) +def format_text(m) -> str: + "Formats the text and returns a string" + out = "" + formatters = m[0] + for chunk in m[1:]: + if isinstance(chunk, ImmuneString): + out += chunk.string + else: + out += format_phrase(chunk, formatters) + return out + + +class ImmuneString(object): + """Wrapper that makes a string immune from formatting.""" + + def __init__(self, string): + self.string = string + + +@mod.capture( + # Add anything else into this that you want to be able to speak during a + # formatter. + rule="(<user.symbol_key> | numb <number>)" +) +def formatter_immune(m) -> ImmuneString: + """Text that can be interspersed into a formatter, e.g. characters. + + It will be inserted directly, without being formatted. + + """ + if hasattr(m, "number"): + value = m.number + else: + value = m[0] + return ImmuneString(str(value)) + + +@mod.action_class +class Actions: + def formatted_text(phrase: Union[str, Phrase], formatters: str) -> str: + """Formats a phrase according to formatters. formatters is a comma-separated string of formatters (e.g. 'CAPITALIZE_ALL_WORDS,DOUBLE_QUOTED_STRING')""" + return format_phrase(phrase, formatters) + + def insert_formatted(phrase: Union[str, Phrase], formatters: str): + """Inserts a phrase formatted according to formatters. Formatters is a comma separated list of formatters (e.g. 'CAPITALIZE_ALL_WORDS,DOUBLE_QUOTED_STRING')""" + actions.insert(format_phrase(phrase, formatters)) + + def formatters_help_toggle(): + """Lists all formatters""" + if gui.showing: + gui.hide() + else: + gui.show() + + def formatters_reformat_last(formatters: str) -> str: + """Clears and reformats last formatted phrase""" + global last_phrase, last_phrase_formatted + if actions.user.get_last_phrase() != last_phrase_formatted: + # The last thing we inserted isn't the same as the last thing we + # formatted, so abort. + logging.warning( + "formatters_reformat_last(): Last phrase wasn't a formatter!" + ) + return + actions.user.clear_last_phrase() + actions.user.insert_formatted(last_phrase, formatters) + + def formatters_reformat_selection(formatters: str) -> str: + """Reformats the current selection.""" + selected = edit.selected_text() + if not selected: + print("Asked to reformat selection, but nothing selected!") + return + unformatted = re.sub(r"[^a-zA-Z0-9]+", " ", selected).lower() + # TODO: Separate out camelcase & studleycase vars + + # Delete separately for compatibility with programs that don't overwrite + # selected text (e.g. Emacs) + edit.delete() + text = actions.self.formatted_text(unformatted, formatters) + actions.insert(text) + return text + + def insert_many(strings: List[str]) -> None: + """Insert a list of strings, sequentially.""" + for string in strings: + actions.insert(string) + + +ctx.lists["self.formatters"] = formatters_words.keys() +ctx.lists["self.prose_formatter"] = { + "say": "NOOP", + "speak": "NOOP", + "sentence": "CAPITALIZE_FIRST_WORD", +} + + +@imgui.open() +def gui(gui: imgui.GUI): + gui.text("List formatters") + gui.line() + for name in sorted(set(formatters_words.keys())): + gui.text(f"{name} | {format_phrase_no_history(['one', 'two', 'three'], name)}") diff --git a/talon-user/help.py b/talon-user/code/help.py diff --git a/talon-user/history.py b/talon-user/code/history.py diff --git a/talon-user/code/homophones.csv b/talon-user/code/homophones.csv @@ -0,0 +1,648 @@ +able,Abel +Adam,atom +Cain,cane +Chile,chili,chilly +check,Czech +Dane,deign +finish,Finnish +gale,Gail +Hugh,hue,hew +I,eye,aye +Jim,gym +laps,lapse,Lapps +Lou,lieu +Nice,niece +Paul,pall +Pete,peat +sue,Sioux +Wayne,wane,wain +acclamation,acclimation +add,ad +addition,edition +adds,ads,adz +adherence,adherents +ado,adieu +aerial,ariel +affected,effected +afterward,afterword +aid,aide +ale,ail +all,awl +alluded,eluded +illusion,allusion +allowed,aloud +alter,altar +analyst,annalist +appetite,apatite +apprize,apprise +arc,ark +ascent,assent +assistance,assistants +augur,auger +aunt,ant +oral,aural +oriole,aureole +away,aweigh +acts,ax +axis,axes +axle,axel +eyes,ayes +bah,baa +Babel,babble +bad,bade +bait,bate +bald,balled,bawled +bail,bale,baal +band,banned +barred,bard +baron,barren +basil,basal +base,bass +basis,bases +basque,bask +based,baste +baited,bated +ball,bawl +bizarre,bazaar +bear,bare +beat,beet +bow,beau +be,bee +beach,beech +been,bin +beer,bier +bell,belle +better,bettor +bib,bibb +bite,byte,bight +bird,burred +birth,berth +blue,blew +block,bloc +bore,boar +boulder,bolder +bomb,balm,bombe +booty,bootie +border,boarder +board,bored +born,borne +bow,bough +bowed,bode +bold,bowled +braid,brayed +brays,braise +breach,breech +break,brake +bread,bred +brews,bruise +bridal,bridle +broach,brooch +brewed,brood +browse,brows +brute,brut +build,billed +bullion,bouillon +boy,buoy +burger,burgher +borough,burrow,burro +bury,berry +bust,bussed +but,butt +by,buy,bye +caddy,caddie +calendar,calender +callous,callus +cannon,canon +cantor,canter +canvas,canvass +capital,capitol +carol,carrel +carrot,carat,caret,karat +cash,cache +cached,cashed +cast,caste +caster,castor +cause,caws +seed,cede +ceiling,sealing +seller,cellar +sensor,censor +sent,cent,scent +serial,cereal +chance,chants +chased,chaste +chauffeur,shofar +cheap,cheep +chic,sheik +choose,chews +cited,sided,sighted +clack,claque +clammer,clamor,clamber +clause,claws +clue,clew +click,clique +climb,clime +close,clothes,cloze +coal,cole +course,coarse +coat,cote +coax,cokes +collared,collard +complacent,complaisant +complement,compliment +conceded,conceited +consonants,consonance +continents,continence +coup,coo +coulee,coolie +cops,copse +coral,choral +cord,chord,cored +core,corps +coughers,coffers +council,counsel +coupe,coop +coarser,courser +cousin,cozen +coward,cowered +craft,kraft +creek,creak +crepe,crape +cruel,crewel +cruise,crews +queue,cue +current,currant +cursor,curser +signet,cygnet +symbol,cymbal +Cyprus,cypress +damn,dam +days,daze +dear,deer +dense,dents +diffused,defused +discrete,discreet +disperse,disburse +descent,dissent +do,due,dew +doc,dock +do,dough,doe +does,doze +draft,draught +ducked,duct +ducks,ducts +dual,duel +done,dun +die,dye +dying,dyeing +adduce,educe +eke,eek +effect,affect +effects,affects +eight,ate +illicit,elicit +elude,allude +elusive,illusive,allusive +emend,amend +innumerable,enumerable +errant,arrant +eve,eave +exceed,accede +except,accept +exercise,exorcise +I'd,eyed +faint,feint +fair,fare +fairy,ferry +foe,faux +fawn,faun +facts,fax +phase,faze +feet,feat +fens,fends +fete,fate +few,phew +Phil,fill +find,fined +fisher,fissure +flare,flair +flee,flea +flow,floe +flower,flour +flew,flu,flue +flyer,flier +fold,foaled +for,four,fore +foregone,forgone +forward,foreword +fort,forte +fourth,forth +foul,fowl +frank,franc +phrase,frays +freeze,frees,frieze +friar,fryer +fur,fir +gaffe,gaff +gate,gait +gamble,gambol +gel,jell +gene,Jean +jibe,gibe +guild,gild +gilder,guilder +guilt,gilt +gnome,Nome +gopher,gofer +gourd,gored +gorilla,guerilla +graham,gram +graft,graphed +great,grate +greater,grater +grade,grayed +graze,grays +Greece,grease +grill,grille +grizzly,grisly +grown,groan +guest,guessed +gauge,gage +hail,hale +hall,haul +have,halve +handmade,handmaid +hangar,hanger +handsome,hansom +hair,hare +hey,hay +haze,hays +heart,hart +he'll,heal,heel +air,heir,err +heard,herd +here,hear +heroin,heroine +he'd,heed +high,hi +higher,hire +ho,hoe +hold,holed +whole,hole +holy,wholly,holey +horde,hoard +horse,hoarse +hose,hoes +hostile,hostel +humorous,humerus +hurts,hertz +him,hymn +idle,idol,idyll +imminent,immanent +impassable,impassible +in,inn +innocence,innocents +insight,incite +instance,instants +intense,intents +I'll,aisle,isle +islet,eyelet +its,it's +jam,jamb +jinx,jinks +kernel,colonel +nap,knap +nave,knave +new,knew,gnu +knit,nit +knock,nock +no,know +coy,koi +crawl,kraal +lacks,lax +latter,ladder +laid,lade +lama,llama +lane,lain +lee,lea +lead,led +leak,leek +lean,lien +least,leased +leach,leech +lay,lei +lays,leis,laze +lens,lends +lesson,lessen +let's,lets +levy,levee +light,lite +liken,lichen +links,lynx +literal,littoral +loathe,loath +loch,lock +load,lode,lowed +loan,lone +loot,lute +low,lo +lochs,locks,lox +lumber,lumbar +lie,lye +liar,lyre,lier +matter,madder +made,maid +maze,maize +male,mail +mall,maul +main,Maine,mane +manner,manor +mantle,mantel +mayor,mare +mark,marc +martial,marshal +martin,marten +mast,massed +mat,matte +meet,meat,mete +meteor,meatier +metal,medal,mettle +medal,meddle,mettle +Mary,marry,merry +metal,meddle,mettle +mule,mewl +mean,mien +mill,mil +mince,mints +mind,mined +minor,miner +Mrs.,misses +missile,missal +missed,mist +might,mite +mode,mowed +mood,mooed +mourn,morn +moat,mote +morning,mourning +moose,mousse +moan,mown +mucus,mucous +muscle,mussel +muse,mews +must,mussed +mustard,mustered +naval,navel +need,knead,kneed +nay,neigh +nice,gneiss +knickers,nickers +night,knight +none,nun +knows,nose +not,knot +or,ore,oar +owed,ode +oh,owe +our,hour +ours,hours +outcast,outcaste +overdue,overdo +overseas,oversees +paste,paced +packed,pact +pale,pail +pain,pane +palette,palate,pallet +pair,pear,pare +parley,parlay +past,passed +patients,patience +patted,padded +pause,paws +piece,peace +peas,pees +pedal,peddle,petal +pee,pea +peel,peal +peer,pier +penance,pennants +paean,peon,paeon +per,purr +parish,perish +petrol,petrel +pew,pugh +flocks,phlox +pie,pi +picot,pekoe +pigeon,pidgin +pilot,Pilate +peak,peek,pique +pistol,pistil +plaque,plack +plate,plait +plane,plain +please,pleas +plum,plumb +poll,pole +pull,pool +pour,pore +praise,prays,preys +precedence,precedents +premier,premiere +presence,presents +pray,prey +pride,pried +primer,primmer +principle,principal +prince,prints +profit,prophet +pros,prose +perl,pearl,purl +pervade,purveyed +quartz,quarts +quince,quints +choir,quire +rabbit,rabbet +rain,reign,rein +wrap,rap +wrapped,rapped,rapt +raise,rays,raze +read,red +read,reed +real,reel +wreak,reek +residents,residence +rest,wrest +wretch,retch +review,revue +rigor,rigger +ring,wring +right,write,rite +roads,rhodes +Rome,roam +row,roe +role,roll +room,rheum +rose,rows +rough,ruff +route,root +rue,roux +road,rode,rowed +rude,rued +rumor,roomer +rung,wrung +Russell,rustle +sack,sac +sacks,sax +sale,sail +sane,seine +sachet,sashay +saver,savor +seen,scene +sense,cents,scents +skull,scull +seer,sear,sere +see,sea +cedar,seeder +seem,seam +sees,seize,seas +sell,cell +series,Ceres +session,cession +sheer,shear +shoe,shoo +shoot,chute +shown,shone +sick,sic +side,sighed +size,sighs +site,sight,cite +sink,sync +slay,sleigh +slight,sleight +slow,sloe +slew,slough,slue +sword,soared +sold,soled +son,sun +sore,soar +sorry,sari +soul,sole +so,sow,sew +spade,spayed +stayed,staid +stare,stair +stationary,stationery +stake,steak +steel,steal +step,steppe +style,stile +straightened,straitened +straight,strait +swayed,suede +suit,soot +some,sum +summary,summery +Sunday,sundae +surf,serf +surge,serge +sweet,suite +tax,tacks +tacked,tact +tale,tail +taper,tapir +terry,tarry +taupe,tope +taught,taut +tea,tee +team,teem +tease,teas,tees +tear,tare +tens,tends +tense,tents +than,then +the,thee +there's,theirs +their,there,they're +through,threw +thrown,throne +throws,throes +time,thyme +tick,tic +tie,Thai +tied,tide +tear,tier +Tigris,tigress +timber,timbre +to,two,too +toe,tow +told,tolled +tool,tulle +tort,torte +torturous,tortuous +towed,toad,toed +tracked,tract +trader,traitor +troop,troupe +trust,trussed +turbine,turban +turn,tern +tutor,Tudor,tooter +tucks,tux +utter,udder +undo,undue +earn,urn +use,ewes,yews +veil,vale +valence,valance +vain,vein,vane +vein,vane +Venus,venous +versus,verses +very,vary +vile,vial +vice,vise +waste,waist +wait,weight +waiter,wader +whale,wail,wale +wears,where's,wares +wave,waive +wax,whacks +way,weigh,whey +we,wee +where,wear,ware +whether,weather,wether +we'd,weed +week,weak +wade,weighed +ways,weighs +wet,whet +we've,weave +Wales,whales,wails +we'll,wheel +which,witch +while,wile +were,whir +word,whirred +hoop,whoop +whose,who's +wine,whine +wind,whined,wined +whoa,woe +walk,wok +one,won +want,wont +would,wood +war,wore +world,whirled,whorled +worn,warn +rack,wrack +rapper,wrapper +wrote,rote +rye,wry +you,yew,ewe +yoke,yolk +your,you're,yore +you'll,Yule diff --git a/talon-user/code/homophones.py b/talon-user/code/homophones.py @@ -0,0 +1,167 @@ +from talon import Context, Module, app, clip, cron, imgui, actions, ui, fs +import os + +######################################################################## +# global settings +######################################################################## + +# a list of homophones where each line is a comma separated list +# e.g. where,wear,ware +# a suitable one can be found here: +# https://github.com/pimentel/homophones +cwd = os.path.dirname(os.path.realpath(__file__)) +homophones_file = os.path.join(cwd, "homophones.csv") +# if quick_replace, then when a word is selected and only one homophone exists, +# replace it without bringing up the options +quick_replace = True +show_help = False +######################################################################## + +ctx = Context() +mod = Module() +mod.mode("homophones") +mod.list("homophones_canonicals", desc="list of words ") + + +main_screen = ui.main_screen() + + +def update_homophones(name, flags): + if name != homophones_file: + return + + phones = {} + canonical_list = [] + with open(homophones_file, "r") as f: + for line in f: + words = line.rstrip().split(",") + canonical_list.append(words[0]) + for word in words: + word = word.lower() + old_words = phones.get(word, []) + phones[word] = sorted(set(old_words + words)) + + global all_homophones + all_homophones = phones + ctx.lists["self.homophones_canonicals"] = canonical_list + + +update_homophones(homophones_file, None) +fs.watch(cwd, update_homophones) +active_word_list = None +is_selection = False + + +def close_homophones(): + gui.hide() + actions.mode.disable("user.homophones") + + +def raise_homophones(word, forced=False, selection=False): + global quick_replace + global active_word_list + global show_help + global force_raise + global is_selection + + force_raise = forced + is_selection = selection + + if is_selection: + word = word.strip() + + is_capitalized = word == word.capitalize() + is_upper = word.isupper() + + word = word.lower() + + if word not in all_homophones: + app.notify("homophones.py", '"%s" not in homophones list' % word) + return + + active_word_list = all_homophones[word] + if ( + is_selection + and len(active_word_list) == 2 + and quick_replace + and not force_raise + ): + if word == active_word_list[0].lower(): + new = active_word_list[1] + else: + new = active_word_list[0] + + if is_capitalized: + new = new.capitalize() + elif is_upper: + new = new.upper() + + clip.set(new) + actions.edit.paste() + + return + + actions.mode.enable("user.homophones") + show_help = False + gui.show() + + +@imgui.open(x=main_screen.x + main_screen.width / 2.6, y=main_screen.y) +def gui(gui: imgui.GUI): + global active_word_list + if show_help: + gui.text("Homephone help - todo") + else: + gui.text("Select a homophone") + gui.line() + index = 1 + for word in active_word_list: + gui.text("Choose {}: {} ".format(index, word)) + index = index + 1 + + +def show_help_gui(): + global show_help + show_help = True + gui.show() + + +@mod.capture(rule="{self.homophones_canonicals}") +def homophones_canonical(m) -> str: + "Returns a single string" + return m.homophones_canonicals + + +@mod.action_class +class Actions: + def homophones_hide(): + """Hides the homophones display""" + close_homophones() + + def homophones_show(m: str): + """Show the homophones display""" + print(m) + raise_homophones(m, False, False) + + def homophones_show_selection(): + """Show the homophones display for the selected text""" + raise_homophones(actions.edit.selected_text(), False, True) + + def homophones_force_show(m: str): + """Show the homophones display forcibly""" + raise_homophones(m, True, False) + + def homophones_force_show_selection(): + """Show the homophones display for the selected text forcibly""" + raise_homophones(actions.edit.selected_text(), True, True) + + def homophones_select(number: int) -> str: + """selects the homophone by number""" + if number <= len(active_word_list) and number > 0: + return active_word_list[number - 1] + + error = "homophones.py index {} is out of range (1-{})".format( + number, len(active_word_list) + ) + app.notify(error) + raise error diff --git a/talon-user/code/keys.py b/talon-user/code/keys.py @@ -0,0 +1,249 @@ +from typing import Set + +from talon import Module, Context, actions, app +import sys + +default_alphabet = "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".split( + " " +) +letters_string = "abcdefghijklmnopqrstuvwxyz" + +default_digits = "zero one two three four five six seven eight nine".split(" ") +numbers = [str(i) for i in range(10)] +default_f_digits = "one two three four five six seven eight nine ten eleven twelve".split( + " " +) + +mod = Module() +mod.list("letter", desc="The spoken phonetic alphabet") +mod.list("symbol_key", desc="All symbols from the keyboard") +mod.list("arrow_key", desc="All arrow keys") +mod.list("number_key", desc="All number keys") +mod.list("modifier_key", desc="All modifier keys") +mod.list("function_key", desc="All function keys") +mod.list("special_key", desc="All special keys") +mod.list("punctuation", desc="words for inserting punctuation into text") + + +@mod.capture(rule="{self.modifier_key}+") +def modifiers(m) -> str: + "One or more modifier keys" + return "-".join(m.modifier_key_list) + + +@mod.capture(rule="{self.arrow_key}") +def arrow_key(m) -> str: + "One directional arrow key" + return m.arrow_key + + +@mod.capture(rule="<self.arrow_key>+") +def arrow_keys(m) -> str: + "One or more arrow keys separated by a space" + return str(m) + + +@mod.capture(rule="{self.number_key}") +def number_key(m) -> str: + "One number key" + return m.number_key + + +@mod.capture(rule="{self.letter}") +def letter(m) -> str: + "One letter key" + return m.letter + + +@mod.capture(rule="{self.special_key}") +def special_key(m) -> str: + "One special key" + return m.special_key + + +@mod.capture(rule="{self.symbol_key}") +def symbol_key(m) -> str: + "One symbol key" + return m.symbol_key + + +@mod.capture(rule="{self.function_key}") +def function_key(m) -> str: + "One function key" + return m.function_key + + +@mod.capture(rule="( <self.letter> | <self.number_key> | <self.symbol_key> )") +def any_alphanumeric_key(m) -> str: + "any alphanumeric key" + return str(m) + + +@mod.capture( + rule="( <self.letter> | <self.number_key> | <self.symbol_key> " + "| <self.arrow_key> | <self.function_key> | <self.special_key> )" +) +def unmodified_key(m) -> str: + "A single key with no modifiers" + return str(m) + + +@mod.capture(rule="{self.modifier_key}* <self.unmodified_key>") +def key(m) -> str: + "A single key with optional modifiers" + try: + mods = m.modifier_key_list + except AttributeError: + mods = [] + return "-".join(mods + [m.unmodified_key]) + + +@mod.capture(rule="<self.key>+") +def keys(m) -> str: + "A sequence of one or more keys with optional modifiers" + return " ".join(m.key_list) + + +@mod.capture(rule="{self.letter}+") +def letters(m) -> str: + "Multiple letter keys" + return "".join(m.letter_list) + + +ctx = Context() +modifier_keys = { + # If you find 'alt' is often misrecognized, try using 'alter'. + "alt": "alt", #'alter': 'alt', + "control": "ctrl", #'troll': 'ctrl', + "shift": "shift", #'sky': 'shift', + "super": "super", +} +if app.platform == "mac": + modifier_keys["command"] = "cmd" + modifier_keys["option"] = "alt" +ctx.lists["self.modifier_key"] = modifier_keys +alphabet = dict(zip(default_alphabet, letters_string)) +ctx.lists["self.letter"] = alphabet + +# `punctuation_words` is for words you want available BOTH in dictation and as +# key names in command mode. `symbol_key_words` is for key names that should be +# available in command mode, but NOT during dictation. +punctuation_words = { + # TODO: I'm not sure why we need these, I think it has something to do with + # Dragon. Possibly it has been fixed by later improvements to talon? -rntz + "`": "`", + ",": ",", # <== these things + "back tick": "`", + "comma": ",", + "period": ".", + "semicolon": ";", + "colon": ":", + "forward slash": "/", + "question mark": "?", + "exclamation mark": "!", + "exclamation point": "!", + "dollar sign": "$", + "asterisk": "*", + "hash sign": "#", + "number sign": "#", + "percent sign": "%", + "at sign": "@", + "and sign": "&", + "ampersand": "&", +} +symbol_key_words = { + "dot": ".", + "quote": "'", + "L square": "[", + "left square": "[", + "square": "[", + "R square": "]", + "right square": "]", + "slash": "/", + "backslash": "\\", + "minus": "-", + "dash": "-", + "equals": "=", + "plus": "+", + "tilde": "~", + "bang": "!", + "dollar": "$", + "down score": "_", + "under score": "_", + "paren": "(", + "L paren": "(", + "left paren": "(", + "R paren": ")", + "right paren": ")", + "brace": "{", + "left brace": "{", + "R brace": "}", + "right brace": "}", + "angle": "<", + "left angle": "<", + "less than": "<", + "rangle": ">", + "R angle": ">", + "right angle": ">", + "greater than": ">", + "star": "*", + "pound": "#", + "hash": "#", + "percent": "%", + "caret": "^", + "amper": "&", + "pipe": "|", + "dubquote": '"', + "double quote": '"', +} + +# make punctuation words also included in {user.symbol_keys} +symbol_key_words.update(punctuation_words) +ctx.lists["self.punctuation"] = punctuation_words +ctx.lists["self.symbol_key"] = symbol_key_words +ctx.lists["self.number_key"] = dict(zip(default_digits, numbers)) +ctx.lists["self.arrow_key"] = { + "down": "down", + "left": "left", + "right": "right", + "up": "up", +} + +simple_keys = [ + "end", + "enter", + "escape", + "home", + "insert", + "pagedown", + "pageup", + "space", + "tab", +] + +alternate_keys = { + "delete": "backspace", + "forward delete": "delete", + #'junk': 'backspace', + "page up": "pageup", + "page down": "pagedown", +} +# mac apparently doesn't have the menu key. +if app.platform in ("windows", "linux"): + alternate_keys["menu key"] = "menu" + alternate_keys["print screen"] = "printscr" + +special_keys = {k: k for k in simple_keys} +special_keys.update(alternate_keys) +ctx.lists["self.special_key"] = special_keys +ctx.lists["self.function_key"] = { + f"F {default_f_digits[i]}": f"f{i + 1}" for i in range(12) +} + + +@mod.action_class +class Actions: + def get_alphabet() -> dict: + """Provides the alphabet dictionary""" + return alphabet + diff --git a/talon-user/code/line_commands.py b/talon-user/code/line_commands.py @@ -0,0 +1,46 @@ +import os +import os.path +import requests +import time +from pathlib import Path +from talon import ctrl, ui, Module, Context, actions, clip +import tempfile + +mod = Module() + +mod.tag( + "line_commands", + desc="Tag for enabling generic line navigation and selection commands", +) + + +@mod.action_class +class Actions: + def extend_until_line(line: int): + """Extends the selection from current line to the specified line""" + + def select_range(line_start: int, line_end: int): + """Selects lines from line_start to line line_end""" + actions.edit.jump_line(line_start) + actions.edit.extend_line_end() + + number_of_lines = line_end - line_start + for i in range(0, number_of_lines): + actions.edit.extend_line_down() + actions.edit.extend_line_end() + + def extend_camel_left(): + """Extends the selection by camel/subword to the left""" + + def extend_camel_right(): + """Extends the selection by camel/subword to the right""" + + def camel_left(): + """Moves cursor to the left by camel case/subword""" + + def camel_right(): + """Move cursor to the right by camel case/subword""" + + def line_clone(line: int): + """Clones specified line at current position""" + diff --git a/talon-user/code/macro.py b/talon-user/code/macro.py @@ -0,0 +1,44 @@ +from talon import actions, Module, speech_system + +mod = Module() + +macro = [] +recording = False + + +@mod.action_class +class Actions: + def macro_record(): + """record a new macro""" + global macro + global recording + + macro = [] + recording = True + + def macro_stop(): + """stop recording""" + global recording + recording = False + + def macro_play(): + """player recorded macro""" + actions.user.macro_stop() + + # :-1 because we don't want to replay `macro play` + for words in macro[:-1]: + print(words) + actions.mimic(words) + + +def fn(d): + if not recording: + return + + if "parsed" not in d: + return + + macro.append(d["parsed"]._unmapped) + + +speech_system.register("pre:phrase", fn) diff --git a/talon-user/code/messaging.py b/talon-user/code/messaging.py @@ -0,0 +1,42 @@ +from talon import Context, actions, ui, Module, app + +mod = Module() +mod.tag("messaging", desc="Tag for generic multi-channel messaging apps") + + +@mod.action_class +class messaging_actions: + # Navigation and UI components + + def messaging_workspace_previous(): + """Move to previous workspace/server""" + + def messaging_workspace_next(): + """Move to next qorkspace/server""" + + def messaging_open_channel_picker(): + """Open channel picker""" + + def messaging_channel_previous(): + """Move to previous channel""" + + def messaging_channel_next(): + """Move to next channel""" + + def messaging_unread_previous(): + """Move to previous unread channel""" + + def messaging_unread_next(): + """Moved to next unread channel""" + + def messaging_open_search(): + """Open message search""" + + def messaging_mark_workspace_read(): + """Mark this workspace/server as read""" + + def messaging_mark_channel_read(): + """Mark this channel as read.""" + + def messaging_upload_file(): + """Upload a file as a message""" diff --git a/talon-user/code/microphone_selection.py b/talon-user/code/microphone_selection.py @@ -0,0 +1,69 @@ +from talon import actions +from talon import Module, actions, imgui, scripting, app +from talon.microphone import manager +from talon.lib import cubeb +from talon import scripting + +ctx = cubeb.Context() +mod = Module() + + +def devices_changed(device_type): + update_microphone_list() + + +microphone_device_list = [] + + +def update_microphone_list(): + global microphone_device_list + microphone_device_list = [] + for device in ctx.inputs(): + if str(device.state) == "DeviceState.ENABLED": + microphone_device_list.append(device) + + +@imgui.open() +def gui(gui: imgui.GUI): + gui.text("Select a Microphone") + gui.line() + for index, item in enumerate(microphone_device_list, 1): + if gui.button("{}. {}".format(index, item.name)): + actions.user.microphone_select(index) + + +@mod.action_class +class Actions: + def microphone_selection_toggle(): + """""" + if gui.showing: + gui.hide() + else: + gui.show() + + def microphone_select(index: int): + """Selects a micropohone""" + # print(str(index) + " " + str(len(microphone_device_list))) + if 1 <= index and index <= len(microphone_device_list): + microphone = microphone_device_list[index - 1].name + for item in manager.menu.items: + # print(item.name + " " + microphone) + if microphone in item.name: + # manager.menu_click(item) + actions.speech.set_microphone(item.name) + app.notify("Activating {}".format(item.name)) + + break + + gui.hide() + + +ctx.register("devices_changed", devices_changed) + + +def on_ready(): + update_microphone_list() + + +app.register("ready", on_ready) + diff --git a/talon-user/code/mouse.py b/talon-user/code/mouse.py @@ -0,0 +1,392 @@ +import os +import pathlib +import subprocess + +from talon import ( + Context, + Module, + actions, + app, + cron, + ctrl, + clip, + imgui, + noise, + settings, + ui, +) +from talon_plugins import eye_mouse, eye_zoom_mouse, speech +from talon_plugins.eye_mouse import config, toggle_camera_overlay, toggle_control + +key = actions.key +self = actions.self +scroll_amount = 0 +click_job = None +scroll_job = None +gaze_job = None +cancel_scroll_on_pop = True +control_mouse_forced = False + +default_cursor = { + "AppStarting": r"%SystemRoot%\Cursors\aero_working.ani", + "Arrow": r"%SystemRoot%\Cursors\aero_arrow.cur", + "Hand": r"%SystemRoot%\Cursors\aero_link.cur", + "Help": r"%SystemRoot%\Cursors\aero_helpsel.cur", + "No": r"%SystemRoot%\Cursors\aero_unavail.cur", + "NWPen": r"%SystemRoot%\Cursors\aero_pen.cur", + "Person": r"%SystemRoot%\Cursors\aero_person.cur", + "Pin": r"%SystemRoot%\Cursors\aero_pin.cur", + "SizeAll": r"%SystemRoot%\Cursors\aero_move.cur", + "SizeNESW": r"%SystemRoot%\Cursors\aero_nesw.cur", + "SizeNS": r"%SystemRoot%\Cursors\aero_ns.cur", + "SizeNWSE": r"%SystemRoot%\Cursors\aero_nwse.cur", + "SizeWE": r"%SystemRoot%\Cursors\aero_ew.cur", + "UpArrow": r"%SystemRoot%\Cursors\aero_up.cur", + "Wait": r"%SystemRoot%\Cursors\aero_busy.ani", + "Crosshair": "", + "IBeam": "", +} + +# todo figure out why notepad++ still shows the cursor sometimes. +hidden_cursor = os.path.join( + os.path.dirname(os.path.realpath(__file__)), r"Resources\HiddenCursor.cur" +) + +mod = Module() +mod.list( + "mouse_button", desc="List of mouse button words to mouse_click index parameter" +) +setting_mouse_enable_pop_click = mod.setting( + "mouse_enable_pop_click", + type=int, + default=0, + desc="Enable pop to click when control mouse is enabled.", +) +setting_mouse_enable_pop_stops_scroll = mod.setting( + "mouse_enable_pop_stops_scroll", + type=int, + default=0, + desc="When enabled, pop stops continuous scroll modes (wheel upper/downer/gaze)", +) +setting_mouse_wake_hides_cursor = mod.setting( + "mouse_wake_hides_cursor", + type=int, + default=0, + desc="When enabled, mouse wake will hide the cursor. mouse_wake enables zoom mouse.", +) +setting_mouse_hide_mouse_gui = mod.setting( + "mouse_hide_mouse_gui", + type=int, + default=0, + desc="When enabled, the 'Scroll Mouse' GUI will not be shown.", +) +setting_mouse_continuous_scroll_amount = mod.setting( + "mouse_continuous_scroll_amount", + type=int, + default=80, + desc="The default amount used when scrolling continuously", +) +setting_mouse_wheel_down_amount = mod.setting( + "mouse_wheel_down_amount", + type=int, + default=120, + desc="The amount to scroll up/down (equivalent to mouse wheel on Windows by default)", +) + +continuous_scoll_mode = "" + + +@imgui.open(x=700, y=0) +def gui_wheel(gui: imgui.GUI): + gui.text("Scroll mode: {}".format(continuous_scoll_mode)) + gui.line() + if gui.button("Wheel Stop [stop scrolling]"): + actions.user.mouse_scroll_stop() + + +@mod.action_class +class Actions: + def mouse_show_cursor(): + """Shows the cursor""" + show_cursor_helper(True) + + def mouse_hide_cursor(): + """Hides the cursor""" + show_cursor_helper(False) + + def mouse_wake(): + """Enable control mouse, zoom mouse, and disables cursor""" + eye_zoom_mouse.toggle_zoom_mouse(True) + # eye_mouse.control_mouse.enable() + if setting_mouse_wake_hides_cursor.get() >= 1: + show_cursor_helper(False) + + def mouse_calibrate(): + """Start calibration""" + eye_mouse.calib_start() + + def mouse_toggle_control_mouse(): + """Toggles control mouse""" + toggle_control(not config.control_mouse) + + def mouse_toggle_camera_overlay(): + """Toggles camera overlay""" + toggle_camera_overlay(not config.show_camera) + + def mouse_toggle_zoom_mouse(): + """Toggles zoom mouse""" + eye_zoom_mouse.toggle_zoom_mouse(not eye_zoom_mouse.zoom_mouse.enabled) + + def mouse_cancel_zoom_mouse(): + """Cancel zoom mouse if pending""" + if ( + eye_zoom_mouse.zoom_mouse.enabled + and eye_zoom_mouse.zoom_mouse.state != eye_zoom_mouse.STATE_IDLE + ): + eye_zoom_mouse.zoom_mouse.cancel() + + def mouse_trigger_zoom_mouse(): + """Trigger zoom mouse if enabled""" + if eye_zoom_mouse.zoom_mouse.enabled: + eye_zoom_mouse.zoom_mouse.on_pop(eye_zoom_mouse.zoom_mouse.state) + + def mouse_drag(): + """(TEMPORARY) Press and hold/release button 0 depending on state for dragging""" + # todo: fixme temporary fix for drag command + button_down = len(list(ctrl.mouse_buttons_down())) > 0 + print(str(ctrl.mouse_buttons_down())) + if not button_down: + # print("start drag...") + ctrl.mouse_click(button=0, down=True) + # app.notify("drag started") + else: + # print("end drag...") + ctrl.mouse_click(button=0, up=True) + + # app.notify("drag stopped") + + def mouse_sleep(): + """Disables control mouse, zoom mouse, and re-enables cursor""" + eye_zoom_mouse.toggle_zoom_mouse(False) + toggle_control(False) + show_cursor_helper(True) + stop_scroll() + + # todo: fixme temporary fix for drag command + button_down = len(list(ctrl.mouse_buttons_down())) > 0 + if button_down: + ctrl.mouse_click(button=0, up=True) + + def mouse_scroll_down(): + """Scrolls down""" + mouse_scroll(setting_mouse_wheel_down_amount.get())() + + def mouse_scroll_down_continuous(): + """Scrolls down continuously""" + global continuous_scoll_mode + continuous_scoll_mode = "scroll down continuous" + mouse_scroll(setting_mouse_continuous_scroll_amount.get())() + + if scroll_job is None: + start_scroll() + + if setting_mouse_hide_mouse_gui.get() == 0: + gui_wheel.show() + + def mouse_scroll_up(): + """Scrolls up""" + mouse_scroll(-setting_mouse_wheel_down_amount.get())() + + def mouse_scroll_up_continuous(): + """Scrolls up continuously""" + global continuous_scoll_mode + continuous_scoll_mode = "scroll up continuous" + mouse_scroll(-setting_mouse_continuous_scroll_amount.get())() + + if scroll_job is None: + start_scroll() + if setting_mouse_hide_mouse_gui.get() == 0: + gui_wheel.show() + + def mouse_scroll_stop(): + """Stops scrolling""" + stop_scroll() + + def mouse_gaze_scroll(): + """Starts gaze scroll""" + global continuous_scoll_mode + continuous_scoll_mode = "gaze scroll" + + start_cursor_scrolling() + if setting_mouse_hide_mouse_gui.get() == 0: + gui_wheel.show() + + # enable 'control mouse' if eye tracker is present and not enabled already + global control_mouse_forced + if eye_mouse.tracker is not None and not config.control_mouse: + toggle_control(True) + control_mouse_forced = True + + def copy_mouse_position(): + """Copy the current mouse position coordinates""" + position = ctrl.mouse_pos() + clip.set_text((repr(position))) + + def mouse_move_center_active_window(): + """move the mouse cursor to the center of the currently active window""" + rect = ui.active_window().rect + ctrl.mouse_move(rect.left + (rect.width / 2), rect.top + (rect.height / 2)) + + +def show_cursor_helper(show): + """Show/hide the cursor""" + if app.platform == "windows": + import ctypes + import winreg + + import win32con + + try: + Registrykey = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, r"Control Panel\Cursors", 0, winreg.KEY_WRITE + ) + + for value_name, value in default_cursor.items(): + if show: + winreg.SetValueEx( + Registrykey, value_name, 0, winreg.REG_EXPAND_SZ, value + ) + else: + winreg.SetValueEx( + Registrykey, value_name, 0, winreg.REG_EXPAND_SZ, hidden_cursor + ) + + winreg.CloseKey(Registrykey) + + ctypes.windll.user32.SystemParametersInfoA( + win32con.SPI_SETCURSORS, 0, None, 0 + ) + + except WindowsError: + print("Unable to show_cursor({})".format(str(show))) + else: + ctrl.cursor_visible(show) + + +def on_pop(active): + if setting_mouse_enable_pop_stops_scroll.get() >= 1 and (gaze_job or scroll_job): + stop_scroll() + elif ( + not eye_zoom_mouse.zoom_mouse.enabled + and eye_mouse.mouse.attached_tracker is not None + ): + if setting_mouse_enable_pop_click.get() >= 1: + ctrl.mouse_click(button=0, hold=16000) + + +noise.register("pop", on_pop) + + +def mouse_scroll(amount): + def scroll(): + global scroll_amount + if (scroll_amount >= 0) == (amount >= 0): + scroll_amount += amount + else: + scroll_amount = amount + actions.mouse_scroll(y=int(amount)) + + return scroll + + +def scroll_continuous_helper(): + global scroll_amount + # print("scroll_continuous_helper") + if scroll_amount and ( + eye_zoom_mouse.zoom_mouse.state == eye_zoom_mouse.STATE_IDLE + ): # or eye_zoom_mouse.zoom_mouse.state == eye_zoom_mouse.STATE_SLEEP): + actions.mouse_scroll(by_lines=False, y=int(scroll_amount / 10)) + + +def start_scroll(): + global scroll_job + scroll_job = cron.interval("60ms", scroll_continuous_helper) + # if eye_zoom_mouse.zoom_mouse.enabled and eye_mouse.mouse.attached_tracker is not None: + # eye_zoom_mouse.zoom_mouse.sleep(True) + + +def gaze_scroll(): + # print("gaze_scroll") + if ( + eye_zoom_mouse.zoom_mouse.state == eye_zoom_mouse.STATE_IDLE + ): # or eye_zoom_mouse.zoom_mouse.state == eye_zoom_mouse.STATE_SLEEP: + x, y = ctrl.mouse_pos() + + # the rect for the window containing the mouse + rect = None + + # on windows, check the active_window first since ui.windows() is not z-ordered + if app.platform == "windows" and ui.active_window().rect.contains(x, y): + rect = ui.active_window().rect + else: + windows = ui.windows() + for w in windows: + if w.rect.contains(x, y): + rect = w.rect + break + + if rect is None: + # print("no window found!") + return + + midpoint = rect.y + rect.height / 2 + amount = int(((y - midpoint) / (rect.height / 10)) ** 3) + actions.mouse_scroll(by_lines=False, y=amount) + + # print(f"gaze_scroll: {midpoint} {rect.height} {amount}") + + +def stop_scroll(): + global scroll_amount, scroll_job, gaze_job + scroll_amount = 0 + if scroll_job: + cron.cancel(scroll_job) + + if gaze_job: + cron.cancel(gaze_job) + + global control_mouse_forced + if control_mouse_forced and config.control_mouse: + toggle_control(False) + control_mouse_forced = False + + scroll_job = None + gaze_job = None + gui_wheel.hide() + + # if eye_zoom_mouse.zoom_mouse.enabled and eye_mouse.mouse.attached_tracker is not None: + # eye_zoom_mouse.zoom_mouse.sleep(False) + + +def start_cursor_scrolling(): + global scroll_job, gaze_job + stop_scroll() + gaze_job = cron.interval("60ms", gaze_scroll) + # if eye_zoom_mouse.zoom_mouse.enabled and eye_mouse.mouse.attached_tracker is not None: + # eye_zoom_mouse.zoom_mouse.sleep(True) + + +if app.platform == "mac": + from talon import tap + + def on_move(e): + if not config.control_mouse: + buttons = ctrl.mouse_buttons_down() + # print(str(ctrl.mouse_buttons_down())) + if not e.flags & tap.DRAG and buttons: + e.flags |= tap.DRAG + # buttons is a set now + e.button = list(buttons)[0] + e.modify() + + tap.register(tap.MMOVE | tap.HOOK, on_move) diff --git a/talon-user/code/multiple_cursors.py b/talon-user/code/multiple_cursors.py @@ -0,0 +1,32 @@ +from talon import Context, actions, ui, Module, app + +mod = Module() +mod.tag("multiple_cursors", desc="Tag for enabling generic multiple cursor commands") + + +@mod.action_class +class multiple_cursor_actions: + def multi_cursor_enable(): + """Enables multi-cursor mode""" + + def multi_cursor_disable(): + """Disables multi-cursor mode""" + + def multi_cursor_add_above(): + """Adds cursor to line above""" + + def multi_cursor_add_below(): + """Adds cursor to line below""" + + def multi_cursor_select_fewer_occurrences(): + """Removes selection & cursor at last occurrence""" + + def multi_cursor_select_more_occurrences(): + """Adds cursor at next occurrence of selection""" + + def multi_cursor_select_all_occurrences(): + """Adds cursor at every occurrence of selection""" + + def multi_cursor_add_to_line_ends(): + """Adds cursor at end of every selected line""" + diff --git a/talon-user/numbers.py b/talon-user/code/numbers.py diff --git a/talon-user/code/ordinals.py b/talon-user/code/ordinals.py @@ -0,0 +1,87 @@ +from talon import Context, Module, actions, app, ui + + +def ordinal(n): + """ + Convert an integer into its ordinal representation:: + ordinal(0) => '0th' + ordinal(3) => '3rd' + ordinal(122) => '122nd' + ordinal(213) => '213th' + """ + n = int(n) + suffix = ["th", "st", "nd", "rd", "th"][min(n % 10, 4)] + if 11 <= (n % 100) <= 13: + suffix = "th" + return str(n) + suffix + + +# The primitive ordinal words in English below a hundred. +ordinal_words = { + 0: "zeroth", + 1: "first", + 2: "second", + 3: "third", + 4: "fourth", + 5: "fifth", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", + 10: "tenth", + 11: "eleventh", + 12: "twelfth", + 13: "thirteenth", + 14: "fourteenth", + 15: "fifteenth", + 16: "sixteenth", + 17: "seventeenth", + 18: "eighteenth", + 19: "nineteenth", + 20: "twentieth", + 30: "thirtieth", + 40: "fortieth", + 50: "fiftieth", + 60: "sixtieth", + 70: "seventieth", + 80: "eightieth", + 90: "ninetieth", +} +tens_words = "zero ten twenty thirty forty fifty sixty seventy eighty ninety".split() + +# ordinal_numbers maps ordinal words into their corresponding numbers. +ordinal_numbers = {} +ordinal_small = {} +for n in range(1, 100): + if n in ordinal_words: + word = ordinal_words[n] + else: + (tens, units) = divmod(n, 10) + assert 1 < tens < 10, "we have already handled all ordinals < 20" + assert 0 < units, "we have already handled all ordinals divisible by ten" + word = f"{tens_words[tens]} {ordinal_words[units]}" + + if n <= 20: + ordinal_small[word] = n + ordinal_numbers[word] = n + + +mod = Module() +ctx = Context() +mod.list("ordinals", desc="list of ordinals") +mod.list("ordinals_small", desc="list of ordinals small (1-20)") + +ctx.lists["self.ordinals"] = ordinal_numbers.keys() +ctx.lists["self.ordinals_small"] = ordinal_small.keys() + + +@mod.capture(rule="{self.ordinals}") +def ordinals(m) -> int: + """Returns a single ordinal as a digit""" + return int(ordinal_numbers[m[0]]) + + +@mod.capture(rule="{self.ordinals_small}") +def ordinals_small(m) -> int: + """Returns a single ordinal as a digit""" + return int(ordinal_numbers[m[0]]) diff --git a/talon-user/code/phrase_history.py b/talon-user/code/phrase_history.py @@ -0,0 +1,60 @@ +from talon import Module, actions, imgui +import logging + +mod = Module() + +# list of recent phrases, most recent first +phrase_history = [] +phrase_history_length = 40 +phrase_history_display_length = 40 + +@mod.action_class +class Actions: + def get_last_phrase() -> str: + """Gets the last phrase""" + return phrase_history[0] if phrase_history else "" + + def get_recent_phrase(number: int) -> str: + """Gets the nth most recent phrase""" + try: return phrase_history[number-1] + except IndexError: return "" + + def clear_last_phrase(): + """Clears the last phrase""" + # Currently, this removes the cleared phrase from the phrase history, so + # that repeated calls clear successively earlier phrases, which is often + # useful. But it would be nice if we could do this without removing + # those phrases from the history entirely, so that they were still + # accessible for copying, for example. + if not phrase_history: + logging.warning("clear_last_phrase(): No last phrase to clear!") + return + for _ in phrase_history[0]: + actions.edit.delete() + phrase_history.pop(0) + + def select_last_phrase(): + """Selects the last phrase""" + if not phrase_history: + logging.warning("select_last_phrase(): No last phrase to select!") + return + for _ in phrase_history[0]: + actions.edit.extend_left() + + def add_phrase_to_history(text: str): + """Adds a phrase to the phrase history""" + global phrase_history + phrase_history.insert(0, text) + phrase_history = phrase_history[:phrase_history_length] + + def toggle_phrase_history(): + """Toggles list of recent phrases""" + if gui.showing: gui.hide() + else: gui.show() + +@imgui.open() +def gui(gui: imgui.GUI): + gui.text("Recent phrases") + gui.line() + for index, text in enumerate(phrase_history[:phrase_history_display_length], 1): + gui.text(f"{index}: {text}") diff --git a/talon-user/code/screenshot.py b/talon-user/code/screenshot.py @@ -0,0 +1,83 @@ +from talon import Module, screen, ui, actions, clip, app, settings +from datetime import datetime +import os, subprocess + +active_platform = app.platform +default_command = None +if active_platform == "windows": + + default_folder = os.path.expanduser(os.path.join("~", r"OneDrive\Desktop")) + # this is probably not the correct way to check for onedrive, quick and dirty + if not os.path.isdir(default_folder): + default_folder = os.path.join("~", "Desktop") +elif active_platform == "mac": + default_folder = os.path.join("~", "Desktop") +elif active_platform == "linux": + default_folder = "~" + default_command = "scrot -s" + +mod = Module() +screenshot_folder = mod.setting( + "screenshot_folder", + type=str, + default=default_folder, + desc="Where to save screenshots. Note this folder must exist.", +) +screenshot_selection_command = mod.setting( + "screenshot_selection_command", + type=str, + default=default_command, + desc="Commandline trigger for taking a selection of the screen. By default, only linux uses this.", +) + + +def get_screenshot_path(): + filename = "screenshot-%s.png" % datetime.now().strftime("%Y%m%d%H%M%S") + folder_path = screenshot_folder.get() + path = os.path.expanduser(os.path.join(folder_path, filename)) + return os.path.normpath(path) + + +@mod.action_class +class Actions: + def screenshot(): + """takes a screenshot of the entire screen and saves it to the desktop as screenshot.png""" + img = screen.capture_rect(screen.main_screen().rect) + path = get_screenshot_path() + + img.write_file(path) + app.notify(subtitle="Screenshot: %s" % path) + + def screenshot_window(): + """takes a screenshot of the current window and says it to the desktop as screenshot.png""" + img = screen.capture_rect(ui.active_window().rect) + path = get_screenshot_path() + img.write_file(path) + app.notify(subtitle="Screenshot: %s" % path) + + def screenshot_selection(): + """triggers an application is capable of taking a screenshot of a portion of the screen""" + command = screenshot_selection_command.get() + if command: + path = get_screenshot_path() + command = command.split() + command.append(path) + subprocess.Popen(command) + app.notify(subtitle="Screenshot: %s" % path) + else: + if active_platform == "windows": + actions.key("super-shift-s") + elif active_platform == "mac": + actions.key("ctrl-shift-cmd-4") + # linux is handled by the command by default + # elif active_platform == "linux": + + def screenshot_clipboard(): + """takes a screenshot of the entire screen and saves it to the clipboard""" + img = screen.capture_rect(screen.main_screen().rect) + clip.set_image(img) + + def screenshot_window_clipboard(): + """takes a screenshot of the window and saves it to the clipboard""" + img = screen.capture_rect(ui.active_window().rect) + clip.set_image(img) diff --git a/talon-user/code/search_engines.py b/talon-user/code/search_engines.py @@ -0,0 +1,33 @@ +from .user_settings import get_list_from_csv +from talon import Module, Context +from urllib.parse import quote_plus +import webbrowser + +mod = Module() +mod.list( + "search_engine", + desc="A search engine. Any instance of %s will be replaced by query text", +) + +_search_engine_defaults = { + "amazon": "https://www.amazon.com/s/?field-keywords=%s", + "google": "https://www.google.com/search?q=%s", + "map": "https://maps.google.com/maps?q=%s", + "scholar": "https://scholar.google.com/scholar?q=%s", + "wiki": "https://en.wikipedia.org/w/index.php?search=%s", +} + +ctx = Context() +ctx.lists["self.search_engine"] = get_list_from_csv( + "search_engines.csv", + headers=("URL Template", "Name"), + default=_search_engine_defaults, +) + + +@mod.action_class +class Actions: + def search_with_search_engine(search_template: str, search_text: str): + """Search a search engine for given text""" + url = search_template.replace("%s", quote_plus(search_text)) + webbrowser.open(url)+ \ No newline at end of file diff --git a/talon-user/code/snippet_watcher.py b/talon-user/code/snippet_watcher.py @@ -0,0 +1,95 @@ +# from talon import app, fs +# import os, csv, re +# from os.path import isfile, join +# from itertools import islice +# from pathlib import Path +# import json +# from jsoncomment import JsonComment + +# parser = JsonComment(json) + +# pattern = re.compile(r"[A-Z][a-z]*|[a-z]+|\d") + +# # todo: should this be an action that lives elsewhere?? +# def create_spoken_form(text, max_len=15): +# return " ".join(list(islice(pattern.findall(text), max_len))) + + +# class snippet_watcher: +# directories = {} +# snippet_dictionary = {} +# callback_function = None +# file_snippet_cache = {} + +# def __notify(self): +# # print("NOTIFY") +# self.snippet_dictionary = {} +# for key, val in self.file_snippet_cache.items(): +# self.snippet_dictionary.update(val) + +# # print(str(self.snippet_dictionary)) +# if self.callback_function: +# self.callback_function(self.snippet_dictionary) + +# def __update_all_snippets(self): +# for directory, file_list in self.directories.items(): +# if os.path.isdir(directory): +# for f in file_list: +# path = os.path.join(directory, f) +# self.__process_file(path) + +# # print(str(self.snippet_dictionary)) +# self.__notify() + +# def __process_file(self, name): +# path_obj = Path(name) +# directory = os.path.normpath(path_obj.parents[0]) +# file_name = path_obj.name +# file_type = path_obj.suffix +# self.file_snippet_cache[str(path_obj)] = {} + +# print("{}, {}, {}, {}".format(name, directory, file_name, file_type)) +# if directory in self.directories and file_name in self.directories[directory]: +# if file_type.lower() == ".json": +# jsonDict = {} + +# if os.path.isfile(name): +# with open(name, "r") as f: +# jsonDict = parser.load(f) +# # else: +# # print("snippet_watcher.py: File {} does not exist".format(directory)) + +# for key, data in jsonDict.items(): +# self.file_snippet_cache[str(path_obj)][ +# create_spoken_form(key) +# ] = data["prefix"] + +# def __on_fs_change(self, name, flags): +# self.__process_file(name) + +# # print(str(self.snippet_dictionary)) +# self.__notify() + +# def __init__(self, dirs, callback): +# self.directories = dirs +# self.callback_function = callback +# self.snippet_dictionary = {} +# self.file_snippet_cache = {} +# # none = process all directories +# self.__update_all_snippets() + +# for directory in self.directories.keys(): +# if os.path.isdir(directory): +# fs.watch(directory, self.__on_fs_change) +# # else: +# # print( +# # "snippet_watcher.py: directory {} does not exist".format(directory) +# # ) + + +# # Test = snippet_watcher( +# # {os.path.expandvars(r"%AppData%\Code\User\snippets"): ["python.json"]}, +# # None +# # # {os.path.expandvars(r"%USERPROFILE%\.vscode\extensions\ms-dotnettools.csharp-1.22.1\snippets": ["csharp.json"]}, +# # ) +# # print(str(Test.directories)) diff --git a/talon-user/code/snippets.py b/talon-user/code/snippets.py @@ -0,0 +1,41 @@ +# defines placeholder actions and captures for ide-specific snippet functionality +from talon import Module, actions, app, Context, imgui, registry + +mod = Module() +mod.tag("snippets", desc="Tag for enabling code snippet-related commands") +mod.list("snippets", desc="List of code snippets") + + +@imgui.open() +def gui(gui: imgui.GUI): + gui.text("snippets") + gui.line() + + if "user.snippets" in registry.lists: + function_list = sorted(registry.lists["user.snippets"][0].keys()) + # print(str(registry.lists["user.snippets"])) + + # print(str(registry.lists["user.code_functions"])) + if function_list: + for i, entry in enumerate(function_list): + gui.text("{}".format(entry, function_list)) + + +@mod.action_class +class Actions: + def snippet_search(text: str): + """Triggers the program's snippet search""" + + def snippet_insert(text: str): + """Inserts a snippet""" + + def snippet_create(): + """Triggers snippet creation""" + + def snippet_toggle(): + """Toggles UI for available snippets""" + if gui.showing: + gui.hide() + else: + gui.show() + diff --git a/talon-user/code/splits.py b/talon-user/code/splits.py @@ -0,0 +1,46 @@ +from talon import Module, actions, app + +mod = Module() +mod.tag("splits", desc="Tag for enabling generic window split commands") + + +@mod.action_class +class Actions: + def split_window_right(): + """Move active tab to right split""" + + def split_window_left(): + """Move active tab to left split""" + + def split_window_down(): + """Move active tab to lower split""" + + def split_window_up(): + """Move active tab to upper split""" + + def split_window_vertically(): + """Splits window vertically""" + + def split_window_horizontally(): + """Splits window horizontally""" + + def split_flip(): + """Flips the orietation of the active split""" + + def split_window(): + """Splits the window""" + + def split_clear(): + """Clears the current split""" + + def split_clear_all(): + """Clears all splits""" + + def split_next(): + """Goes to next split""" + + def split_last(): + """Goes to last split""" + + def split_number(index: int): + """Navigates to a the specified split""" diff --git a/talon-user/code/sql.py b/talon-user/code/sql.py diff --git a/talon-user/code/switcher.py b/talon-user/code/switcher.py @@ -0,0 +1,383 @@ +import os +import re +import time + +import talon +from talon import Context, Module, app, imgui, ui, fs, actions +from glob import glob +from itertools import islice +from pathlib import Path + +# Construct at startup a list of overides for application names (similar to how homophone list is managed) +# ie for a given talon recognition word set `one note`, recognized this in these switcher functions as `ONENOTE` +# the list is a comma seperated `<Recognized Words>, <Overide>` +# TODO: Consider put list csv's (homophones.csv, app_name_overrides.csv) files together in a seperate directory,`knausj_talon/lists` +cwd = os.path.dirname(os.path.realpath(__file__)) +overrides_directory = os.path.join(cwd, "app_names") +override_file_name = f"app_name_overrides.{talon.app.platform}.csv" +override_file_path = os.path.join(overrides_directory, override_file_name) + + +mod = Module() +mod.list("running", desc="all running applications") +mod.list("launch", desc="all launchable applications") +ctx = Context() + +# a list of the current overrides +overrides = {} + +# a list of the currently running application names +running_application_dict = {} + + +mac_application_directories = [ + "/Applications", + "/Applications/Utilities", + "/System/Applications", + "/System/Applications/Utilities", +] + +# windows_application_directories = [ +# "%AppData%/Microsoft/Windows/Start Menu/Programs", +# "%ProgramData%/Microsoft/Windows/Start Menu/Programs", +# "%AppData%/Microsoft/Internet Explorer/Quick Launch/User Pinned/TaskBar", +# ] + +words_to_exclude = [ + "and", + "zero", + "one", + "two", + "three", + "for", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "microsoft", + "windows", + "Windows", +] + +# windows-specific logic +if app.platform == "windows": + import os + import ctypes + import pywintypes + import pythoncom + import winerror + + try: + import winreg + except ImportError: + # Python 2 + import _winreg as winreg + + bytes = lambda x: str(buffer(x)) + + from ctypes import wintypes + from win32com.shell import shell, shellcon + from win32com.propsys import propsys, pscon + + # KNOWNFOLDERID + # https://msdn.microsoft.com/en-us/library/dd378457 + # win32com defines most of these, except the ones added in Windows 8. + FOLDERID_AppsFolder = pywintypes.IID("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}") + + # win32com is missing SHGetKnownFolderIDList, so use ctypes. + + _ole32 = ctypes.OleDLL("ole32") + _shell32 = ctypes.OleDLL("shell32") + + _REFKNOWNFOLDERID = ctypes.c_char_p + _PPITEMIDLIST = ctypes.POINTER(ctypes.c_void_p) + + _ole32.CoTaskMemFree.restype = None + _ole32.CoTaskMemFree.argtypes = (wintypes.LPVOID,) + + _shell32.SHGetKnownFolderIDList.argtypes = ( + _REFKNOWNFOLDERID, # rfid + wintypes.DWORD, # dwFlags + wintypes.HANDLE, # hToken + _PPITEMIDLIST, + ) # ppidl + + def get_known_folder_id_list(folder_id, htoken=None): + if isinstance(folder_id, pywintypes.IIDType): + folder_id = bytes(folder_id) + pidl = ctypes.c_void_p() + try: + _shell32.SHGetKnownFolderIDList(folder_id, 0, htoken, ctypes.byref(pidl)) + return shell.AddressAsPIDL(pidl.value) + except WindowsError as e: + if e.winerror & 0x80070000 == 0x80070000: + # It's a WinAPI error, so re-raise it, letting Python + # raise a specific exception such as FileNotFoundError. + raise ctypes.WinError(e.winerror & 0x0000FFFF) + raise + finally: + if pidl: + _ole32.CoTaskMemFree(pidl) + + def enum_known_folder(folder_id, htoken=None): + id_list = get_known_folder_id_list(folder_id, htoken) + folder_shell_item = shell.SHCreateShellItem(None, None, id_list) + items_enum = folder_shell_item.BindToHandler( + None, shell.BHID_EnumItems, shell.IID_IEnumShellItems + ) + result = [] + for item in items_enum: + # print(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) + result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) + + return result + + def list_known_folder(folder_id, htoken=None): + result = [] + for item in enum_known_folder(folder_id, htoken): + result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) + result.sort(key=lambda x: x.upper()) + return result + + +@mod.capture(rule="{self.running}") # | <user.text>)") +def running_applications(m) -> str: + "Returns a single application name" + try: + return m.running + except AttributeError: + return m.text + + +@mod.capture(rule="{self.launch}") +def launch_applications(m) -> str: + "Returns a single application name" + return m.launch + + +def split_camel(word): + return re.findall(r"[0-9A-Z]*[a-z]+(?=[A-Z]|$)", word) + + +def get_words(name): + words = re.findall(r"[0-9A-Za-z]+", name) + out = [] + for word in words: + out += split_camel(word) + return out + + +def update_lists(): + global running_application_dict + running_application_dict = {} + running = {} + for cur_app in ui.apps(background=False): + name = cur_app.name + + if name.endswith(".exe"): + name = name.rsplit(".", 1)[0] + + words = get_words(name) + for word in words: + if word and word not in running and len(word) >= 3: + running[word.lower()] = cur_app.name + + running[name.lower()] = cur_app.name + running_application_dict[cur_app.name] = True + + for override in overrides: + running[override] = overrides[override] + + lists = { + "self.running": running, + # "self.launch": launch, + } + + # batch update lists + ctx.lists.update(lists) + + +def update_overrides(name, flags): + """Updates the overrides list""" + global overrides + overrides = {} + + if name is None or name == override_file_path: + # print("update_overrides") + with open(override_file_path, "r") as f: + for line in f: + line = line.rstrip() + line = line.split(",") + if len(line) == 2: + overrides[line[0].lower()] = line[1].strip() + + update_lists() + + +pattern = re.compile(r"[A-Z][a-z]*|[a-z]+|\d|[+]") + +# todo: this is garbage +def create_spoken_forms(name, max_len=30): + result = " ".join(list(islice(pattern.findall(name), max_len))) + + result = ( + result.replace("0", "zero") + .replace("1", "one") + .replace("2", "two") + .replace("3", "three") + .replace("4", "four") + .replace("5", "five") + .replace("6", "six") + .replace("7", "seven") + .replace("8", "eight") + .replace("9", "nine") + .replace("+", "plus") + ) + return result + + +@mod.action_class +class Actions: + def get_running_app(name: str) -> ui.App: + """Get the first available running app with `name`.""" + # We should use the capture result directly if it's already in the list + # of running applications. Otherwise, name is from <user.text> and we + # can be a bit fuzzier + if name not in running_application_dict: + if len(name) < 3: + raise RuntimeError( + f'Skipped getting app: "{name}" has less than 3 chars.' + ) + for running_name, full_application_name in ctx.lists[ + "self.running" + ].items(): + if running_name == name or running_name.lower().startswith( + name.lower() + ): + name = full_application_name + break + for app in ui.apps(): + if app.name == name and not app.background: + return app + raise RuntimeError(f'App not running: "{name}"') + + def switcher_focus(name: str): + """Focus a new application by name""" + app = actions.user.get_running_app(name) + app.focus() + + # Hacky solution to do this reliably on Mac. + timeout = 5 + t1 = time.monotonic() + if talon.app.platform == "mac": + while ui.active_app() != app and time.monotonic() - t1 < timeout: + time.sleep(0.1) + + def switcher_launch(path: str): + """Launch a new application by path""" + if app.platform == "windows": + is_valid_path = False + try: + current_path = Path(path) + is_valid_path = current_path.is_file() + # print("valid path: {}".format(is_valid_path)) + + except: + # print("invalid path") + is_valid_path = False + + if is_valid_path: + # print("path: " + path) + ui.launch(path=path) + + else: + # print("envelop") + actions.key("super-s") + actions.sleep("300ms") + actions.insert("apps: {}".format(path)) + actions.sleep("150ms") + actions.key("enter") + + else: + ui.launch(path=path) + + def switcher_toggle_running(): + """Shows/hides all running applications""" + if gui.showing: + gui.hide() + else: + gui.show() + + def switcher_hide_running(): + """Hides list of running applications""" + gui.hide() + + +@imgui.open() +def gui(gui: imgui.GUI): + gui.text("Names of running applications") + gui.line() + for line in ctx.lists["self.running"]: + gui.text(line) + + +def update_launch_list(): + launch = {} + if app.platform == "mac": + for base in mac_application_directories: + if os.path.isdir(base): + for name in os.listdir(base): + path = os.path.join(base, name) + name = name.rsplit(".", 1)[0].lower() + launch[name] = path + words = name.split(" ") + for word in words: + if word and word not in launch: + if len(name) > 6 and len(word) < 3: + continue + launch[word] = path + + elif app.platform == "windows": + shortcuts = enum_known_folder(FOLDERID_AppsFolder) + # str(shortcuts) + for name in shortcuts: + # print("hit: " + name) + # print(name) + # name = path.rsplit("\\")[-1].split(".")[0].lower() + if "install" not in name: + spoken_form = create_spoken_forms(name) + # print(spoken_form) + launch[spoken_form] = name + words = spoken_form.split(" ") + for word in words: + if word not in words_to_exclude and word not in launch: + if len(name) > 6 and len(word) < 3: + continue + launch[word] = name + + ctx.lists["self.launch"] = launch + + +def ui_event(event, arg): + if event in ("app_launch", "app_close"): + update_lists() + + +# Currently update_launch_list only does anything on mac, so we should make sure +# to initialize user launch to avoid getting "List not found: user.launch" +# errors on other platforms. +ctx.lists["user.launch"] = {} +ctx.lists["user.running"] = {} + +# Talon starts faster if you don't use the `talon.ui` module during launch +def on_ready(): + update_overrides(None, None) + fs.watch(overrides_directory, update_overrides) + update_launch_list() + ui.register("", ui_event) + + +# NOTE: please update this from "launch" to "ready" in Talon v0.1.5 +app.register("ready", on_ready) diff --git a/talon-user/code/tabs.py b/talon-user/code/tabs.py @@ -0,0 +1,11 @@ +from talon import Context, actions, ui, Module, app + +mod = Module() + +@mod.action_class +class tab_actions: + def tab_jump(number: int): + """Jumps to the specified tab""" + + def tab_final(): + """Jumps to the final tab""" diff --git a/talon-user/code/tags.py b/talon-user/code/tags.py @@ -0,0 +1,17 @@ +from talon import Context, Module + +mod = Module() + +tagList = [ + "debugger", + "disassembler", + "gdb", + "git", # commandline tag for git commands + "ida", + "tabs", + "taskwarrior", # commandline tag for taskwarrior commands + "tmux", + "windbg", +] +for entry in tagList: + mod.tag(entry, f"tag to load {entry} and/or related plugins ") diff --git a/talon-user/code/talon_helpers.py b/talon-user/code/talon_helpers.py @@ -0,0 +1,55 @@ +from talon import Context, actions, ui, Module, app, clip +import os +import re +from itertools import islice + + +mod = Module() +pattern = re.compile(r"[A-Z][a-z]*|[a-z]+|\d") + +# todo: should this be an action that lives elsewhere?? +def create_name(text, max_len=20): + return "_".join(list(islice(pattern.findall(text), max_len))).lower() + + +@mod.action_class +class Actions: + def talon_add_context_clipboard_python(): + """Adds os-specific context info to the clipboard for the focused app for .py files. Assumes you've a Module named mod declared.""" + friendly_name = actions.app.name() + # print(actions.app.executable()) + executable = actions.app.executable().split(os.path.sep)[-1] + app_name = create_name(friendly_name.replace(".exe", "")) + if app.platform == "mac": + result = 'mod.apps.{} = """\nos: {}\nand app.bundle: {}\n"""'.format( + app_name, app.platform, actions.app.bundle() + ) + elif app.platform == "windows": + result = 'mod.apps.{} = """\nos: windows\nand app.name: {}\nos: windows\nand app.exe: {}\n"""'.format( + app_name, friendly_name, executable + ) + else: + result = 'mod.apps.{} = """\nos: {}\nand app.name: {}\n"""'.format( + app_name, app.platform, friendly_name + ) + + clip.set_text(result) + + def talon_add_context_clipboard(): + """Adds os-specific context info to the clipboard for the focused app for .talon files""" + friendly_name = actions.app.name() + # print(actions.app.executable()) + executable = actions.app.executable().split(os.path.sep)[-1] + if app.platform == "mac": + result = "os: {}\nand app.bundle: {}\n".format( + app.platform, actions.app.bundle() + ) + elif app.platform == "windows": + result = "os: windows\nand app.name: {}\nos: windows\nand app.exe: {}\n".format( + friendly_name, executable + ) + else: + result = "os: {}\nand app.name: {}\n".format(app.platform, friendly_name) + + clip.set_text(result) + diff --git a/talon-user/code/user_settings.py b/talon-user/code/user_settings.py @@ -0,0 +1,60 @@ +import csv +import os +from pathlib import Path +from typing import Dict, List, Tuple +from talon import resource + +# NOTE: This method requires this module to be one folder below the top-level +# knausj folder. +SETTINGS_DIR = Path(__file__).parents[1] / "settings" + +if not SETTINGS_DIR.is_dir(): + os.mkdir(SETTINGS_DIR) + + +def get_list_from_csv( + filename: str, headers: Tuple[str, str], default: Dict[str, str] = {} +): + """Retrieves list from CSV""" + path = SETTINGS_DIR / filename + assert filename.endswith(".csv") + + if not path.is_file(): + with open(path, "w", encoding="utf-8") as file: + writer = csv.writer(file) + writer.writerow(headers) + for key, value in default.items(): + writer.writerow([key] if key == value else [value, key]) + + # Now read via resource to take advantage of talon's + # ability to reload this script for us when the resource changes + with resource.open(str(path), "r") as f: + rows = list(csv.reader(f)) + + # print(str(rows)) + mapping = {} + if len(rows) >= 2: + actual_headers = rows[0] + if not actual_headers == list(headers): + print( + f'"{filename}": Malformed headers - {actual_headers}.' + + f" Should be {list(headers)}. Ignoring row." + ) + for row in rows[1:]: + if len(row) == 0: + # Windows newlines are sometimes read as empty rows. :champagne: + continue + if len(row) == 1: + output = spoken_form = row[0] + else: + output, spoken_form = row[:2] + if len(row) > 2: + print( + f'"{filename}": More than two values in row: {row}.' + + " Ignoring the extras." + ) + # Leading/trailing whitespace in spoken form can prevent recognition. + spoken_form = spoken_form.strip() + mapping[spoken_form] = output + + return mapping diff --git a/talon-user/code/vocabulary.py b/talon-user/code/vocabulary.py @@ -0,0 +1,90 @@ +from talon import Context, Module +from .user_settings import get_list_from_csv + +mod = Module() +ctx = Context() + +mod.list("vocabulary", desc="additional vocabulary words") + + +# Default words that will need to be capitalized (particularly under w2l). +# NB. These defaults and those later in this file are ONLY used when +# auto-creating the corresponding settings/*.csv files. Those csv files +# determine the contents of user.vocabulary and dictate.word_map. Once they +# exist, the contents of the lists/dictionaries below are irrelevant. +_capitalize_defaults = [ + "I", + "I'm", + "I've", + "I'll", + "I'd", + "Monday", + "Mondays", + "Tuesday", + "Tuesdays", + "Wednesday", + "Wednesdays", + "Thursday", + "Thursdays", + "Friday", + "Fridays", + "Saturday", + "Saturdays", + "Sunday", + "Sundays", + "January", + "February", + # March omitted because it's a regular word too + "April", + # May omitted because it's a regular word too + "June", + "July", + "August", + "September", + "October", + "November", + "December", +] + +# Default words that need to be remapped. +_word_map_defaults = { + # E.g: + # "cash": "cache", +} +_word_map_defaults.update({word.lower(): word for word in _capitalize_defaults}) + + +# "dictate.word_map" is used by `actions.dictate.replace_words` to rewrite words +# Talon recognized. Entries in word_map don't change the priority with which +# Talon recognizes some words over others. + +ctx.settings["dictate.word_map"] = get_list_from_csv( + "words_to_replace.csv", + headers=("Replacement", "Original"), + default=_word_map_defaults, +) + + +# Default words that should be added to Talon's vocabulary. +_simple_vocab_default = ["nmap", "admin", "Cisco", "Citrix", "VPN", "DNS", "Minecraft"] + +# Defaults for different pronounciations of words that need to be added to +# Talon's vocabulary. +_default_vocabulary = { + "N map": "nmap", + "under documented": "under-documented", +} +_default_vocabulary.update({word: word for word in _simple_vocab_default}) + +# "user.vocabulary" is used to explicitly add words/phrases that Talon doesn't +# recognize. Words in user.vocabulary (or other lists and captures) are +# "command-like" and their recognition is prioritized over ordinary words. +ctx.lists["user.vocabulary"] = get_list_from_csv( + "additional_words.csv", + headers=("Word(s)", "Spoken Form (If Different)"), + default=_default_vocabulary, +) + +# for quick verification of the reload +# print(str(ctx.settings["dictate.word_map"])) +# print(str(ctx.lists["user.vocabulary"])) diff --git a/talon-user/code/window_snap.py b/talon-user/code/window_snap.py @@ -0,0 +1,221 @@ +"""Tools for voice-driven window management. + +Originally from dweil/talon_community - modified for newapi by jcaw. + +""" + +# TODO: Map keyboard shortcuts to this manager once Talon has key hooks on all +# platforms + +import time +from operator import xor +from typing import Optional + +from talon import ui, Module, Context, actions + + +def sorted_screens(): + """Return screens sorted by their topmost, then leftmost, edge. + + Screens will be sorted left-to-right, then top-to-bottom as a tiebreak. + + """ + + return sorted( + sorted(ui.screens(), key=lambda screen: screen.visible_rect.top), + key=lambda screen: screen.visible_rect.left, + ) + + +def _set_window_pos(window, x, y, width, height): + """Helper to set the window position.""" + # TODO: Special case for full screen move - use os-native maximize, rather + # than setting the position? + + # 2020/10/01: While the upstream Talon implementation for MS Windows is + # settling, this may be buggy on full screen windows. Aegis doesn't want a + # hacky solution merged, so for now just repeat the command. + # + # TODO: Audit once upstream Talon is bug-free on MS Windows + window.rect = ui.Rect(round(x), round(y), round(width), round(height)) + + +def _bring_forward(window): + current_window = ui.active_window() + try: + window.focus() + current_window.focus() + except Exception as e: + # We don't want to block if this fails. + print(f"Couldn't bring window to front: {e}") + + +def _get_app_window(app_name: str) -> ui.Window: + return actions.self.get_running_app(app_name).active_window + + +def _move_to_screen( + window, offset: Optional[int] = None, screen_number: Optional[int] = None +): + """Move a window to a different screen. + + Provide one of `offset` or `screen_number` to specify a target screen. + + Provide `window` to move a specific window, otherwise the current window is + moved. + + """ + assert ( + screen_number or offset and not (screen_number and offset) + ), "Provide exactly one of `screen_number` or `offset`." + + src_screen = window.screen + screens = sorted_screens() + if offset: + screen_number = (screens.index(src_screen) + offset) % len(screens) + else: + # Human to array index + screen_number -= 1 + + dest_screen = screens[screen_number] + if src_screen == dest_screen: + return + + # Retain the same proportional position on the new screen. + dest = dest_screen.visible_rect + src = src_screen.visible_rect + # TODO: Test this on different-sized screens + # + # TODO: Is this the best behaviour for moving to a vertical screen? Probably + # not. + proportional_width = dest.width / src.width + proportional_height = dest.height / src.height + _set_window_pos( + window, + x=dest.left + (window.rect.left - src.left) * proportional_width, + y=dest.top + (window.rect.top - src.top) * proportional_height, + width=window.rect.width * proportional_width, + height=window.rect.height * proportional_height, + ) + + +def _snap_window_helper(window, pos): + screen = window.screen.visible_rect + + _set_window_pos( + window, + x=screen.x + (screen.width * pos.left), + y=screen.y + (screen.height * pos.top), + width=screen.width * (pos.right - pos.left), + height=screen.height * (pos.bottom - pos.top), + ) + + +class RelativeScreenPos(object): + """Represents a window position as a fraction of the screen.""" + + def __init__(self, left, top, right, bottom): + self.left = left + self.top = top + self.bottom = bottom + self.right = right + + +mod = Module() +mod.list( + "window_snap_positions", + "Predefined window positions for the current window. See `RelativeScreenPos`.", +) + + +_snap_positions = { + # Halves + # .---.---. .-------. + # | | | & |-------| + # '---'---' '-------' + "left": RelativeScreenPos(0, 0, 0.5, 1), + "right": RelativeScreenPos(0.5, 0, 1, 1), + "top": RelativeScreenPos(0, 0, 1, 0.5), + "bottom": RelativeScreenPos(0, 0.5, 1, 1), + # Thirds + # .--.--.--. + # | | | | + # '--'--'--' + "center third": RelativeScreenPos(1 / 3, 0, 2 / 3, 1), + "left third": RelativeScreenPos(0, 0, 1 / 3, 1), + "right third": RelativeScreenPos(2 / 3, 0, 1, 1), + "left two thirds": RelativeScreenPos(0, 0, 2 / 3, 1), + "right two thirds": RelativeScreenPos(1 / 3, 0, 1, 1,), + # Quarters + # .---.---. + # |---|---| + # '---'---' + "top left": RelativeScreenPos(0, 0, 0.5, 0.5), + "top right": RelativeScreenPos(0.5, 0, 1, 0.5), + "bottom left": RelativeScreenPos(0, 0.5, 0.5, 1), + "bottom right": RelativeScreenPos(0.5, 0.5, 1, 1), + # Sixths + # .--.--.--. + # |--|--|--| + # '--'--'--' + "top right third": RelativeScreenPos(2 / 3, 0, 1, 0.5), + "top left two thirds": RelativeScreenPos(0, 0, 2 / 3, 0.5), + "top right two thirds": RelativeScreenPos(1 / 3, 0, 1, 0.5), + "top center third": RelativeScreenPos(1 / 3, 0, 2 / 3, 0.5), + "bottom left third": RelativeScreenPos(0, 0.5, 1 / 3, 1), + "bottom right third": RelativeScreenPos(2 / 3, 0.5, 1, 1), + "bottom left two thirds": RelativeScreenPos(0, 0.5, 2 / 3, 1), + "bottom right two thirds": RelativeScreenPos(1 / 3, 0.5, 1, 1), + "bottom center third": RelativeScreenPos(1 / 3, 0.5, 2 / 3, 1), + # Special + "center": RelativeScreenPos(1 / 8, 1 / 6, 7 / 8, 5 / 6), + "full": RelativeScreenPos(0, 0, 1, 1), + "fullscreen": RelativeScreenPos(0, 0, 1, 1), +} + + +@mod.capture(rule="{user.window_snap_positions}") +def window_snap_position(m) -> RelativeScreenPos: + return _snap_positions[m.window_snap_positions] + + +ctx = Context() +ctx.lists["user.window_snap_positions"] = _snap_positions.keys() + + +@mod.action_class +class Actions: + def snap_window(pos: RelativeScreenPos) -> None: + """Move the active window to a specific position on-screen. + + See `RelativeScreenPos` for the structure of this position. + + """ + _snap_window_helper(ui.active_window(), pos) + + def move_window_next_screen() -> None: + """Move the active window to a specific screen.""" + _move_to_screen(ui.active_window(), offset=1) + + def move_window_previous_screen() -> None: + """Move the active window to the previous screen.""" + _move_to_screen(ui.active_window(), offset=-1) + + def move_window_to_screen(screen_number: int) -> None: + """Move the active window leftward by one.""" + _move_to_screen(ui.active_window(), screen_number=screen_number) + + def snap_app(app_name: str, pos: RelativeScreenPos): + """Snap a specific application to another screen.""" + window = _get_app_window(app_name) + _bring_forward(window) + _snap_window_helper(window, pos) + + def move_app_to_screen(app_name: str, screen_number: int): + """Move a specific application to another screen.""" + window = _get_app_window(app_name) + print(window) + _bring_forward(window) + _move_to_screen( + window, screen_number=screen_number, + ) diff --git a/talon-user/exec.py b/talon-user/exec.py @@ -1,17 +0,0 @@ -import os -import subprocess - -from talon import Module - -mod = Module() - - -@mod.action_class -class Actions: - def system_command(cmd: str): - """execute a command on the system""" - os.system('. ~/.dotfiles/shell/env; . ~/.dotfiles/shell/paths;' + cmd) - - def system_command_nb(cmd: str): - """execute a command on the system without blocking""" - subprocess.Popen(cmd, shell=True) diff --git a/talon-user/eye_tracking_settings.py b/talon-user/eye_tracking_settings.py @@ -0,0 +1,11 @@ +# from talon import app +# from talon.track.geom import Point2d +# from talon_plugins import speech, eye_mouse, eye_zoom_mouse + +# if app.platform == "mac": +# eye_zoom_mouse.config.screen_area = Point2d(100, 75) +# eye_zoom_mouse.config.img_scale = 6 +# elif app.platform == "windows": +# eye_zoom_mouse.config.screen_area = Point2d(200, 150) +# eye_zoom_mouse.config.img_scale = 4.5 + diff --git a/talon-user/mac-edit.talon b/talon-user/mac-edit.talon @@ -0,0 +1,208 @@ +os: mac +- +action(edit.copy): + key(cmd-c) + +action(edit.cut): + key(cmd-x) + +action(edit.delete): + key(backspace) + +action(edit.delete_line): + edit.select_line() + edit.delete() + +#action(edit.delete_paragraph): + +#action(edit.delete_sentence): + +action(edit.delete_word): + edit.select_word() + edit.delete() + +action(edit.down): + key(down) + +#action(edit.extend_again): + +#action(edit.extend_column): + +action(edit.extend_down): + key(shift-down) + +action(edit.extend_file_end): + key(cmd-shift-down) + +action(edit.extend_file_start): + key(cmd-shift-up) + +action(edit.extend_left): + key(shift-left) + +#action(edit.extend_line): + +action(edit.extend_line_down): + key(shift-down) + +action(edit.extend_line_end): + key(cmd-shift-right) + +action(edit.extend_line_start): + key(cmd-shift-left) + +action(edit.extend_line_up): + key(shift-up) + +action(edit.extend_page_down): + key(cmd-shift-pagedown) + +action(edit.extend_page_up): + key(cmd-shift-pageup) + +#action(edit.extend_paragraph_end): +#action(edit.extend_paragraph_next()): +#action(edit.extend_paragraph_previous()): +#action(edit.extend_paragraph_start()): + +action(edit.extend_right): + key(shift-right) + +#action(edit.extend_sentence_end): +#action(edit.extend_sentence_next): +#action(edit.extend_sentence_previous): +#action(edit.extend_sentence_start): + +action(edit.extend_up): + key(shift-up) + +action(edit.extend_word_left): + key(shift-alt-left) + +action(edit.extend_word_right): + key(shift-alt-right) + +action(edit.file_end): + key(cmd-down cmd-left) + +action(edit.file_start): + key(cmd-up cmd-left) + +action(edit.find): + key(cmd-f) + #actions.insert(text) + +action(edit.find_next): + key(cmd-g) + +action(edit.find_previous): + key(cmd-shift-g) + +action(edit.indent_less): + key(cmd-left delete) + +action(edit.indent_more): + key(cmd-left tab) + +#action(edit.jump_column(n: int) +#action(edit.jump_line(n: int) + +action(edit.left): + key(left) + +action(edit.line_down): + key(down home) + +action(edit.line_end): + key(cmd-right) + +action(edit.line_insert_down): + key(end enter) + +action(edit.line_insert_up): + key(cmd-left enter up) + +action(edit.line_start): + key(cmd-left) + +action(edit.line_up): + key(up cmd-left) + +#action(edit.move_again): + +action(edit.page_down): + key(pagedown) + +action(edit.page_up): + key(pageup) + +#action(edit.paragraph_end): +#action(edit.paragraph_next): +#action(edit.paragraph_previous): +#action(edit.paragraph_start): + +action(edit.paste): + key(cmd-v) + +action(edit.paste_match_style): + key(cmd-alt-shift-v) + +action(edit.print): + key(cmd-p) + +action(edit.redo): + key(cmd-shift-z) + +action(edit.right): + key(right) + +action(edit.save): + key(cmd-s) + +action(edit.save_all): + key(cmd-shift-s) + +action(edit.select_all): + key(cmd-a) + +action(edit.select_line): + key(cmd-right cmd-shift-left) + +#action(edit.select_lines(a: int, b: int)): + +action(edit.select_none): + key(right) + +#action(edit.select_paragraph): +#action(edit.select_sentence): + +action(edit.select_word): + edit.word_left() + edit.extend_word_right() + +#action(edit.selected_text): -> str +#action(edit.sentence_end): +#action(edit.sentence_next): +#action(edit.sentence_previous): +#action(edit.sentence_start): + +action(edit.undo): + key(cmd-z) + +action(edit.up): + key(up) + +action(edit.word_left): + key(alt-left) + +action(edit.word_right): + key(alt-right) + +action(edit.zoom_in): + key(cmd-=) + +action(edit.zoom_out): + key(cmd--) + +action(edit.zoom_reset): + key(cmd-0) diff --git a/talon-user/mac.talon b/talon-user/mac.talon @@ -0,0 +1,40 @@ +os: mac +- +action(app.preferences): + key(cmd-,) + +action(app.tab_close): + key(cmd-w) + +#action(app.tab_detach): +# Move the current tab to a new window + +action(app.tab_next): + key(cmd-alt-right) + +action(app.tab_open): + key(cmd-t) + +action(app.tab_previous): + key(cmd-alt-left) + +action(app.tab_reopen): + key(cmd-shift-t) + +action(app.window_close): + key(cmd-w) + +action(app.window_hide): + key(cmd-m) + +action(app.window_hide_others): + key(cmd-alt-h) + +action(app.window_next): + key(cmd-`) + +action(app.window_open): + key(cmd-n) + +action(app.window_previous): + key(cmd-shift-`) diff --git a/talon-user/misc/abbreviate.talon b/talon-user/misc/abbreviate.talon @@ -0,0 +1,2 @@ +- +(abbreviate|abreviate|brief) {user.abbreviation}: "{abbreviation}" diff --git a/talon-user/misc/extensions.talon b/talon-user/misc/extensions.talon @@ -0,0 +1,14 @@ +#extensions +dot pie: ".py" +dot talon: ".talon" +dot mark down: ".md" +dot shell: ".sh" +dot vim: ".vim" +dot see: ".c" +dot see sharp: ".cs" +dot com: ".com" +dot net: ".net" +dot org: ".org" +dot exe: ".exe" +dot (bin | bend): ".bin" +dot (jason | jay son): ".json"+ \ No newline at end of file diff --git a/talon-user/misc/formatters.talon b/talon-user/misc/formatters.talon @@ -0,0 +1,16 @@ +#provide both anchored and unachored commands via 'over' +phrase <user.text>$: user.insert_formatted(text, "NOOP") +phrase <user.text> over: user.insert_formatted(text, "NOOP") +{user.prose_formatter} <user.prose>$: user.insert_formatted(prose, prose_formatter) +{user.prose_formatter} <user.prose> over: user.insert_formatted(prose, prose_formatter) +<user.format_text>+$: user.insert_many(format_text_list) +<user.format_text>+ over: user.insert_many(format_text_list) +<user.formatters> that: user.formatters_reformat_selection(user.formatters) +word <user.word>: user.insert_formatted(user.word, "NOOP") +format help | help format: user.formatters_help_toggle() +recent list: user.toggle_phrase_history() +recent repeat <number_small>: insert(user.get_recent_phrase(number_small)) +recent copy <number_small>: clip.set_text(user.get_recent_phrase(number_small)) +select that: user.select_last_phrase() +nope that | scratch that: user.clear_last_phrase() +nope that was <user.formatters>: user.formatters_reformat_last(formatters) diff --git a/talon-user/misc/git.talon b/talon-user/misc/git.talon @@ -0,0 +1,103 @@ +tag: terminal +and tag: user.git +- +# Standard commands +git add patch: "git add . -p\n" +git add: "git add " +git add everything: "git add -u\n" +git bisect: "git bisect " +git blame: "git blame " +git branch: "git branch " +git remote branches: "git branch --remote\n" +git branch <user.text>: "git branch {text}" +git checkout: "git checkout " +git checkout master: "git checkout master\n" +git checkout main: "git checkout main\n" +git checkout <user.text>: "git checkout {text}" +git cherry pick: "git cherry-pick " +git cherry pick continue: "git cherry-pick --continue " +git cherry pick abort: "git cherry-pick --abort " +git cherry pick skip: "git cherry-pick --skip " +git clone: "git clone " +# Leave \n out for confirmation since the operation is destructive +git clean everything: "git clean -dfx" +git commit message <user.text>: "git commit -m '{text}'" +git commit: "git commit\n" +git diff (colour|color) words: "git diff --color-words " +git diff: "git diff " +git diff cached: "git diff --cached\n" +git fetch: "git fetch\n" +git fetch <user.text>: "git fetch {text}" +git fetch prune: "git fetch --prune\n" +git in it: "git init\n" +git log all: "git log\n" +git log all changes: "git log -c\n" +git log: "git log " +git log changes: "git log -c " +git merge: "git merge " +git merge <user.text>:"git merge {text}" +git move: "git mv " +git new branch: "git checkout -b " +git pull: "git pull\n" +git pull origin: "git pull origin " +git pull rebase: "git pull --rebase\n" +git pull fast forward: "git pull --ff-only\n" +git pull <user.text>: "git pull {text} " +git push: "git push\n" +git push origin: "git push origin " +git push up stream origin: "git push -u origin" +git push <user.text>: "git push {text} " +git push tags: "git push --tags\n" +git rebase: "git rebase\n" +git rebase continue: "git rebase --continue" +git rebase skip: "git rebase --skip" +git remove: "git rm " +git (remove|delete) branch: "git branch -d " +git (remove|delete) remote branch: "git push --delete origin " +git reset: "git reset " +git reset soft: "git reset --soft " +git reset hard: "git reset --hard " +git restore: "git restore " +git restore staged: "git restore --staged " +git remote show origin: "git remote show origin\n" +git remote add upstream: "git remote add upstream " +git show: "git show " +git stash pop: "git stash pop\n" +git stash: "git stash\n" +git stash apply: "git stash apply\n" +git stash list: "git stash list\n" +git stash show: "git stash show" +git status: "git status\n" +git submodule add: "git submodule add " +git tag: "git tag " + +# Convenience +git edit config: "git config --local -e\n" + +git clone clipboard: + insert("git clone ") + edit.paste() + key(enter) +git diff highlighted: + edit.copy() + insert("git diff ") + edit.paste() + key(enter) +git diff clipboard: + insert("git diff ") + edit.paste() + key(enter) +git add highlighted: + edit.copy() + insert("git add ") + edit.paste() + key(enter) +git add clipboard: + insert("git add ") + edit.paste() + key(enter) +git commit highlighted: + edit.copy() + insert("git add ") + edit.paste() + insert("\ngit commit\n") diff --git a/talon-user/misc/git_add_patch.talon b/talon-user/misc/git_add_patch.talon @@ -0,0 +1,19 @@ +tag: terminal +and tag: user.git +title: /git add .*\-p/ +- +yank: + key(y) + key(enter) +near: + key(n) + key(enter) +quench: + key(q) + key(enter) +drum: + key(d) + key(enter) +air: + key(a) + key(enter) diff --git a/talon-user/help.talon b/talon-user/misc/help.talon diff --git a/talon-user/misc/help_open.talon b/talon-user/misc/help_open.talon @@ -0,0 +1,7 @@ +mode: user.help +- +help next$: user.help_next() +help previous$: user.help_previous() +help <number>$: user.help_select_index(number - 1) +help return$: user.help_return() +help close$: user.help_hide()+ \ No newline at end of file diff --git a/talon-user/history.talon b/talon-user/misc/history.talon diff --git a/talon-user/misc/keys.talon b/talon-user/misc/keys.talon @@ -0,0 +1,8 @@ +go <user.arrow_keys>: key(arrow_keys) +<user.letter>: key(letter) +(ship | uppercase) <user.letters> [(lowercase | sunk)]: + user.insert_formatted(letters, "ALL_CAPS") +<user.symbol_key>: key(symbol_key) +<user.function_key>: key(function_key) +<user.special_key>: key(special_key) +<user.modifiers> <user.unmodified_key>: key("{modifiers}-{unmodified_key}")+ \ No newline at end of file diff --git a/talon-user/misc/macro.talon b/talon-user/misc/macro.talon @@ -0,0 +1,3 @@ +macro record: user.macro_record() +macro stop: user.macro_stop() +macro play: user.macro_play()+ \ No newline at end of file diff --git a/talon-user/misc/media.talon b/talon-user/misc/media.talon @@ -0,0 +1,7 @@ +volume up: key(volup) +volume down: key(voldown) +set volume <number>: user.media_set_volume(number) +(volume|media) mute: key(mute) +[media] play next: key(next) +[media] play previous: key(prev) +media (play | pause): key(play) diff --git a/talon-user/misc/messaging.talon b/talon-user/misc/messaging.talon @@ -0,0 +1,17 @@ +tag: user.messaging +- +# Navigation +previous (workspace | server): user.messaging_workspace_previous() +next (workspace | server): user.messaging_workspace_next() +channel: user.messaging_open_channel_picker() +channel <user.text>: + user.messaging_open_channel_picker() + insert(user.formatted_text(user.text, "ALL_LOWERCASE")) +channel up: user.messaging_channel_previous() +channel down: user.messaging_channel_next() +([channel] unread last | gopreev): user.messaging_unread_previous() +([channel] unread next | goneck): user.messaging_unread_next() +go (find | search): user.messaging_open_search() +mark (all | workspace | server) read: user.messaging_mark_workspace_read() +mark channel read: user.messaging_mark_channel_read() +upload file: user.messaging_upload_file() diff --git a/talon-user/misc/microphone_selection.talon b/talon-user/misc/microphone_selection.talon @@ -0,0 +1,2 @@ +^microphone show$: user.microphone_selection_toggle() +^microphone pick <number_small>$: user.microphone_select(number_small)+ \ No newline at end of file diff --git a/talon-user/misc/mouse.talon b/talon-user/misc/mouse.talon @@ -0,0 +1,103 @@ +control mouse: user.mouse_toggle_control_mouse() +zoom mouse: user.mouse_toggle_zoom_mouse() +camera overlay: user.mouse_toggle_camera_overlay() +run calibration: user.mouse_calibrate() +touch: + mouse_click(0) + # close the mouse grid if open + user.grid_close() + +righty: + mouse_click(1) + # close the mouse grid if open + user.grid_close() + +midclick: + mouse_click(2) + # close the mouse grid + user.grid_close() + +#see keys.py for modifiers. +#defaults +#command +#control +#option = alt +#shift +#super = windows key +<user.modifiers> touch: + key("{modifiers}:down") + mouse_click(0) + key("{modifiers}:up") + # close the mouse grid + user.grid_close() +<user.modifiers> righty: + key("{modifiers}:down") + mouse_click(1) + key("{modifiers}:up") + # close the mouse grid + user.grid_close() +(dubclick | duke): + mouse_click() + mouse_click() + # close the mouse grid + user.grid_close() +(tripclick | triplick): + mouse_click() + mouse_click() + mouse_click() + # close the mouse grid + user.grid_close() +drag: + user.mouse_drag() + # close the mouse grid + user.grid_close() +wheel down: user.mouse_scroll_down() +wheel down here: + user.mouse_move_center_active_window() + user.mouse_scroll_down() +wheel tiny [down]: mouse_scroll(20) +wheel tiny [down] here: + user.mouse_move_center_active_window() + mouse_scroll(20) +wheel downer: user.mouse_scroll_down_continuous() +wheel downer here: + user.mouse_move_center_active_window() + user.mouse_scroll_down_continuous() +wheel up: user.mouse_scroll_up() +wheel up here: + user.mouse_scroll_up() +wheel tiny up: mouse_scroll(-20) +wheel tiny up here: + user.mouse_move_center_active_window() + mouse_scroll(-20) +wheel upper: user.mouse_scroll_up_continuous() +wheel upper here: + user.mouse_move_center_active_window() + user.mouse_scroll_up_continuous() +wheel gaze: user.mouse_gaze_scroll() +wheel gaze here: + user.mouse_move_center_active_window() + user.mouse_gaze_scroll() +wheel stop: user.mouse_scroll_stop() +wheel stop here: + user.mouse_move_center_active_window() + user.mouse_scroll_stop() +wheel left: mouse_scroll(0, -40) +wheel left here: + user.mouse_move_center_active_window() + mouse_scroll(0, -40) +wheel tiny left: mouse_scroll(0, -20) +wheel tiny left here: + user.mouse_move_center_active_window() + mouse_scroll(0, -20) +wheel right: mouse_scroll(0, 40) +wheel right here: + user.mouse_move_center_active_window() + mouse_scroll(0, 40) +wheel tiny right: mouse_scroll(0, 20) +wheel tiny right here: + user.mouse_move_center_active_window() + mouse_scroll(0, 20) +curse yes: user.mouse_show_cursor() +curse no: user.mouse_hide_cursor() +copy mouse position: user.copy_mouse_position()+ \ No newline at end of file diff --git a/talon-user/misc/multiple_cursors.talon b/talon-user/misc/multiple_cursors.talon @@ -0,0 +1,10 @@ +tag: user.multiple_cursors +- +cursor multiple: user.multi_cursor_enable() +cursor stop: user.multi_cursor_disable() +cursor up: user.multi_cursor_add_above() +cursor down: user.multi_cursor_add_below() +cursor less: user.multi_cursor_select_fewer_occurrences() +cursor more: user.multi_cursor_select_more_occurrences() +cursor all: user.multi_cursor_select_all_occurrences() +cursor lines: user.multi_cursor_add_to_line_ends() diff --git a/talon-user/misc/repeater.talon b/talon-user/misc/repeater.talon @@ -0,0 +1,4 @@ +# -1 because we are repeating, so the initial command counts as one +<user.ordinals>: core.repeat_command(ordinals-1) +(repeat that|twice|again): core.repeat_command(1) +repeat that <number_small> [times]: core.repeat_command(number_small) diff --git a/talon-user/misc/screenshot.talon b/talon-user/misc/screenshot.talon @@ -0,0 +1,5 @@ +^grab window$: user.screenshot_window() +^grab screen$: user.screenshot() +^grab selection$: user.screenshot_selection() +^grab window clip$: user.screenshot_window_clipboard() +^grab screen clip$: user.screenshot_clipboard()+ \ No newline at end of file diff --git a/talon-user/misc/search_engines.talon b/talon-user/misc/search_engines.talon @@ -0,0 +1,4 @@ +{user.search_engine} hunt <user.text>: user.search_with_search_engine(search_engine, user.text) +{user.search_engine} (that|this): + text = edit.selected_text() + user.search_with_search_engine(search_engine, text)+ \ No newline at end of file diff --git a/talon-user/misc/splits.talon b/talon-user/misc/splits.talon @@ -0,0 +1,15 @@ +tag: user.splits +- +split right: user.split_window_right() +split left: user.split_window_left() +split down: user.split_window_down() +split up: user.split_window_up() +split (vertically | vertical): user.split_window_vertically() +split (horizontally | horizontal): user.split_window_horizontally() +split flip: user.split_flip() +split window: user.split_window() +split clear: user.split_clear() +split clear all: user.split_clear_all() +split next: user.split_next() +split last: user.split_last() +go split <number>: user.split_number(number) diff --git a/talon-user/misc/standard.talon b/talon-user/misc/standard.talon @@ -0,0 +1,30 @@ +#(jay son | jason ): "json" +#(http | htp): "http" +#tls: "tls" +#M D five: "md5" +#word (regex | rejex): "regex" +#word queue: "queue" +#word eye: "eye" +#word iter: "iter" +#word no: "NULL" +#word cmd: "cmd" +#word dup: "dup" +#word shell: "shell". +zoom in: edit.zoom_in() +zoom out: edit.zoom_out() +scroll up: edit.page_up() +scroll down: edit.page_down() +copy that: edit.copy() +cut that: edit.cut() +paste that: edit.paste() +undo that: edit.undo() +redo that: edit.redo() +paste match: edit.paste_match_style() +file save: edit.save() +wipe: key(backspace) +(pad | padding): + insert(" ") + key(left) +slap: + edit.line_end() + key(enter)+ \ No newline at end of file diff --git a/talon-user/misc/tabs.talon b/talon-user/misc/tabs.talon @@ -0,0 +1,9 @@ +tag: user.tabs +- +tab (open | new): app.tab_open() +tab last: app.tab_previous() +tab next: app.tab_next() +tab close: app.tab_close() +tab (reopen|restore): app.tab_reopen() +go tab <number>: user.tab_jump(number) +go tab final: user.tab_final() diff --git a/talon-user/misc/talon_helpers.talon b/talon-user/misc/talon_helpers.talon @@ -0,0 +1,14 @@ +talon copy context pie: user.talon_add_context_clipboard_python() +talon copy context: user.talon_add_context_clipboard() +talon copy title: + title = win.title() + clip.set_text(title) +talon dump context: + name = app.name() + executable = app.executable() + bundle = app.bundle() + title = win.title() + print("Name: {name}") + print("Executable: {executable}") + print("Bundle: {bundle}") + print("Title: {title}") diff --git a/talon-user/misc/toggles.talon b/talon-user/misc/toggles.talon @@ -0,0 +1,2 @@ + + diff --git a/talon-user/misc/window_management.talon b/talon-user/misc/window_management.talon @@ -0,0 +1,16 @@ +window (new|open): app.window_open() +window next: app.window_next() +window last: app.window_previous() +window close: app.window_close() +focus <user.running_applications>: user.switcher_focus(running_applications) +running list: user.switcher_toggle_running() +launch <user.launch_applications>: user.switcher_launch(launch_applications) + +snap <user.window_snap_position>: user.snap_window(window_snap_position) +snap next [screen]: user.move_window_next_screen() +snap last [screen]: user.move_window_previous_screen() +snap screen <number>: user.move_window_to_screen(number) +snap <user.running_applications> <user.window_snap_position>: + user.snap_app(running_applications, window_snap_position) +snap <user.running_applications> [screen] <number>: + user.move_app_to_screen(running_applications, number) diff --git a/talon-user/modes/dictation_mode.talon b/talon-user/modes/dictation_mode.talon @@ -0,0 +1,78 @@ +mode: dictation +- +^press <user.keys>$: key("{keys}") + +# Everything here should call auto_insert to preserve the state to correctly auto-capitalize/auto-space. +<user.prose>: auto_insert(prose) +new line: "\n" +new paragraph: "\n\n" +cap <user.word>: + result = user.formatted_text(word, "CAPITALIZE_FIRST_WORD") + auto_insert(result) + +# Navigation +go up <number_small> (line|lines): + edit.up() + repeat(number_small - 1) +go down <number_small> (line|lines): + edit.down() + repeat(number_small - 1) +go left <number_small> (word|words): + edit.word_left() + repeat(number_small - 1) +go right <number_small> (word|words): + edit.word_right() + repeat(number_small - 1) +go line start: edit.line_start() +go line end: edit.line_end() + +# Selection +select left <number_small> (word|words): + edit.extend_word_left() + repeat(number_small - 1) +select right <number_small> (word|words): + edit.extend_word_right() + repeat(number_small - 1) +select left <number_small> (character|characters): + edit.extend_left() + repeat(number_small - 1) +select right <number_small> (character|characters): + edit.extend_right() + repeat(number_small - 1) +clear left <number_small> (word|words): + edit.extend_word_left() + repeat(number_small - 1) + edit.delete() +clear right <number_small> (word|words): + edit.extend_word_right() + repeat(number_small - 1) + edit.delete() +clear left <number_small> (character|characters): + edit.extend_left() + repeat(number_small - 1) + edit.delete() +clear right <number_small> (character|characters): + edit.extend_right() + repeat(number_small - 1) + edit.delete() + +# Formatting +formatted <user.format_text>: + user.dictation_insert_raw(format_text) +^format selection <user.formatters>$: + user.formatters_reformat_selection(formatters) + +# Corrections +scratch that: user.clear_last_phrase() +scratch selection: edit.delete() +select that: user.select_last_phrase() +spell that <user.letters>: auto_insert(letters) +spell that <user.formatters> <user.letters>: + result = user.formatted_text(letters, formatters) + user.auto_format_pause() + auto_insert(result) + user.auto_format_resume() + +# Escape, type things that would otherwise be commands +^escape <user.text>$: + auto_insert(user.text) diff --git a/talon-user/modes/dragon_modes.talon b/talon-user/modes/dragon_modes.talon @@ -0,0 +1,8 @@ +#defines modes specific to Dragon. +speech.engine: dragon +mode: all +- +# wakes Dragon on Mac, deactivates talon speech commands +dragon mode: user.dragon_mode() +#sleep dragon on Mac, activates talon speech commands +talon mode: user.talon_mode()+ \ No newline at end of file diff --git a/talon-user/modes/language_modes.talon b/talon-user/modes/language_modes.talon @@ -0,0 +1,16 @@ +^force see sharp$: user.code_set_language_mode("csharp") +^force see plus plus$: user.code_set_language_mode("cplusplus") +^force go (lang|language)$: user.code_set_language_mode("go") +^force java$: user.code_set_language_mode("java") +^force java script$: user.code_set_language_mode("javascript") +^force type script$: user.code_set_language_mode("typescript") +^force markdown$: user.code_set_language_mode("markdown") +^force python$: user.code_set_language_mode("python") +^force are language$: user.code_set_language_mode("r") +^force talon [language]$: user.code_set_language_mode("talon") +^clear language modes$: user.code_clear_language_mode() +[enable] debug mode: + mode.enable("user.gdb") +disable debug mode: + mode.disable("user.gdb") + + \ No newline at end of file diff --git a/talon-user/modes/modes.py b/talon-user/modes/modes.py @@ -0,0 +1,47 @@ +from talon import Context, Module, app, actions, speech_system + +mod = Module() + +modes = { + "admin": "enable extra administration commands terminal (docker, etc)", + "debug": "a way to force debugger commands to be loaded", + "gdb": "a way to force gdb commands to be loaded", + "ida": "a way to force ida commands to be loaded", + "presentation": "a more strict form of sleep where only a more strict wake up command works", + "windbg": "a way to force windbg commands to be loaded", +} + +for key, value in modes.items(): + mod.mode(key, value) + + +@mod.action_class +class Actions: + def talon_mode(): + """For windows and Mac with Dragon, enables Talon commands and Dragon's command mode.""" + actions.speech.enable() + + engine = speech_system.engine.name + # app.notify(engine) + if "dragon" in engine: + if app.platform == "mac": + actions.user.engine_sleep() + elif app.platform == "windows": + actions.user.engine_wake() + # note: this may not do anything for all versions of Dragon. Requires Pro. + actions.user.engine_mimic("switch to command mode") + + def dragon_mode(): + """For windows and Mac with Dragon, disables Talon commands and exits Dragon's command mode""" + engine = speech_system.engine.name + # app.notify(engine) + + if "dragon" in engine: + # app.notify("dragon mode") + actions.speech.disable() + if app.platform == "mac": + actions.user.engine_wake() + elif app.platform == "windows": + actions.user.engine_wake() + # note: this may not do anything for all versions of Dragon. Requires Pro. + actions.user.engine_mimic("start normal mode") diff --git a/talon-user/modes/modes.talon b/talon-user/modes/modes.talon @@ -0,0 +1,12 @@ +not mode: sleep +- +^dictation mode$: + mode.disable("sleep") + mode.disable("command") + mode.enable("dictation") + user.code_clear_language_mode() + mode.disable("user.gdb") +^command mode$: + mode.disable("sleep") + mode.disable("dictation") + mode.enable("command")+ \ No newline at end of file diff --git a/talon-user/modes/sleep_mode.talon b/talon-user/modes/sleep_mode.talon @@ -0,0 +1,7 @@ +mode: sleep +- +settings(): + #stop continuous scroll/gaze scroll with a pop + user.mouse_enable_pop_stops_scroll = 0 + #enable pop click with 'control mouse' mode + user.mouse_enable_pop_click = 0+ \ No newline at end of file diff --git a/talon-user/modes/sleep_mode_wav2letter.talon b/talon-user/modes/sleep_mode_wav2letter.talon @@ -0,0 +1,6 @@ +mode: sleep +speech.engine: wav2letter +- +#this exists solely to prevent talon from waking up super easily in sleep mode at the moment with wav2letter +#you probably shouldn't have any other commands here +<phrase>: skip()+ \ No newline at end of file diff --git a/talon-user/modes/wake_up.talon b/talon-user/modes/wake_up.talon @@ -0,0 +1,22 @@ +#defines the commands that sleep/wake Talon +mode: all +- +^welcome back$: + user.mouse_wake() + user.history_enable() + user.talon_mode() +^sleep all$: + user.switcher_hide_running() + user.history_disable() + user.homophones_hide() + user.help_hide() + user.mouse_sleep() + speech.disable() + user.engine_sleep() +^talon sleep$: + speech.disable() + user.system_path_command('say "sleeping"') +^talon wake$: + user.system_path_command('say "awake"') + speech.enable() + diff --git a/talon-user/mouse_grid/mouse_grid.py b/talon-user/mouse_grid/mouse_grid.py @@ -0,0 +1,295 @@ +# courtesy of https://github.com/timo/ +# see https://github.com/timo/talon_scripts +from talon import Module, Context, app, canvas, screen, settings, ui, ctrl, cron +from talon.skia import Shader, Color, Paint, Rect +from talon.types.point import Point2d +from talon_plugins import eye_mouse, eye_zoom_mouse +from typing import Union + +import math, time + +import typing + +mod = Module() +narrow_expansion = mod.setting( + "grid_narrow_expansion", + type=int, + default=0, + desc="""After narrowing, grow the new region by this many pixels in every direction, to make things immediately on edges easier to hit, and when the grid is at its smallest, it allows you to still nudge it around""", +) + +mod.tag("mouse_grid_showing", desc="Tag indicates whether the mouse grid is showing") +mod.tag("mouse_grid_enabled", desc="Tag enables the mouse grid commands.") +ctx = Context() + + +class MouseSnapNine: + def __init__(self): + self.screen = None + self.rect = None + self.history = [] + self.img = None + self.mcanvas = None + self.active = False + self.count = 0 + self.was_control_mouse_active = False + self.was_zoom_mouse_active = False + + def setup(self, *, rect: Rect = None, screen_num: int = None): + screens = ui.screens() + # each if block here might set the rect to None to indicate failure + if rect is not None: + try: + screen = ui.screen_containing(*rect.center) + except Exception: + rect = None + if rect is None and screen_num is not None: + screen = screens[screen_num % len(screens)] + rect = screen.rect + if rect is None: + screen = screens[0] + rect = screen.rect + self.rect = rect.copy() + self.screen = screen + self.count = 0 + self.img = None + if self.mcanvas is not None: + self.mcanvas.close() + self.mcanvas = canvas.Canvas.from_screen(screen) + if self.active: + self.mcanvas.register("draw", self.draw) + self.mcanvas.freeze() + + def show(self): + if self.active: + return + # noinspection PyUnresolvedReferences + if eye_zoom_mouse.zoom_mouse.enabled: + self.was_zoom_mouse_active = True + eye_zoom_mouse.toggle_zoom_mouse(False) + if eye_mouse.control_mouse.enabled: + self.was_control_mouse_active = True + eye_mouse.control_mouse.toggle() + self.mcanvas.register("draw", self.draw) + self.mcanvas.freeze() + self.active = True + return + + def close(self): + if not self.active: + return + self.mcanvas.unregister("draw", self.draw) + self.mcanvas.close() + self.mcanvas = None + self.img = None + + self.active = False + if self.was_control_mouse_active and not eye_mouse.control_mouse.enabled: + eye_mouse.control_mouse.toggle() + if self.was_zoom_mouse_active and not eye_zoom_mouse.zoom_mouse.enabled: + eye_zoom_mouse.toggle_zoom_mouse(True) + + self.was_zoom_mouse_active = False + self.was_control_mouse_active = False + + def draw(self, canvas): + paint = canvas.paint + + def draw_grid(offset_x, offset_y, width, height): + canvas.draw_line( + offset_x + width // 3, + offset_y, + offset_x + width // 3, + offset_y + height, + ) + canvas.draw_line( + offset_x + 2 * width // 3, + offset_y, + offset_x + 2 * width // 3, + offset_y + height, + ) + + canvas.draw_line( + offset_x, + offset_y + height // 3, + offset_x + width, + offset_y + height // 3, + ) + canvas.draw_line( + offset_x, + offset_y + 2 * height // 3, + offset_x + width, + offset_y + 2 * height // 3, + ) + + def draw_crosses(offset_x, offset_y, width, height): + for row in range(0, 2): + for col in range(0, 2): + cx = offset_x + width / 6 + (col + 0.5) * width / 3 + cy = offset_y + height / 6 + (row + 0.5) * height / 3 + + canvas.draw_line(cx - 10, cy, cx + 10, cy) + canvas.draw_line(cx, cy - 10, cx, cy + 10) + + grid_stroke = 1 + + def draw_text(offset_x, offset_y, width, height): + canvas.paint.text_align = canvas.paint.TextAlign.CENTER + for row in range(3): + for col in range(3): + text_string = "" + if settings["user.grids_put_one_bottom_left"]: + text_string = f"{(2 - row)*3+col+1}" + else: + text_string = f"{row*3+col+1}" + text_rect = canvas.paint.measure_text(text_string)[1] + background_rect = text_rect.copy() + background_rect.center = Point2d( + offset_x + width / 6 + col * width / 3, + offset_y + height / 6 + row * height / 3, + ) + background_rect = background_rect.inset(-4) + paint.color = "9999995f" + paint.style = Paint.Style.FILL + canvas.draw_rect(background_rect) + paint.color = "00ff00ff" + canvas.draw_text( + text_string, + offset_x + width / 6 + col * width / 3, + offset_y + height / 6 + row * height / 3 + text_rect.height / 2, + ) + + if self.count < 2: + paint.color = "00ff007f" + for which in range(1, 10): + gap = 35 - self.count * 10 + if not self.active: + gap = 45 + draw_crosses(*self.calc_narrow(which, self.rect)) + + paint.stroke_width = grid_stroke + if self.active: + paint.color = "ff0000ff" + else: + paint.color = "000000ff" + if self.count >= 2: + aspect = self.rect.width / self.rect.height + if aspect >= 1: + w = self.screen.width / 3 + h = w / aspect + else: + h = self.screen.height / 3 + w = h * aspect + x = self.screen.x + (self.screen.width - w) / 2 + y = self.screen.y + (self.screen.height - h) / 2 + self.draw_zoom(canvas, x, y, w, h) + draw_grid(x, y, w, h) + draw_text(x, y, w, h) + else: + draw_grid(self.rect.x, self.rect.y, self.rect.width, self.rect.height) + + paint.textsize += 12 - self.count * 3 + draw_text(self.rect.x, self.rect.y, self.rect.width, self.rect.height) + + def calc_narrow(self, which, rect): + rect = rect.copy() + bdr = narrow_expansion.get() + row = int(which - 1) // 3 + col = int(which - 1) % 3 + if settings["user.grids_put_one_bottom_left"]: + row = 2 - row + rect.x += int(col * rect.width // 3) - bdr + rect.y += int(row * rect.height // 3) - bdr + rect.width = (rect.width // 3) + bdr * 2 + rect.height = (rect.height // 3) + bdr * 2 + return rect + + def narrow(self, which, move=True): + if which < 1 or which > 9: + return + self.save_state() + rect = self.calc_narrow(which, self.rect) + # check count so we don't bother zooming in _too_ far + if self.count < 5: + self.rect = rect.copy() + self.count += 1 + if move: + ctrl.mouse_move(*rect.center) + if self.count >= 2: + self.update_screenshot() + else: + self.mcanvas.freeze() + + def update_screenshot(self): + def finish_capture(): + self.img = screen.capture_rect(self.rect) + self.mcanvas.freeze() + + self.mcanvas.hide() + cron.after("16ms", finish_capture) + + def draw_zoom(self, canvas, x, y, w, h): + if self.img: + src = Rect(0, 0, self.img.width, self.img.height) + dst = Rect(x, y, w, h) + canvas.draw_image_rect(self.img, src, dst) + + def narrow_to_pos(self, x, y): + col_size = int(self.width // 3) + row_size = int(self.height // 3) + col = math.floor((x - self.rect.x) / col_size) + row = math.floor((y - self.rect.x) / row_size) + self.narrow(1 + col + 3 * row, move=False) + + def save_state(self): + self.history.append((self.count, self.rect.copy())) + + def go_back(self): + # FIXME: need window and screen tracking + self.count, self.rect = self.history.pop() + self.mcanvas.freeze() + + +mg = MouseSnapNine() + + +@mod.action_class +class GridActions: + def grid_activate(): + """Show mouse grid""" + if not mg.mcanvas: + mg.setup() + mg.show() + ctx.tags = ["user.mouse_grid_showing"] + + def grid_place_window(): + """Places the grid on the currently active window""" + mg.setup(rect=ui.active_window().rect) + + def grid_reset(): + """Resets the grid to fill the whole screen again""" + if mg.active: + mg.setup() + + def grid_select_screen(screen: int): + """Brings up mouse grid""" + mg.setup(screen_num=screen - 1) + mg.show() + + def grid_narrow_list(digit_list: typing.List[str]): + """Choose fields multiple times in a row""" + for d in digit_list: + GridActions.grid_narrow(int(d)) + + def grid_narrow(digit: Union[int, str]): + """Choose a field of the grid and narrow the selection down""" + mg.narrow(int(digit)) + + def grid_go_back(): + """Sets the grid state back to what it was before the last command""" + mg.go_back() + + def grid_close(): + """Close the active grid""" + ctx.tags = [] + mg.close() diff --git a/talon-user/mouse_grid/mouse_grid.talon b/talon-user/mouse_grid/mouse_grid.talon @@ -0,0 +1,17 @@ +tag: user.mouse_grid_enabled +- +M grid: + user.grid_select_screen(1) + user.grid_activate() + +grid win: + user.grid_place_window() + user.grid_activate() + +grid <user.number_key>+: + user.grid_activate() + user.grid_narrow_list(number_key_list) + +grid screen [<number>]: + user.grid_select_screen(number or 1) + user.grid_activate() diff --git a/talon-user/mouse_grid/mouse_grid_open.talon b/talon-user/mouse_grid/mouse_grid_open.talon @@ -0,0 +1,12 @@ +tag: user.mouse_grid_showing +- +<user.number_key>: + user.grid_narrow(number_key) +grid off: + user.grid_close() + +grid reset: + user.grid_reset() + +grid back: + user.grid_go_back() diff --git a/talon-user/music.talon b/talon-user/music.talon @@ -1 +1 @@ -now playing: user.system_command('notify "Current song" "$(mpc current)" talon') +now playing: user.system_path_command('notify "Current song" "$(mpc current)" talon') diff --git a/talon-user/power.talon b/talon-user/power.talon @@ -0,0 +1,7 @@ +caffeinate start: user.system_path_command('killall caffeinate; setsid -f caffeinate -d && notify "Caffeinated" "Sleep disabled" talon') +caffeinate stop: user.system_path_command('killall caffeinate && notify "Sleepy" "Will sleep again" talon') +caffeinate status: user.system_path_command('if pgrep caffeinate; then notify "Caffeinated" "Not sleeping" talon; else notify "Sleepy" "Will sleep if given the chance" talon; fi') +battery: + speech.disable() + user.system_path_command('say "$(battery -p)%"') + speech.enable() diff --git a/talon-user/settings.talon b/talon-user/settings.talon @@ -2,16 +2,38 @@ settings(): #adjust the scale of the imgui to my liking imgui.scale = 1.3 - + # enable if you'd like the picker gui to automatically appear when explorer has focus + user.file_manager_auto_show_pickers = 0 #set the max number of command lines per page in help user.help_max_command_lines_per_page = 50 - # set the max number of contexts display per page in help user.help_max_contexts_per_page = 20 - + # The default amount used when scrolling continuously + user.mouse_continuous_scroll_amount = 80 + #stop continuous scroll/gaze scroll with a pop + user.mouse_enable_pop_stops_scroll = 1 + #enable pop click with 'control mouse' mode + user.mouse_enable_pop_click = 1 + #When enabled, the 'Scroll Mouse' GUI will not be shown. + user.mouse_hide_mouse_gui = 0 + #hide cursor when mouse_wake is called to enable zoom mouse + user.mouse_wake_hides_cursor = 0 + #the amount to scroll up/down (equivalent to mouse wheel on Windows by default) + user.mouse_wheel_down_amount = 120 + #mouse grid and friends put the number one on the bottom left (vs on the top left) + user.grids_put_one_bottom_left = 1 # the number of lines of command history to display by default user.command_history_display = 10 - # the number of lines of command history to keep in total; # "command history more" to display all of them, "command history less" to restore user.command_history_size = 50 + + # Uncomment the below to enable context-sensitive dictation. This determines + # how to format (capitalize, space) dictation-mode speech by selecting & + # copying surrounding text before inserting. This can be slow and may not + # work in some applications. You may wish to enable this on a + # per-application basis. + #user.context_sensitive_dictation = 1 + +# uncomment tag to enable mouse grid +tag(): user.mouse_grid_enabled diff --git a/talon-user/shared_settings_module.py b/talon-user/shared_settings_module.py @@ -0,0 +1,9 @@ +from talon import Module + +mod = Module() +grids_put_one_bottom_left = mod.setting( + "grids_put_one_bottom_left", + type=bool, + default=False, + desc="""Allows you to switch mouse grid and friends between a computer numpad and a phone numpad (the number one goes on the bottom left or the top left)""", +) diff --git a/talon-user/talon_draft_window/LICENSE b/talon-user/talon_draft_window/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Stefan Schneider + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/talon-user/talon_draft_window/README.md b/talon-user/talon_draft_window/README.md @@ -0,0 +1,33 @@ +The draft window allows you to more easily edit prose style text via a task-specific UI. + +# Usage + +The main idea is that we have a Talon controlled text area where each word is labelled with a letter (called an anchor). You can use the anchors to indicate which word you want to operate on. + +An session might go like this for example: + + # Start with the text "this is a sentence with an elephant." in your editor or other textbox + draft edit all # Select all the text in your editor and moves it to the draft window + replace gust with error # Replaces the word corresponding with the red anchor 'g' (gust in knausj_talon) with the word 'error' + period # Add a full stop + select each through fine # Select the words starting at the 'e' anchor and ending at 'f' + say without # Insert the word 'without' (knausj_talon) + title word air # Make the word corresponding to the 'a' anchor capitalised + draft submit # Type the text in your draft window back into your editor + # End with the text "This is a sentence without error." in your editor or other textbox + +Here's a video of me going through the above commands: + +![Video of talon draft window in action](doc/talon-draft-demo.gif) + +# Customising + +If you want to change the display of the window you can do by adding some settings to one of your .talon files. See `settings.talon.example` for more details. + +# Running tests + +There are unit tests that you can run from the repository root like this (assuming your directory is called talon\_draft\_window): + + (cd ../ && python -m unittest talon_draft_window.test_draft_ui) + +The reason for the weirdness is because we have everything in the same directory and are doing relative imports. diff --git a/talon-user/talon_draft_window/doc/talon-draft-demo.gif b/talon-user/talon_draft_window/doc/talon-draft-demo.gif Binary files differ. diff --git a/talon-user/talon_draft_window/draft_global.talon b/talon-user/talon_draft_window/draft_global.talon @@ -0,0 +1,38 @@ +# These are available globally (in command mode) +mode: command +- +^draft show: + # Do this toggle so we can have focus when saying 'draft show' + user.draft_hide() + user.draft_show() + +^draft show <user.draft_window_position>: + # Do this toggle so we can have focus when saying 'draft show' + user.draft_hide() + user.draft_show() + user.draft_named_move(draft_window_position) + +^draft show small: + # Do this toggle so we can have focus when saying 'draft show' + user.draft_hide() + user.draft_show() + user.draft_resize(600, 200) + +^draft show large: + # Do this toggle so we can have focus when saying 'draft show' + user.draft_hide() + user.draft_show() + user.draft_resize(800, 500) + +^draft empty: user.draft_show("") + +^draft edit: + text = edit.selected_text() + key(backspace) + user.draft_show(text) + +^draft edit all: + edit.select_all() + text = edit.selected_text() + key(backspace) + user.draft_show(text) diff --git a/talon-user/talon_draft_window/draft_talon_helpers.py b/talon-user/talon_draft_window/draft_talon_helpers.py @@ -0,0 +1,320 @@ +from typing import Optional +from talon import ui, settings, Module, Context, actions +from .draft_ui import DraftManager + +mod = Module() + +# ctx is for toggling the draft_window_showing variable +# which lets you execute actions whenever the window is visible. +ctx = Context() + +# ctx_focused is active only when the draft window is focussed. This +# lets you execute actions under that condition. +ctx_focused = Context() +ctx_focused.matches = r""" +title: Talon Draft +""" + +mod.tag("draft_window_showing", desc="Tag set when draft window showing") +setting_theme = mod.setting( + "draft_window_theme", + type=str, + default="dark", + desc="Sets the main colors of the window, one of 'dark' or 'light'", +) +setting_label_size = mod.setting( + "draft_window_label_size", + type=int, + default=20, + desc="Sets the size of the word labels used in the draft window", +) +setting_label_color = mod.setting( + "draft_window_label_color", + type=str, + default=None, + desc=( + "Sets the color of the word labels used in the draft window. " + "E.g. 00ff00 would be green" + ), +) +setting_text_size = mod.setting( + "draft_window_text_size", + type=int, + default=20, + desc="Sets the size of the text used in the draft window", +) + + +draft_manager = DraftManager() + +# Update the styling of the draft window dynamically as user settings change +def _update_draft_style(*args): + draft_manager.set_styling( + **{ + arg: setting.get() + for setting, arg in ( + (setting_theme, "theme"), + (setting_label_size, "label_size"), + (setting_label_color, "label_color"), + (setting_text_size, "text_size"), + ) + } + ) + + +settings.register("", _update_draft_style) + + +@ctx_focused.action_class("user") +class ContextSensitiveDictationActions: + """ + Override these actions to assist 'Smart dictation mode'. + see https://github.com/knausj85/knausj_talon/pull/356 + """ + + def dictation_peek_left(clobber=False): + area = draft_manager.area + return area[max(0, area.sel.left - 50) : area.sel.left] + + def dictation_peek_right(): + area = draft_manager.area + return area[area.sel.right : area.sel.right + 50] + + def paste(text: str): + # todo: remove once user.paste works reliably with the draft window + actions.insert(text) + + +@ctx_focused.action_class("edit") +class EditActions: + """ + Make default edit actions more efficient. + """ + + def selected_text() -> str: + area = draft_manager.area + if area.sel: + result = area[area.sel.left : area.sel.right] + return result + return "" + + +from talon import cron + + +class UndoWorkaround: + """ + Workaround for the experimental textarea's undo being character by character. + This keeps a debounced undo history. Can be deleted once this todo item is + fixed: https://github.com/talonvoice/talon/issues/254#issuecomment-789149734 + """ + + # Set this to False if you want to turn it off, or just delete all references + # to this class + enable_workaround = True + + # Stack of (text_value, selection) tuples representing the undo stack + undo_stack = [] + # Stack of (text_value, selection) tuples representing the redo stack + redo_stack = [] + # Used by the timer to check when the text has stopped changing + pending_undo = None + + # timer handle + timer_handle = None + + @classmethod + def start_logger(cls, reset_undo_stack: bool): + if reset_undo_stack: + cls.undo_stack = [] + cls.redo_stack = [] + + cls.stop_logger() + cls.timer_handle = cron.interval("500ms", cls._log_changes) + + @classmethod + def stop_logger(cls): + if cls.timer_handle is not None: + cron.cancel(cls.timer_handle) + cls.timer_handle = None + cls.pending_undo = None + + @classmethod + def perform_undo(cls): + if len(cls.undo_stack) == 0: + return + + curr_text = draft_manager.area.value + curr_sel = (draft_manager.area.sel.left, draft_manager.area.sel.right) + text, sel = cls.undo_stack[-1] + if text == curr_text: + cls.undo_stack.pop() + if len(cls.undo_stack) == 0: + return + + # Most of the time (unless user has only just finished updating) the + # top of the stack will have the same contents as the text area. In + # this case pop again to get a bit lower. We should never have the + # same text twice, hence we don't need a loop. + text, sel = cls.undo_stack[-1] + + # Remember the current state in the redo stack + cls.redo_stack.append((curr_text, curr_sel)) + draft_manager.area.value = text + draft_manager.area.sel = sel + + cls.pending_undo = (text, sel) + + @classmethod + def perform_redo(cls): + if len(cls.redo_stack) == 0: + return + + text, sel = cls.redo_stack.pop() + + draft_manager.area.value = text + draft_manager.area.sel = sel + + cls.pending_undo = (text, sel) + cls.undo_stack.append((text, sel)) + + @classmethod + def _log_changes(cls): + """ + If the text and cursor position hasn't changed for two interval iterations + (1s) and the undo stack doesn't match the current state, then add to the stack. + """ + + curr_val = draft_manager.area.value + # Turn the Span into a tuple, because we can't == Spans + curr_sel = (draft_manager.area.sel.left, draft_manager.area.sel.right) + curr_state = (curr_val, curr_sel) + + state_stack_mismatch = ( + len(cls.undo_stack) == 0 + or + # Only want to update the undo stack if the value has changed, not just + # the selection + curr_state[0] != cls.undo_stack[-1][0] + ) + + if cls.pending_undo == curr_state and state_stack_mismatch: + cls.undo_stack.append(curr_state) + # Clear out the redo stack because we've changed the text + cls.redo_stack = [] + elif cls.pending_undo != curr_state: + cls.pending_undo = curr_state + elif not state_stack_mismatch and len(cls.undo_stack) > 0: + # Remember the cursor position in the undo stack for the current text value + cls.undo_stack[-1] = (cls.undo_stack[-1][0], curr_sel) + else: + # The text area text is not changing, do nothing + pass + + +if UndoWorkaround.enable_workaround: + ctx_focused.action("edit.undo")(UndoWorkaround.perform_undo) + ctx_focused.action("edit.redo")(UndoWorkaround.perform_redo) + + +@mod.action_class +class Actions: + def draft_show(text: Optional[str] = None): + """ + Shows draft window + """ + + draft_manager.show(text) + UndoWorkaround.start_logger(text is not None) + ctx.tags = ["user.draft_window_showing"] + + def draft_hide(): + """ + Hides draft window + """ + + draft_manager.hide() + UndoWorkaround.stop_logger() + ctx.tags = [] + + def draft_select( + start_anchor: str, end_anchor: str = "", include_trailing_whitespace: int = 0 + ): + """ + Selects text in the draft window + """ + + draft_manager.select_text( + start_anchor, + end_anchor=None if end_anchor == "" else end_anchor, + include_trailing_whitespace=include_trailing_whitespace == 1, + ) + + def draft_position_caret(anchor: str, after: int = 0): + """ + Positions the caret in the draft window + """ + + draft_manager.position_caret(anchor, after=after == 1) + + def draft_get_text() -> str: + """ + Returns the text in the draft window + """ + + return draft_manager.get_text() + + def draft_resize(width: int, height: int): + """ + Resize the draft window. + """ + + draft_manager.reposition(width=width, height=height) + + def draft_named_move(name: str, screen_number: Optional[int] = None): + """ + Lets you move the window to the top, bottom, left, right, or middle + of the screen. + """ + + screen = ui.screens()[screen_number or 0] + window_rect = draft_manager.get_rect() + xpos = (screen.width - window_rect.width) / 2 + ypos = (screen.height - window_rect.height) / 2 + + if name == "top": + ypos = 50 + elif name == "bottom": + ypos = screen.height - window_rect.height - 50 + elif name == "left": + xpos = 50 + elif name == "right": + xpos = screen.width - window_rect.width - 50 + elif name == "middle": + # That's the default values + pass + + # Adjust for the fact that the screen may not be at 0,0. + xpos += screen.x + ypos += screen.y + draft_manager.reposition(xpos=xpos, ypos=ypos) + + +# Some capture groups we need + + +@mod.capture(rule="{self.letter}+") +def draft_anchor(m) -> str: + """ + An anchor (string of letters) + """ + return "".join(m) + + +@mod.capture(rule="(top|bottom|left|right|middle)") +def draft_window_position(m) -> str: + """ + One of the named positions you can move the window to + """ + + return "".join(m) diff --git a/talon-user/talon_draft_window/draft_ui.py b/talon-user/talon_draft_window/draft_ui.py @@ -0,0 +1,213 @@ +from typing import Optional +import re + +from talon.experimental.textarea import ( + TextArea, + Span, + DarkThemeLabels, + LightThemeLabels +) + + +word_matcher = re.compile(r"([^\s]+)(\s*)") +def calculate_text_anchors(text, cursor_position, anchor_labels=None): + """ + Produces an iterator of (anchor, start_word_index, end_word_index, last_space_index) + tuples from the given text. Each tuple indicates a particular point you may want to + reference when editing along with some useful ranges you may want to operate on. + + - text is the text you want to process. + - cursor_position is the current position of the cursor, anchors will be placed around + this. + - anchor_labels is a list of characters you want to use for your labels. + - *index is just a character offset from the start of the string (e.g. the first character is at index 0) + - end_word_index is the index of the character after the last one included in the + anchor. That is, you can use it with a slice directly like [start:end] + - anchor is a short piece of text you can use to identify it (e.g. 'a', or '1'). + """ + anchor_labels = anchor_labels or "abcdefghijklmnopqrstuvwxyz" + + if len(text) == 0: + return [] + + # Find all the word spans + matches = [] + cursor_idx = None + for match in word_matcher.finditer(text): + matches.append(( + match.start(), + match.end() - len(match.group(2)), + match.end() + )) + if matches[-1][0] <= cursor_position and matches[-1][2] >= cursor_position: + cursor_idx = len(matches) - 1 + + # Now work out what range of those matches are getting an anchor. The aim is + # to centre the anchors around the cursor position, but also to use all the + # anchors. + anchors_before_cursor = len(anchor_labels) // 2 + anchor_start_idx = max(0, cursor_idx - anchors_before_cursor) + anchor_end_idx = min(len(matches), anchor_start_idx + len(anchor_labels)) + anchor_start_idx = max(0, anchor_end_idx - len(anchor_labels)) + + # Now add anchors to the selected matches + for i, anchor in zip(range(anchor_start_idx, anchor_end_idx), anchor_labels): + word_start, word_end, whitespace_end = matches[i] + yield ( + anchor, + word_start, + word_end, + whitespace_end + ) + + +class DraftManager: + """ + API to the draft window + """ + + def __init__(self): + self.area = TextArea() + self.area.title = "Talon Draft" + self.area.value = "" + self.area.register("label", self._update_labels) + self.set_styling() + + def set_styling( + self, + theme="dark", + text_size=20, + label_size=20, + label_color=None + ): + """ + Allow settings the style of the draft window. Will dynamically + update the style based on the passed in parameters. + """ + + area_theme = DarkThemeLabels if theme == "dark" else LightThemeLabels + theme_changes = { + "text_size": text_size, + "label_size": label_size, + } + if label_color is not None: + theme_changes["label"] = label_color + self.area.theme = area_theme(**theme_changes) + + def show(self, text: Optional[str] = None): + """ + Show the window. If text is None then keep the old contents, + otherwise set the text to the given value. + """ + + if text is not None: + self.area.value = text + self.area.show() + + def hide(self): + """ + Hide the window. + """ + + self.area.hide() + + def get_text(self) -> str: + """ + Gets the context of the text area + """ + + return self.area.value + + def get_rect(self) -> "talon.types.Rect": + """ + Get the Rect for the window + """ + + return self.area.rect + + def reposition( + self, + xpos: Optional[int] = None, + ypos: Optional[int] = None, + width: Optional[int] = None, + height: Optional[int] = None, + ): + """ + Move the window or resize it without having to change all properties. + """ + + rect = self.area.rect + if xpos is not None: + rect.x = xpos + + if ypos is not None: + rect.y = ypos + + if width is not None: + rect.width = width + + if height is not None: + rect.height = height + + self.area.rect = rect + + def select_text( + self, start_anchor, end_anchor=None, include_trailing_whitespace=False + ): + """ + Selects the word corresponding to start_anchor. If end_anchor supplied, selects + from start_anchor to the end of end_anchor. If include_trailing_whitespace=True + then also selects trailing space characters (useful for delete). + """ + + start_index, end_index, last_space_index = self.anchor_to_range(start_anchor) + if end_anchor is not None: + _, end_index, last_space_index = self.anchor_to_range(end_anchor) + + if include_trailing_whitespace: + end_index = last_space_index + + self.area.sel = Span(start_index, end_index) + + def position_caret(self, anchor, after=False): + """ + Positions the caret before the given anchor. If after=True position it directly after. + """ + + start_index, end_index, _ = self.anchor_to_range(anchor) + index = end_index if after else start_index + + self.area.sel = index + + def anchor_to_range(self, anchor): + anchors_data = calculate_text_anchors(self._get_visible_text(), self.area.sel.left) + for loop_anchor, start_index, end_index, last_space_index in anchors_data: + if anchor == loop_anchor: + return (start_index, end_index, last_space_index) + + raise RuntimeError(f"Couldn't find anchor {anchor}") + + def _update_labels(self, _visible_text): + """ + Updates the position of the labels displayed on top of each word + """ + + anchors_data = calculate_text_anchors(self._get_visible_text(), self.area.sel.left) + return [ + (Span(start_index, end_index), anchor) + for anchor, start_index, end_index, _ in anchors_data + ] + + def _get_visible_text(self): + # Placeholder for a future method of getting this + return self.area.value + + +if False: + # Some code for testing, change above False to True and edit as desired + draft_manager = DraftManager() + draft_manager.show( + "This is a line of text\nand another line of text and some more text so that the line gets so long that it wraps a bit.\nAnd a final sentence" + ) + draft_manager.reposition(xpos=100, ypos=100) + draft_manager.select_text("c") diff --git a/talon-user/talon_draft_window/draft_window.talon b/talon-user/talon_draft_window/draft_window.talon @@ -0,0 +1,51 @@ +# These are active when we have focus on the draft window +title:Talon Draft +- +settings(): + # Enable 'Smart dictation mode', see https://github.com/knausj85/knausj_talon/pull/356 + user.context_sensitive_dictation = 1 + +# Replace a single word with a phrase +replace <user.draft_anchor> with <user.text>: + user.draft_select("{draft_anchor}") + result = user.formatted_text(text, "NOOP") + insert(result) + +# Position cursor before word +cursor <user.draft_anchor>: + user.draft_position_caret("{draft_anchor}") + +cursor before <user.draft_anchor>: + user.draft_position_caret("{draft_anchor}") + +# Position cursor after word +cursor after <user.draft_anchor>: + user.draft_position_caret("{draft_anchor}", 1) + +# Select a whole word +select <user.draft_anchor>: + user.draft_select("{draft_anchor}") + +# Select a range of words +select <user.draft_anchor> through <user.draft_anchor>: + user.draft_select("{draft_anchor_1}", "{draft_anchor_2}") + +# Delete a word +clear <user.draft_anchor>: + user.draft_select("{draft_anchor}", "", 1) + key(backspace) + +# Delete a range of words +clear <user.draft_anchor> through <user.draft_anchor>: + user.draft_select(draft_anchor_1, draft_anchor_2, 1) + key(backspace) + +# reformat word +<user.formatters> word <user.draft_anchor>: + user.draft_select("{draft_anchor}", "", 1) + user.formatters_reformat_selection(user.formatters) + +# reformat range +<user.formatters> <user.draft_anchor> through <user.draft_anchor>: + user.draft_select(draft_anchor_1, draft_anchor_2, 1) + user.formatters_reformat_selection(user.formatters) diff --git a/talon-user/talon_draft_window/draft_window_open.talon b/talon-user/talon_draft_window/draft_window_open.talon @@ -0,0 +1,12 @@ +# These are available when the draft window is open, but not necessarily focussed +tag: user.draft_window_showing +- +draft hide: user.draft_hide() + +draft submit: + content = user.draft_get_text() + user.draft_hide() + insert(content) + # user.paste may be somewhat faster, but seems to be unreliable on MacOSX, see + # https://github.com/talonvoice/talon/issues/254#issuecomment-789355238 + # user.paste(content) diff --git a/talon-user/talon_draft_window/settings.talon.example b/talon-user/talon_draft_window/settings.talon.example @@ -0,0 +1,8 @@ +# Put some settings like this in one of your Talon files to override the styling +# of the draft window. +- +settings(): + user.draft_window_theme = "dark" # or light + user.draft_window_text_size = 20 + user.draft_window_label_size = 20 + user.draft_window_label_color = "ff0000" # Any hex code RGB value, e.g. this is red diff --git a/talon-user/talon_draft_window/test_draft_ui.py b/talon-user/talon_draft_window/test_draft_ui.py @@ -0,0 +1,86 @@ +try: + import talon.experimental.textarea + running_in_talon = True +except ModuleNotFoundError: + # Some shenanigans to stub out the Talon imports + import imp, sys + name = "talon.experimental.textarea" + module = imp.new_module(name) + sys.modules[name] = module + exec( + "\n".join([ + "TextArea = 1", + "Span = 1", + "DarkThemeLabels = 1", + "LightThemeLabels = 1" + ]), + module.__dict__ + ) + running_in_talon = False + +from unittest import TestCase +from functools import wraps + +from .draft_ui import calculate_text_anchors + + +class CalculateAnchorsTest(TestCase): + """ + Tests calculate_text_anchors + """ + + def test_finds_anchors(self): + examples = [ + ("one-word", [("a", 0, 8, 8)]), + ("two words", [("a", 0, 3, 4), ("b", 4, 9, 9)]), + ("two\nwords", [("a", 0, 3, 4), ("b", 4, 9, 9)]), + ] + anchor_labels = ["a", "b"] + for text, expected in examples: + # Given an example + + # When we calculate the result and turn it into a list + result = list(calculate_text_anchors(text, 0, anchor_labels=anchor_labels)) + + # Then it matches what we expect + self.assertEqual(result, expected, text) + + def test_positions_anchors_around_cursor(self): + # In these examples the cursor is at the asterisk which is stripped by the test + # code. Indicies after the asterisk have to take this into account. + examples = [ + ("one*-word", [("a", 0, 8, 8)]), + ("one-word*", [("a", 0, 8, 8)]), + ( + "the three words*", + [("a", 0, 3, 4), ("b", 4, 9, 10), ("c", 10, 15, 15)] + ), + ( + "*the three words", + [("a", 0, 3, 4), ("b", 4, 9, 10), ("c", 10, 15, 15)] + ), + ( + "too many* words for the number of anchors", + [("a", 0, 3, 4), ("b", 4, 8, 9), ("c", 9, 14, 15)] + ), + ( + "too many words fo*r the number of anchors", + [("a", 9, 14, 15), ("b", 15, 18, 19), ("c", 19, 22, 23)] + ), + ] + anchor_labels = ["a", "b", "c"] + + for text_with_cursor, expected in examples: + # Given an example + cursor_pos = text_with_cursor.index("*") + text = text_with_cursor.replace("*", "") + + # When we calculate the result and turn it into a list + result = list(calculate_text_anchors( + text, + cursor_pos, + anchor_labels=anchor_labels + )) + + # Then it matches what we expect + self.assertEqual(result, expected, text) diff --git a/talon-user/termapps.talon b/talon-user/termapps.talon @@ -0,0 +1 @@ +files: user.system_path_command('setsid -f alacritty -e lf') diff --git a/talon-user/text/find_and_replace.talon b/talon-user/text/find_and_replace.talon @@ -0,0 +1,88 @@ +tag: user.find_and_replace +- +hunt this: user.find("") +hunt this <user.text>: user.find(text) +hunt all: user.find_everywhere("") +hunt all <user.text>: user.find_everywhere(text) +hunt case : user.find_toggle_match_by_case() +hunt word : user.find_toggle_match_by_word() +hunt expression : user.find_toggle_match_by_regex() +hunt next: user.find_next() +hunt previous: user.find_previous() +replace this [<user.text>]: user.replace(text or "") +replace all: user.replace_everywhere("") +replace <user.text> all: user.replace_everywhere(text) +replace confirm that: user.replace_confirm() +replace confirm all: user.replace_confirm_all() + +#quick replace commands, modeled after jetbrains +clear last <user.text> [over]: + user.select_previous_occurrence(text) + sleep(100ms) + edit.delete() +clear next <user.text> [over]: + user.select_next_occurrence(text) + sleep(100ms) + edit.delete() +clear last clip: + user.select_previous_occurrence(clip.text()) + edit.delete() +clear next clip: + user.select_next_occurrence(clip.text()) + sleep(100ms) + edit.delete() +comment last <user.text> [over]: + user.select_previous_occurrence(text) + sleep(100ms) + code.toggle_comment() +comment last clip: + user.select_previous_occurrence(clip.text()) + sleep(100ms) + code.toggle_comment() +comment next <user.text> [over]: + user.select_next_occurrence(text) + sleep(100ms) + code.toggle_comment() +comment next clip: + user.select_next_occurrence(clip.text()) + sleep(100ms) + code.toggle_comment() +go last <user.text> [over]: + user.select_previous_occurrence(text) + sleep(100ms) + edit.right() +go last clip: + user.select_previous_occurrence(clip.text()) + sleep(100ms) + edit.right() +go next <user.text> [over]: + user.select_next_occurrence(text) + edit.right() +go next clip: + user.select_next_occurrence(clip.text()) + edit.right() +paste last <user.text> [over]: + user.select_previous_occurrence(text) + sleep(100ms) + edit.right() + edit.paste() +paste next <user.text> [over]: + user.select_next_occurrence(text) + sleep(100ms) + edit.right() + edit.paste() +replace last <user.text> [over]: + user.select_previous_occurrence(text) + sleep(100ms) + edit.paste() +replace next <user.text> [over]: + user.select_next_occurrence(text) + sleep(100ms) + edit.paste() +select last <user.text> [over]: user.select_previous_occurrence(text) +select next <user.text> [over]: user.select_next_occurrence(text) +select last clip: user.select_previous_occurrence(clip.text()) +select next clip: user.select_next_occurrence(clip.text()) + + + diff --git a/talon-user/text/homophones.talon b/talon-user/text/homophones.talon @@ -0,0 +1,5 @@ +phones <user.homophones_canonical>: user.homophones_show(homophones_canonical) +phones that: user.homophones_show_selection() +phones force <user.homophones_canonical>: user.homophones_force_show(homophones_canonical) +phones force: user.homophones_force_show_selection() +phones hide: user.homophones_hide() diff --git a/talon-user/text/homophones_open.talon b/talon-user/text/homophones_open.talon @@ -0,0 +1,10 @@ +mode: user.homophones +- +choose <number_small>: + result = user.homophones_select(number_small) + insert(result) + user.homophones_hide() +choose <user.formatters> <number_small>: + result = user.homophones_select(number_small) + insert(user.formatted_text(result, formatters)) + user.homophones_hide()+ \ No newline at end of file diff --git a/talon-user/text/line_commands.talon b/talon-user/text/line_commands.talon @@ -0,0 +1,68 @@ +tag: user.line_commands +- +#this defines some common line commands. More may be defined that are ide-specific. +lend: edit.line_end() +bend: edit.line_start() +go <number>: edit.jump_line(number) +go <number> end: + edit.jump_line(number) + edit.line_end() +comment [line] <number>: + user.select_range(number, number) + code.toggle_comment() +comment <number> until <number>: + user.select_range(number_1, number_2) + code.toggle_comment() +clear [line] <number>: + edit.jump_line(number) + user.select_range(number, number) + edit.delete() +clear <number> until <number>: + user.select_range(number_1, number_2) + edit.delete() +copy [line] <number>: + user.select_range(number, number) + edit.copy() +copy <number> until <number>: + user.select_range(number_1, number_2) + edit.copy() +cut [line] <number>: + user.select_range(number, number) + edit.cut() +cut [line] <number> until <number>: + user.select_range(number_1, number_2) + edit.cut() +(paste | replace) <number> until <number>: + user.select_range(number_1, number_2) + edit.paste() +(select | cell | sell) [line] <number>: user.select_range(number, number) +(select | cell | sell) <number> until <number>: user.select_range(number_1, number_2) +tab that: edit.indent_more() +tab [line] <number>: + edit.jump_line(number) + edit.indent_more() +tab <number> until <number>: + user.select_range(number_1, number_2) + edit.indent_more() +retab that: edit.indent_less() +retab [line] <number>: + user.select_range(number, number) + edit.indent_less() +retab <number> until <number>: + user.select_range(number_1, number_2) + edit.indent_less() +drag [line] down: edit.line_swap_down() +drag [line] up: edit.line_swap_up() +drag up [line] <number>: + user.select_range(number, number) + edit.line_swap_up() +drag up <number> until <number>: + user.select_range(number_1, number_2) + edit.line_swap_up() +drag down [line] <number>: + user.select_range(number, number) + edit.line_swap_down() +drag down <number> until <number>: + user.select_range(number_1, number_2) + edit.line_swap_down() +clone (line|that): edit.line_clone() diff --git a/talon-user/text/numbers.talon b/talon-user/text/numbers.talon @@ -0,0 +1,3 @@ +not tag: user.mouse_grid_showing +- +<user.number_string>: "{number_string}" diff --git a/talon-user/text/symbols.talon b/talon-user/text/symbols.talon @@ -0,0 +1,63 @@ +question [mark]: "?" +(downscore | underscore): "_" +double dash: "--" +(bracket | brack | left bracket): "[" +(rbrack | are bracket | right bracket): "]" +(brace | left brace): "{" +(rbrace | are brace | right brace): "}" +triple quote: "'''" +(dot dot | dotdot): ".." +#ellipses: "…" +ellipses: "..." +(comma and | spamma): ", " +plus: "+" +arrow: "->" +dub arrow: "=>" +new line: "\\n" +carriage return: "\\r" +line feed: "\\r\\n" +empty dubstring: + '""' + key(left) +empty escaped (dubstring|dub quotes): + '\\"\\"' + key(left) + key(left) +empty string: + "''" + key(left) +empty escaped string: + "\\'\\'" + key(left) + key(left) +(inside parens | args): + insert("()") + key(left) +inside (squares | list): + insert("[]") + key(left) +inside (bracket | braces): + insert("{}") + key(left) +inside percent: + insert("%%") + key(left) +inside quotes: + insert('""') + key(left) +angle that: + text = edit.selected_text() + user.paste("<{text}>") +(bracket | brace) that: + text = edit.selected_text() + user.paste("{{{text}}}") +(parens | args) that: + text = edit.selected_text() + user.paste("({text})") +percent that: + text = edit.selected_text() + user.paste("%{text}%") +quote that: + text = edit.selected_text() + user.paste('"{text}"') + diff --git a/talon-user/text/text_navigation.py b/talon-user/text/text_navigation.py @@ -0,0 +1,294 @@ +import re +from talon import ctrl, ui, Module, Context, actions, clip +import itertools +from typing import Union + +ctx = Context() +mod = Module() + + +text_navigation_max_line_search = mod.setting( + "text_navigation_max_line_search", + type=int, + default=10, + desc="the maximum number of rows that will be included in the search for the keywords above and below in <user direction>", +) + +mod.list( + "navigation_action", + desc="actions to perform, for instance move, select, cut, etc", +) +mod.list( + "before_or_after", + desc="words to indicate if the cursor should be moved before or after a given reference point", +) +mod.list( + "navigation_target_name", + desc="names for regular expressions for common things to navigate to, for instance a word with or without underscores", +) + +ctx.lists["self.navigation_action"] = { + "move": "GO", + "extend": "EXTEND", + "select": "SELECT", + "clear": "DELETE", + "cut": "CUT", + "copy": "COPY", +} +ctx.lists["self.before_or_after"] = { + "before": "BEFORE", + "after": "AFTER", + # DEFAULT is also a valid option as input for this capture, but is not directly accessible for the user. +} +navigation_target_names = { + "word": r"\w+", + "small": r"[A-Z]?[a-z0-9]+", + "big": r"[\S]+", + "parens": r'\((.*?)\)', + "squares": r'\[(.*?)\]', + "braces": r'\{(.*?)\}', + "quotes": r'\"(.*?)\"', + "angles": r'\<(.*?)\>', + #"single quotes": r'\'(.*?)\'', + "all": r'(.+)', + "method": r'\w+\((.*?)\)', + "constant": r'[A-Z_][A-Z_]+' +} +ctx.lists["self.navigation_target_name"] = navigation_target_names + +@mod.capture(rule="<user.any_alphanumeric_key> | {user.navigation_target_name} | phrase <user.text>") +def navigation_target(m) -> re.Pattern: + """A target to navigate to. Returns a regular expression.""" + if hasattr(m, 'any_alphanumeric_key'): + return re.compile(re.escape(m.any_alphanumeric_key), re.IGNORECASE) + if hasattr(m, 'navigation_target_name'): + return re.compile(m.navigation_target_name) + return re.compile(re.escape(m.text), re.IGNORECASE) + +@mod.action_class +class Actions: + def navigation( + navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY + direction: str, # up, down, left, right + navigation_target_name: str, + before_or_after: str, # BEFORE, AFTER, DEFAULT + regex: re.Pattern, + occurrence_number: int, + ): + """Navigate in `direction` to the occurrence_number-th time that `regex` occurs, then execute `navigation_action` at the given `before_or_after` position.""" + direction = direction.upper() + navigation_target_name = re.compile((navigation_target_names["word"] if (navigation_target_name == "DEFAULT") else navigation_target_name)) + function = navigate_left if direction in ("UP", "LEFT") else navigate_right + function(navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction) + + def navigation_by_name( + navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY + direction: str, # up, down, left, right + before_or_after: str, # BEFORE, AFTER, DEFAULT + navigation_target_name: str, # word, big, small + occurrence_number: int, + ): + """Like user.navigation, but to a named target.""" + r = re.compile(navigation_target_names[navigation_target_name]) + actions.user.navigation(navigation_action, direction, "DEFAULT", before_or_after, r, occurrence_number) + +def get_text_left(): + actions.edit.extend_line_start() + text = actions.edit.selected_text() + actions.edit.right() + return text + + +def get_text_right(): + actions.edit.extend_line_end() + text = actions.edit.selected_text() + actions.edit.left() + return text + + +def get_text_up(): + actions.edit.up() + actions.edit.line_end() + for j in range(0, text_navigation_max_line_search.get()): + actions.edit.extend_up() + actions.edit.extend_line_start() + text = actions.edit.selected_text() + actions.edit.right() + return text + + +def get_text_down(): + actions.edit.down() + actions.edit.line_start() + for j in range(0, text_navigation_max_line_search.get()): + actions.edit.extend_down() + actions.edit.extend_line_end() + text = actions.edit.selected_text() + actions.edit.left() + return text + + +def get_current_selection_size(): + return len(actions.edit.selected_text()) + + +def go_right(i): + for j in range(0, i): + actions.edit.right() + + +def go_left(i): + for j in range(0, i): + actions.edit.left() + + +def extend_left(i): + for j in range(0, i): + actions.edit.extend_left() + + +def extend_right(i): + for j in range(0, i): + actions.edit.extend_right() + + +def select(direction, start, end, length): + if direction == "RIGHT" or direction == "DOWN": + go_right(start) + extend_right(end - start) + else: + go_left(length - end) + extend_left(end - start) + + +def navigate_left( + navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction +): + current_selection_length = get_current_selection_size() + if current_selection_length > 0: + actions.edit.right() + text = get_text_left() if direction == "LEFT" else get_text_up() + # only search in the text that was not selected + subtext = ( + text if current_selection_length <= 0 else text[:-current_selection_length] + ) + match = match_backwards(regex, occurrence_number, subtext) + if match == None: + # put back the old selection, if the search failed + extend_left(current_selection_length) + return + start = match.start() + end = match.end() + handle_navigation_action( + navigation_action, navigation_target_name, before_or_after, direction, text, start, end + ) + + +def navigate_right( + navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction +): + current_selection_length = get_current_selection_size() + if current_selection_length > 0: + actions.edit.left() + text = get_text_right() if direction == "RIGHT" else get_text_down() + # only search in the text that was not selected + sub_text = text[current_selection_length:] + # pick the next interrater, Skip n number of occurrences, get an iterator given the Regex + match = match_forward(regex, occurrence_number, sub_text) + if match == None: + # put back the old selection, if the search failed + extend_right(current_selection_length) + return + start = current_selection_length + match.start() + end = current_selection_length + match.end() + handle_navigation_action( + navigation_action, navigation_target_name, before_or_after, direction, text, start, end + ) + + +def handle_navigation_action( + navigation_action, navigation_target_name, before_or_after, direction, text, start, end +): + length = len(text) + if navigation_action == "GO": + handle_move(direction, before_or_after, start, end, length) + elif navigation_action == "SELECT": + handle_select(navigation_target_name, before_or_after, direction, text, start, end, length) + elif navigation_action == "DELETE": + handle_select(navigation_target_name, before_or_after, direction, text, start, end, length) + actions.edit.delete() + elif navigation_action == "CUT": + handle_select(navigation_target_name, before_or_after, direction, text, start, end, length) + actions.edit.cut() + elif navigation_action == "COPY": + handle_select(navigation_target_name, before_or_after, direction, text, start, end, length) + actions.edit.copy() + elif navigation_action == "EXTEND": + handle_extend(before_or_after, direction, start, end, length) + + +def handle_select(navigation_target_name, before_or_after, direction, text, start, end, length): + if before_or_after == "BEFORE": + select_left = length - start + text_left = text[:-select_left] + match2 = match_backwards(navigation_target_name, 1, text_left) + if match2 == None: + end = start + start = 0 + else: + start = match2.start() + end = match2.end() + elif before_or_after == "AFTER": + text_right = text[end:] + match2 = match_forward(navigation_target_name, 1, text_right) + if match2 == None: + start = end + end = length + else: + start = end + match2.start() + end = end + match2.end() + select(direction, start, end, length) + + +def handle_move(direction, before_or_after, start, end, length): + if direction == "RIGHT" or direction == "DOWN": + if before_or_after == "BEFORE": + go_right(start) + else: + go_right(end) + else: + if before_or_after == "AFTER": + go_left(length - end) + else: + go_left(length - start) + + +def handle_extend(before_or_after, direction, start, end, length): + if direction == "RIGHT" or direction == "DOWN": + if before_or_after == "BEFORE": + extend_right(start) + else: + extend_right(end) + else: + if before_or_after == "AFTER": + extend_left(length - end) + else: + extend_left(length - start) + + +def match_backwards(regex, occurrence_number, subtext): + try: + match = list(regex.finditer(subtext))[-occurrence_number] + return match + except IndexError: + return + + +def match_forward(regex, occurrence_number, sub_text): + try: + match = next( + itertools.islice(regex.finditer(sub_text), occurrence_number - 1, None) + ) + return match + except StopIteration: + return None diff --git a/talon-user/text/text_navigation.talon b/talon-user/text/text_navigation.talon @@ -0,0 +1,76 @@ +## (2021-03-09) This syntax is experimental and may change. See below for an explanation. +navigate [{user.arrow_key}] [{user.navigation_action}] [{user.navigation_target_name}] [{user.before_or_after}] [<user.ordinals>] <user.navigation_target>: +## If you use this command a lot, you may wish to have a shorter syntax that omits the navigate keyword. Note that you then at least have to say either a navigation_action or before_or_after: +#({user.navigation_action} [{user.arrow_key}] [{user.navigation_target_name}] [{user.before_or_after}] | [{user.arrow_key}] {user.before_or_after}) [<user.ordinals>] <user.navigation_target>: + user.navigation(navigation_action or "GO", arrow_key or "RIGHT", navigation_target_name or "DEFAULT", before_or_after or "DEFAULT", navigation_target, ordinals or 1) + +# ===== Examples of use ===== +# +# navigate comma: moves after the next "," on the line. +# navigate before five: moves before the next "5" on the line. +# navigate left underscore: moves before the previous "_" on the line. +# navigate left after second plex: moves after the second-previous "x" on the line. +# +# Besides characters, we can find phrases or move in predetermined units: +# +# navigate phrase hello world: moves after the next "hello world" on the line. +# navigate left third word: moves left over three words. +# navigate before second big: moves before the second-next 'big' word (a chunk of anything except white space). +# navigate left second small: moves left over two 'small' words (chunks of a camelCase name). +# +# We can search several lines (default 10) above or below the cursor: +# +# navigate up phrase john: moves before the previous "john" (case-insensitive) on the preceding lines. +# navigate down third period: moves after the third period on the following lines. +# +# Besides movement, we can cut, copy, select, clear (delete), or extend the current selection: +# +# navigate cut after comma: cut the word following the next comma on the line. +# navigate left copy third word: copy the third word to the left. +# navigate extend third big: extend the selection three big words right. +# navigate down clear phrase I think: delete the next occurrence of "I think" on the following lines. +# navigate up select colon: select the closest colon on the preceeding lines. +# +# We can specify what gets selected before or after the given input: +# +# navigate select parens after equals: Select the first "(" and everything until the first ")" after the "=" +# navigate left copy all before equals: Copy everything from the start of the line until the first "=" you encounter while moving left +# navigate clear constant before semicolon: Delete the last word consisting of only uppercase characters or underscores before a ";" +# +# ===== Explanation of the grammar ===== +# +# [{user.arrow_key}]: left, right, up, down (default: right) +# Which direction to navigate in. +# left/right work on the current line. +# up/down work on the closest lines (default: 10) above or below. +# +# [{user.navigation_action}]: move, extend, select, clear, cut, copy (default: move) +# What action to perform. +# +# [{user.navigation_target_name}]: word, small, big, parens, squares, braces, quotes, angles, all, method, constant (default: word) +# The predetermined unit to select if before_or_after was specified. +# Defaults to "word" +# +# [{user.before_or_after}]: before, after (default: special behavior) +# For move/extend: where to leave the cursor, before or after the target. +# Defaults to "after" for right/down and "before" for left/up. +# +# For select/copy/cut: if absent, select/copy/cut the target iself. If +# present, the navigation_target_name before/after the target. +# +# [<user.ordinals>]: an english ordinal, like "second" (default: first) +# Which occurrence of the target to navigate to. +# +# <user.navigation_target>: one of the following: +# - a character name, like "comma" or "five". +# - "word" or "big" or "small" +# - "phrase <some text to search for>" +# Specifies the target to search for/navigate to. + +# The functionality for all these commands is covered in the lines above, but these commands are kept here for convenience. Originally from word_selection.talon. +word neck [<number_small>]: user.navigation_by_name("SELECT", "RIGHT", "DEFAULT", "word", number_small or 1) +word pre [<number_small>]: user.navigation_by_name("SELECT", "LEFT", "DEFAULT", "word", number_small or 1) +small word neck [<number_small>]: user.navigation_by_name("SELECT", "RIGHT", "DEFAULT", "small", number_small or 1) +small word pre [<number_small>]: user.navigation_by_name("SELECT", "LEFT", "DEFAULT", "small", number_small or 1) +big word neck [<number_small>]: user.navigation_by_name("SELECT", "RIGHT", "DEFAULT", "big", number_small or 1) +big word pre [<number_small>]: user.navigation_by_name("SELECT", "LEFT", "DEFAULT", "big", number_small or 1) diff --git a/talon-user/weather.talon b/talon-user/weather.talon @@ -0,0 +1,5 @@ +weather forecast: user.system_path_command('env LESS="RiX" setsid -f alacritty -e weather nebusice') +weather now: + speech.disable() + user.system_path_command('curl -sL https://wttr.in/\\?format=%C:%t:%f:%w:%p\\&m | ruby -e "d=ARGF.read.split(\':\'); puts d.first + \', temperature \' + d[2] + \', wind \' + d[3] + \', rain \' + d[4]" | say') + speech.enable()