From da4cf25cb7b20c58d395d0422aa7c33f57b367ed Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 23 Oct 2024 15:40:49 +0200 Subject: [PATCH 01/99] Clean up emoji (#1579) Dead code left by the migration to Talon list files --- tags/emoji/emoji.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/tags/emoji/emoji.py b/tags/emoji/emoji.py index f2c00cada0..682c27cced 100644 --- a/tags/emoji/emoji.py +++ b/tags/emoji/emoji.py @@ -1,19 +1,8 @@ -from pathlib import Path +from talon import Module -from talon import Context, Module - -# --- Tag definition --- mod = Module() -mod.tag("emoji", desc="Emoji, ascii emoticons and kaomoji") -# Context matching -ctx = Context() -ctx.matches = """ -tag: user.emoji -""" - -# --- Define and implement lists --- -path = Path(__file__).parents[0] +mod.tag("emoji", desc="Emoji, ascii emoticons and kaomoji") mod.list("emoticon", desc="Western emoticons (ascii)") mod.list("emoji", desc="Emoji (unicode)") From 703d9bbb1305d3a8db4498106d90a1554730b494 Mon Sep 17 00:00:00 2001 From: Philippe Hardardt Date: Wed, 23 Oct 2024 10:47:30 -0300 Subject: [PATCH 02/99] Add talon open debug (#1568) Command for opening debug window: image --------- Co-authored-by: Jeff Knaus --- plugin/talon_helpers/talon_helpers.talon | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/talon_helpers/talon_helpers.talon b/plugin/talon_helpers/talon_helpers.talon index 25e802a817..83cac8b492 100644 --- a/plugin/talon_helpers/talon_helpers.talon +++ b/plugin/talon_helpers/talon_helpers.talon @@ -1,4 +1,6 @@ talon check updates: menu.check_for_updates() +# the debug window is only available in the talon beta +talon open debug: menu.open_debug_window() talon open log: menu.open_log() talon open rebel: menu.open_repl() talon home: menu.open_talon_home() From 86aaee9bbc27851ada67b97489e3476defb97801 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 23 Oct 2024 15:06:26 +0100 Subject: [PATCH 03/99] Add tab clone command and improve file organization (#1565) Implements the tab clone command in vscode. As you can't have two tabs of the same file open in the same tab group it splits the window vertically which clones the tab left and right. Also cleans up the functions, grouping all of the splits,py, and tabs.py functions together --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley Co-authored-by: Jeff Knaus --- apps/vscode/vscode.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index c56612755a..df6cb0953c 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -223,6 +223,23 @@ def split_window_vertically(): def split_window(): actions.user.vscode("workbench.action.splitEditor") + def split_number(index: int): + supported_ordinals = [ + "First", + "Second", + "Third", + "Fourth", + "Fifth", + "Sixth", + "Seventh", + "Eighth", + ] + + if 0 <= index - 1 < len(supported_ordinals): + actions.user.vscode( + f"workbench.action.focus{supported_ordinals[index - 1]}EditorGroup" + ) + # splits.py support end # multiple_cursor.py support begin @@ -254,11 +271,14 @@ def multi_cursor_select_more_occurrences(): def multi_cursor_skip_occurrence(): actions.user.vscode("editor.action.moveSelectionToNextFindMatch") + # multiple_cursor.py support end + def command_search(command: str = ""): actions.user.vscode("workbench.action.showCommands") if command != "": actions.insert(command) + # tabs.py support begin def tab_jump(number: int): if number < 10: if is_mac: @@ -278,16 +298,12 @@ def tab_final(): else: actions.key("alt-0") - # splits.py support begin - def split_number(index: int): - """Navigates to a the specified split""" - if index < 9: - if is_mac: - actions.key(f"cmd-{index}") - else: - actions.key(f"ctrl-{index}") + def tab_duplicate(): + # Duplicates the current tab into a new tab group + # vscode does not allow duplicate tabs in the same tab group, and so is implemented through splits + actions.user.split_window_vertically() - # splits.py support end + # tabs.py support end # find_and_replace.py support begin From f6b4f85170a04b381bee257e1be3792b1b381df9 Mon Sep 17 00:00:00 2001 From: reecpj <54263157+reecpj@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:42:30 +1000 Subject: [PATCH 04/99] Fix window maximize not being preserved when moving screen (#1428) Now windows maximized on the source screen get properly maximized on the dest screen --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jeff Knaus --- core/windows_and_tabs/window_snap.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core/windows_and_tabs/window_snap.py b/core/windows_and_tabs/window_snap.py index 920d421c18..f23b26f240 100644 --- a/core/windows_and_tabs/window_snap.py +++ b/core/windows_and_tabs/window_snap.py @@ -36,14 +36,6 @@ 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)) @@ -120,6 +112,7 @@ def _move_to_screen( dest = dest_screen.visible_rect src = src_screen.visible_rect + maximized = window.maximized how = settings.get("user.window_snap_screen") if how == "size aware": r = window.rect @@ -133,6 +126,8 @@ def _move_to_screen( r.width = right - left r.height = bot - top window.rect = r + if maximized: + window.maximized = True return # TODO: Test vertical screen with different aspect ratios @@ -189,6 +184,8 @@ def _move_to_screen( width = window.rect.width * proportional_width height = window.rect.height * proportional_height _set_window_pos(window, x=x, y=y, width=width, height=height) + if maximized: + window.maximized = True def _snap_window_helper(window, pos): From 9e448767f3dbd47ae5510672879a0191a33f6dc0 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 26 Oct 2024 17:05:57 +0100 Subject: [PATCH 05/99] Add not operators (#1582) Add the unary operator not and bitwise not. Initially implemented in C & Python simply because I use those the most Surprised this wasn't implemented previously, not sure if there was a reason why --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- lang/c/c.py | 6 ++++++ lang/python/python.py | 6 ++++++ lang/tags/operators_bitwise.py | 3 +++ lang/tags/operators_bitwise.talon | 1 + lang/tags/operators_math.py | 3 +++ lang/tags/operators_math.talon | 1 + 6 files changed, 20 insertions(+) diff --git a/lang/c/c.py b/lang/c/c.py index 44fe90abd3..b485d0c732 100644 --- a/lang/c/c.py +++ b/lang/c/c.py @@ -266,6 +266,9 @@ def code_operator_and(): def code_operator_or(): actions.auto_insert(" || ") + def code_operator_not(): + actions.auto_insert("!") + def code_operator_bitwise_and(): actions.auto_insert(" & ") @@ -284,6 +287,9 @@ def code_operator_bitwise_exclusive_or(): def code_operator_bitwise_exclusive_or_assignment(): actions.auto_insert(" ^= ") + def code_operator_bitwise_not(): + actions.auto_insert("~") + def code_operator_bitwise_left_shift(): actions.auto_insert(" << ") diff --git a/lang/python/python.py b/lang/python/python.py index ead965deb6..3945bc0dbe 100644 --- a/lang/python/python.py +++ b/lang/python/python.py @@ -214,6 +214,9 @@ def code_operator_and(): def code_operator_or(): actions.auto_insert(" or ") + def code_operator_not(): + actions.auto_insert("not ") + def code_operator_in(): actions.auto_insert(" in ") @@ -238,6 +241,9 @@ def code_operator_bitwise_exclusive_or(): def code_operator_bitwise_exclusive_or_assignment(): actions.auto_insert(" ^= ") + def code_operator_bitwise_not(): + actions.auto_insert("~") + def code_operator_bitwise_left_shift(): actions.auto_insert(" << ") diff --git a/lang/tags/operators_bitwise.py b/lang/tags/operators_bitwise.py index c5c8fdf176..d5f7797c33 100644 --- a/lang/tags/operators_bitwise.py +++ b/lang/tags/operators_bitwise.py @@ -14,6 +14,9 @@ def code_operator_bitwise_and(): def code_operator_bitwise_or(): """code_operator_bitwise_or""" + def code_operator_bitwise_not(): + """code_operator_bitwise_not""" + def code_operator_bitwise_exclusive_or(): """code_operator_bitwise_exclusive_or""" diff --git a/lang/tags/operators_bitwise.talon b/lang/tags/operators_bitwise.talon index fffaaed5ec..e403e66c89 100644 --- a/lang/tags/operators_bitwise.talon +++ b/lang/tags/operators_bitwise.talon @@ -4,6 +4,7 @@ tag: user.code_operators_bitwise #bitwise operators [op] bitwise and: user.code_operator_bitwise_and() [op] bitwise or: user.code_operator_bitwise_or() +[op] bitwise not: user.code_operator_bitwise_not() # TODO: split these out into separate logical and bitwise operator commands diff --git a/lang/tags/operators_math.py b/lang/tags/operators_math.py index 0c474c5625..34507bf690 100644 --- a/lang/tags/operators_math.py +++ b/lang/tags/operators_math.py @@ -53,6 +53,9 @@ def code_operator_and(): def code_operator_or(): """code_operator_or""" + def code_operator_not(): + """code_operator_not""" + def code_operator_in(): """code_operator_in""" diff --git a/lang/tags/operators_math.talon b/lang/tags/operators_math.talon index a1e1fddc75..57d590e0d8 100644 --- a/lang/tags/operators_math.talon +++ b/lang/tags/operators_math.talon @@ -20,6 +20,7 @@ op mod: user.code_operator_modulo() # logical operators (op | logical) and: user.code_operator_and() (op | logical) or: user.code_operator_or() +(op | logical) not: user.code_operator_not() # set operators (op | is) in: user.code_operator_in() From c5b38ae8b7915cfa1f1608c111548067c59339ca Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 26 Oct 2024 18:20:51 +0200 Subject: [PATCH 06/99] Remove unprefixed numbers command (#1576) This is a source of misrecognitions for new users. We have now had the deprecation message for nine months so I think it's fine to remove this. --- core/mouse_grid/mouse_grid_open.talon | 2 -- core/numbers/numbers.py | 2 +- core/numbers/numbers_unprefixed.talon | 7 +++---- settings.talon | 9 ++++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/core/mouse_grid/mouse_grid_open.talon b/core/mouse_grid/mouse_grid_open.talon index c3ed00ffe5..a22aaf2194 100644 --- a/core/mouse_grid/mouse_grid_open.talon +++ b/core/mouse_grid/mouse_grid_open.talon @@ -1,7 +1,5 @@ tag: user.mouse_grid_showing - -# Force prefixed numbers elsewhere in the config, which allows unprefixed use below -tag(): user.prefixed_numbers : user.grid_narrow(number_key) grid (off | close | hide): user.grid_close() diff --git a/core/numbers/numbers.py b/core/numbers/numbers.py index f19c656d78..20a807c0b6 100644 --- a/core/numbers/numbers.py +++ b/core/numbers/numbers.py @@ -177,7 +177,7 @@ def split_list(value, l: list) -> Iterator: number_small_map = {n: i for i, n in enumerate(number_small_list)} mod.list("number_small", desc="List of small numbers") -mod.tag("prefixed_numbers", desc="Require prefix when saying a number") +mod.tag("unprefixed_numbers", desc="Dont require prefix when saying a number") ctx.lists["self.number_small"] = number_small_map.keys() diff --git a/core/numbers/numbers_unprefixed.talon b/core/numbers/numbers_unprefixed.talon index 9aac81ea15..87c8d84ef1 100644 --- a/core/numbers/numbers_unprefixed.talon +++ b/core/numbers/numbers_unprefixed.talon @@ -1,5 +1,4 @@ -not tag: user.prefixed_numbers +tag: user.unprefixed_numbers - -: - insert("{number_string}") - user.deprecate_command("2024-01-27", "", "numb ") + +: "{number_string}" diff --git a/settings.talon b/settings.talon index 99e4331d5a..644ac46dcd 100644 --- a/settings.talon +++ b/settings.talon @@ -90,8 +90,7 @@ settings(): # See issue #688 for more detail: https://github.com/talonhub/community/issues/688 # tag(): user.mouse_cursor_commands_enable -# Uncomment the below to disable support for saying numbers without a prefix. -# By default saying "one" would write "1", however many users find this behavior -# prone to false positives. If you uncomment this, you will need to say -# "numb one" to write "1". Note that this tag will eventually be activated by default -# tag(): user.prefixed_numbers +# Uncomment the below to enable support for saying numbers without a prefix. +# By default you need to say "numb one" to write "1". If you uncomment this, +# you can say "one" to write "1". +# tag(): user.unprefixed_numbers From 0a6a001ed104cbad950950dc18441af371eab53e Mon Sep 17 00:00:00 2001 From: Aaron Adams Date: Sun, 27 Oct 2024 20:09:05 +0000 Subject: [PATCH 07/99] add nix support to app switcher (#1471) This tweaks the app switcher code a bit to work when using applications installed via nix on both linux and darwin. It also removes the `/usr/bin/` assumption for linux executable path, and uses `which` instead to find the real path instead. Due to pulling out the path that way now, I had to tweak the regex slightly to get it to also match if there's only one argument to the app, as it will no longer be prefixed with a space. --- core/app_switcher/app_switcher.py | 99 ++++++++++++++++++------------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/core/app_switcher/app_switcher.py b/core/app_switcher/app_switcher.py index d3ca588da7..15a1dac7e1 100644 --- a/core/app_switcher/app_switcher.py +++ b/core/app_switcher/app_switcher.py @@ -35,21 +35,6 @@ running_application_dict = {} -mac_application_directories = [ - "/Applications", - "/Applications/Utilities", - "/System/Applications", - "/System/Applications/Utilities", -] - -linux_application_directories = [ - "/usr/share/applications", - "/usr/local/share/applications", - os.path.expandvars("/home/$USER/.local/share/applications"), - "/var/lib/flatpak/exports/share/applications", - "/var/lib/snapd/desktop/applications", -] - words_to_exclude = [ "zero", "one", @@ -145,7 +130,7 @@ def list_known_folder(folder_id, htoken=None): result.sort(key=lambda x: x.upper()) return result - def get_windows_apps(): + def get_apps(): items = {} for item in enum_known_folder(FOLDERID_AppsFolder): try: @@ -168,17 +153,29 @@ def get_windows_apps(): return items - -if app.platform == "linux": +elif app.platform == "linux": import configparser import re - def get_linux_apps(): + linux_application_directories = [ + "/usr/share/applications", + "/usr/local/share/applications", + f"{Path.home()}/.local/share/applications", + "/var/lib/flatpak/exports/share/applications", + "/var/lib/snapd/desktop/applications", + ] + xdg_data_dirs = os.environ.get("XDG_DATA_DIRS") + if xdg_data_dirs is not None: + for directory in xdg_data_dirs.split(":"): + linux_application_directories.append(f"{directory}/applications") + linux_application_directories = list(set(linux_application_directories)) + + def get_apps(): # app shortcuts in program menu are contained in .desktop files. This function parses those files for the app name and command items = {} # find field codes in exec key with regex # https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables - args_pattern = re.compile(r" \%[UufFcik]") + args_pattern = re.compile(r"\%[UufFcik]") for base in linux_application_directories: if os.path.isdir(base): for entry in os.scandir(base): @@ -187,7 +184,7 @@ def get_linux_apps(): config = configparser.ConfigParser(interpolation=None) config.read(entry.path) # only parse shortcuts that are not hidden - if config.has_option("Desktop Entry", "NoDisplay") == False: + if not config.has_option("Desktop Entry", "NoDisplay"): name_key = config["Desktop Entry"]["Name"] exec_key = config["Desktop Entry"]["Exec"] # remove extra quotes from exec @@ -197,16 +194,51 @@ def get_linux_apps(): if exec_key[0] == "/": items[name_key] = re.sub(args_pattern, "", exec_key) else: - items[name_key] = "/usr/bin/" + re.sub( - args_pattern, "", exec_key + exec_path = ( + subprocess.check_output( + ["which", exec_key.split()[0]], + stderr=subprocess.DEVNULL, + ) + .decode("utf-8") + .strip() + ) + items[name_key] = ( + exec_path + + " " + + re.sub( + args_pattern, + "", + " ".join(exec_key.split()[1:]), + ) ) - except: + except Exception: print( - "get_linux_apps: skipped parsing application file ", + "linux get_apps(): skipped parsing application file ", entry.name, ) return items +elif app.platform == "mac": + mac_application_directories = [ + "/Applications", + "/Applications/Utilities", + "/System/Applications", + "/System/Applications/Utilities", + f"{Path.home()}/Applications", + f"{Path.home()}/.nix-profile/Applications", + ] + + def get_apps(): + items = {} + for base in mac_application_directories: + base = os.path.expanduser(base) + if os.path.isdir(base): + for name in os.listdir(base): + path = os.path.join(base, name) + name = name.rsplit(".", 1)[0].lower() + items[name] = path + return items + @mod.capture(rule="{self.running}") # | )") def running_applications(m) -> str: @@ -398,22 +430,9 @@ def gui_running(gui: imgui.GUI): 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 - - elif app.platform == "windows": - launch = get_windows_apps() - - elif app.platform == "linux": - launch = get_linux_apps() + launch = get_apps() - # actions.user.talon_pretty_print(launch) + # actions.user.talon_pretty_print(launch) ctx.lists["self.launch"] = actions.user.create_spoken_forms_from_map( launch, words_to_exclude From e78c7520fe75bdc83f64967850cc677cef63f4eb Mon Sep 17 00:00:00 2001 From: Trillium Smith Date: Sun, 27 Oct 2024 13:12:36 -0700 Subject: [PATCH 08/99] Add pop_twice_to_wake and pop_twice_to_repeat functions (#1398) Following on from #1364 Haven't seen that PR change much over the past few weeks, I've been using that functionality in my personal repo and it's been great so I wanted to make sure other users got to try it out. ## Changes from #1364 - Switched to tag at request of @pokey in review - This will prevent blocking on other pop commands - Added Module import to be able to define tag - Updated context matcher to look for `user.pop_twice_to_wake` - Dropped changes to mouse.py --- - Merged with #1399 as per discussion in grooming session - Add explanation comments - Switch double pop time minimum and maximum to a user.setting --------- Co-authored-by: Guenther Schmitz <727316+gpunktschmitz@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> Co-authored-by: Jeff Knaus --- core/modes/sleep_mode_pop_twice_to_wake.py | 44 ++++++++++++++++++++++ plugin/repeater/pop_twice_to_repeat.py | 30 +++++++++++++++ settings.talon | 15 ++++++++ 3 files changed, 89 insertions(+) create mode 100644 core/modes/sleep_mode_pop_twice_to_wake.py create mode 100644 plugin/repeater/pop_twice_to_repeat.py diff --git a/core/modes/sleep_mode_pop_twice_to_wake.py b/core/modes/sleep_mode_pop_twice_to_wake.py new file mode 100644 index 0000000000..2fe45f938e --- /dev/null +++ b/core/modes/sleep_mode_pop_twice_to_wake.py @@ -0,0 +1,44 @@ +import time + +from talon import Context, Module, actions, settings + +ctx = Context() +mod = Module() + +mod.tag("pop_twice_to_wake", desc="tag for enabling pop twice to wake in sleep mode") + +mod.setting( + "double_pop_speed_minimum", + type=float, + desc="""Shortest time in seconds to accept a second pop to trigger additional actions""", + default=0.1, +) + +mod.setting( + "double_pop_speed_maximum", + type=float, + desc="""Longest time in seconds to accept a second pop to trigger additional actions""", + default=0.3, +) + +ctx.matches = r""" +mode: sleep +and tag: user.pop_twice_to_wake +""" + +time_last_pop = 0 + + +@ctx.action_class("user") +class UserActions: + def noise_trigger_pop(): + # Since zoom mouse is registering against noise.register("pop", on_pop), let that take priority + if actions.tracking.control_zoom_enabled(): + return + global time_last_pop + double_pop_speed_minimum = settings.get("user.double_pop_speed_minimum") + double_pop_speed_maximum = settings.get("user.double_pop_speed_maximum") + delta = time.perf_counter() - time_last_pop + if delta >= double_pop_speed_minimum and delta <= double_pop_speed_maximum: + actions.speech.enable() + time_last_pop = time.perf_counter() diff --git a/plugin/repeater/pop_twice_to_repeat.py b/plugin/repeater/pop_twice_to_repeat.py new file mode 100644 index 0000000000..48ca816e7b --- /dev/null +++ b/plugin/repeater/pop_twice_to_repeat.py @@ -0,0 +1,30 @@ +import time + +from talon import Context, Module, actions, settings + +ctx = Context() +mod = Module() + +mod.tag("pop_twice_to_repeat", desc="tag for enabling pop twice to repeat") + +ctx.matches = r""" +mode: command +and tag: user.pop_twice_to_repeat +""" + +time_last_pop = 0 + + +@ctx.action_class("user") +class UserActions: + def noise_trigger_pop(): + # Since zoom mouse is registering against noise.register("pop", on_pop), let that take priority + if actions.tracking.control_zoom_enabled(): + return + global time_last_pop + delta = time.perf_counter() - time_last_pop + double_pop_speed_minimum = settings.get("user.double_pop_speed_minimum") + double_pop_speed_maximum = settings.get("user.double_pop_speed_maximum") + if delta >= double_pop_speed_minimum and delta <= double_pop_speed_maximum: + actions.core.repeat_command() + time_last_pop = time.perf_counter() diff --git a/settings.talon b/settings.talon index 644ac46dcd..443252eb3d 100644 --- a/settings.talon +++ b/settings.talon @@ -60,6 +60,10 @@ settings(): # Set the total number of command history lines to display user.command_history_size = 50 + # Set the time window size for to for pop_twice_to_sleep and pop_twice_to_repeat. By default, the pops must be more than 0.1 seconds apart and less then 0.3 seconds, to reduce false positives + user.double_pop_speed_minimum = 0.1 + user.double_pop_speed_maximum = 0.3 + # Uncomment to add a directory (relative to the Talon user dir) with additional # .snippet files. Changing this setting requires a restart of Talon. # user.snippets_dir = "snippets" @@ -90,6 +94,17 @@ settings(): # See issue #688 for more detail: https://github.com/talonhub/community/issues/688 # tag(): user.mouse_cursor_commands_enable +# Uncomment below enable pop_twice_to_wake +# Without this tag noise_trigger_pop is usually associated with pop to click actions +# Enabling this tag disables other pop to click actions in sleep mode, including pop to click +# tag(): user.pop_twice_to_wake + +# Uncomment below enable pop_twice_to_repeat +# Enabling this tag will repeat the last command when two pops are heard within the allotted time window +# Without this tag noise_trigger_pop is usually associated with pop to click actions +# Enabling this tag disables other pop to click actions in command mode, including pop to click +# tag(): user.pop_twice_to_repeat + # Uncomment the below to enable support for saying numbers without a prefix. # By default you need to say "numb one" to write "1". If you uncomment this, # you can say "one" to write "1". From 720572f8eb31479d7f6be52944afb14c61418f8f Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sun, 27 Oct 2024 20:14:55 +0000 Subject: [PATCH 09/99] File manager commands improvements (#1497) Previously the file manager commands were quite messy and some commands were not consistent so I cleaned it up. Added an optional `[select | cell]` for all select commands, making them all consistent Also enforced ordering of non numbered item, and then numbered item Does have a slight breaking change in that `open ` has now moved too `open num ` And now instead we have the command `open {user.file_manager_files}` But this does mean we have consistency across open, select, and follow for both files and folders, which I think is worth the small breaking change. Thoughts? --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley --- tags/file_manager/file_manager.talon | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tags/file_manager/file_manager.talon b/tags/file_manager/file_manager.talon index aa4dd8ff1a..7d5f65d51f 100644 --- a/tags/file_manager/file_manager.talon +++ b/tags/file_manager/file_manager.talon @@ -8,25 +8,25 @@ manager close: user.file_manager_hide_pickers() manager refresh: user.file_manager_update_lists() go : user.file_manager_open_directory(system_path) (go parent | daddy): user.file_manager_open_parent() +^follow {user.file_manager_directories}$: + user.file_manager_open_directory(file_manager_directories) ^follow numb $: directory = user.file_manager_get_directory_by_index(number_small - 1) user.file_manager_open_directory(directory) -^follow {user.file_manager_directories}$: - user.file_manager_open_directory(file_manager_directories) -^(select | cell) folder {user.file_manager_directories}$: +^[select | cell] folder {user.file_manager_directories}$: user.file_manager_select_directory(file_manager_directories) -^open $: - file = user.file_manager_get_file_by_index(number_small - 1) - user.file_manager_open_file(file) -^folder numb $: +^[select | cell] folder numb $: directory = user.file_manager_get_directory_by_index(number_small - 1) user.file_manager_select_directory(directory) -^file numb $: +^[select | cell] file {user.file_manager_files}$: + user.file_manager_select_file(file_manager_files) +^[select | cell] file numb $: file = user.file_manager_get_file_by_index(number_small - 1) user.file_manager_select_file(file) -^file {user.file_manager_files}$: user.file_manager_select_file(file_manager_files) -^(select | cell) file {user.file_manager_files}$: - user.file_manager_select_file(file_manager_files) +^open {user.file_manager_files}$: user.file_manager_open_file(file_manager_files) +^open numb $: + file = user.file_manager_get_file_by_index(number_small - 1) + user.file_manager_open_file(file) #new folder folder new : user.file_manager_new_folder(text) From f968f36722e43f7ac28cbe03cdf77d4be6d425d1 Mon Sep 17 00:00:00 2001 From: David Tejada Date: Sat, 2 Nov 2024 17:03:15 +0100 Subject: [PATCH 10/99] Fix maximize split VSCode action (#1583) It looks like the vscode command has changed. --- apps/vscode/vscode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index df6cb0953c..245479423a 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -191,7 +191,7 @@ def split_flip(): actions.user.vscode("workbench.action.toggleEditorGroupLayout") def split_maximize(): - actions.user.vscode("workbench.action.maximizeEditor") + actions.user.vscode("workbench.action.toggleMaximizeEditorGroup") def split_reset(): actions.user.vscode("workbench.action.evenEditorWidths") From 0dd9478a67c709aa0856586dfb72fa71ee57797f Mon Sep 17 00:00:00 2001 From: thetomcraig Date: Sat, 2 Nov 2024 09:13:10 -0700 Subject: [PATCH 11/99] Add mission control to focus for app switcher (#1542) This adds functionality for application switching on MacOS. The actual equivalent on MacOS is the Application Switcher (Command + Tab), but there is no built in way for this, to be persistent on screen with a single hot key. The best alternative is Mission Control, which will show all of the open windows. --------- Co-authored-by: Tom Craig <147539579+thetomcraig-aya@users.noreply.github.com> Co-authored-by: Nicholas Riley --- apps/dock/dock.py | 22 ++++++++++++++++++++++ apps/dock/dock.talon | 5 +++++ core/app_switcher/app_switcher.py | 3 +++ 3 files changed, 30 insertions(+) create mode 100644 apps/dock/dock.py create mode 100644 apps/dock/dock.talon diff --git a/apps/dock/dock.py b/apps/dock/dock.py new file mode 100644 index 0000000000..7174ac51a6 --- /dev/null +++ b/apps/dock/dock.py @@ -0,0 +1,22 @@ +from talon import Context, Module, actions, clip, ui + +ctx = Context() +mod = Module() + +ctx.matches = """ +os: mac +""" + + +@mod.action_class +class Actions: + def dock_send_notification(notification: str): + """Send a CoreDock notification to the macOS Dock using SPI""" + + +@ctx.action_class("user") +class UserActions: + def dock_send_notification(notification: str): + from talon.mac.dock import dock_notify + + dock_notify(notification) diff --git a/apps/dock/dock.talon b/apps/dock/dock.talon new file mode 100644 index 0000000000..23879a9d58 --- /dev/null +++ b/apps/dock/dock.talon @@ -0,0 +1,5 @@ +os: mac +- +^desktop$: user.dock_send_notification("com.apple.showdesktop.awake") +^window$: user.dock_send_notification("com.apple.expose.front.awake") +^launch pad$: user.dock_send_notification("com.apple.launchpad.toggle") diff --git a/core/app_switcher/app_switcher.py b/core/app_switcher/app_switcher.py index 15a1dac7e1..7c81595fa1 100644 --- a/core/app_switcher/app_switcher.py +++ b/core/app_switcher/app_switcher.py @@ -399,6 +399,9 @@ def switcher_menu(): """Open a menu of running apps to switch to""" if app.platform == "windows": actions.key("alt-ctrl-tab") + elif app.platform == "mac": + # MacOS equivalent is "Mission Control" + actions.user.dock_send_notification("com.apple.expose.awake") else: print("Persistent Switcher Menu not supported on " + app.platform) From f97f87bf9fd13293f5b95734ff51b5b86d2ace84 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Sat, 2 Nov 2024 09:16:49 -0700 Subject: [PATCH 12/99] Select final web area to avoid devtools (#1548) On mac os, I was having an issue with `browser.host` which can be reproduced with the following steps. 1. put the following in a talon file: ``` browser.host: www.google.com - testing: app.notify("testing") ``` 2. in firefox, open google.com 3. say "testing", verify you get the notification 4. open dev tools, focus another app, and then focused firefox again 5. say "testing", you will NOT get the notification It seems that in this scenario browser.host context matcher is no longer respected. I hunted down the issue to this line. My change is probably pretty brittle, but at least works on mac (the only operating system where this code applies) chrome and firefox. Some slack discussion took place here: https://talonvoice.slack.com/archives/C9MHQ4AGP/p1724030602939929 --- tags/browser/browser_mac.py | 48 ++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/tags/browser/browser_mac.py b/tags/browser/browser_mac.py index 2767cd91fe..34c3dde10d 100644 --- a/tags/browser/browser_mac.py +++ b/tags/browser/browser_mac.py @@ -1,5 +1,4 @@ from talon import Context, actions, app, mac, ui -from talon.mac import applescript ctx = Context() ctx.matches = r""" @@ -27,22 +26,37 @@ def address(): except IndexError: return "" try: - web_area = window.element.children.find_one(AXRole="AXWebArea") - address = web_area.AXURL - except (ui.UIErr, AttributeError): - try: - address = applescript.run( - """ - tell application id "{bundle}" - if not (exists (window 1)) then return "" - return the URL of the active tab of the front window - end tell""".format( - bundle=actions.app.bundle() - ) - ) - except mac.applescript.ApplescriptErr: - return actions.next() - return address + # for Firefox and Chromium-based browsers (if accessibility available) + addresses = [ + web_area.AXURL for web_area in window.children.find(AXRole="AXWebArea") + ] + match len(addresses): + case 0: + pass + case 1: + return addresses[0] + case _: + addresses = [ + a + for a in addresses + if not ( + a.startswith("devtools:") + or a.startswith("about:devtools") + or a.startswith("chrome://devtools/") + ) + ] + if len(addresses) == 1: + return addresses[0] + except (ui.UIErr, AttributeError) as e: + pass + try: + # for Chromium-based browsers (if scripting available) + front_window = window.appscript() + if tab := getattr(front_window, "active_tab", None): + return tab.URL() + return "" + except: + return actions.next() def bookmark(): actions.key("cmd-d") From f58302057390c6c59c4df3d659edf3dac611303c Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 2 Nov 2024 12:52:20 -0400 Subject: [PATCH 13/99] Swap go back and go forward commands on Mac. (#1586) Fixes #1563. --- core/navigation/navigation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/navigation/navigation.py b/core/navigation/navigation.py index bb76ff9bdf..0069efa0d6 100644 --- a/core/navigation/navigation.py +++ b/core/navigation/navigation.py @@ -26,10 +26,10 @@ def go_forward(): @ctx_mac.action_class("user") class MacActions: def go_back(): - actions.key("cmd-]") + actions.key("cmd-[") def go_forward(): - actions.key("cmd-[") + actions.key("cmd-]") @mod.action_class From 5beb931dffe68071e301bfb35629693c0c3a6a04 Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 2 Nov 2024 17:54:16 -0400 Subject: [PATCH 14/99] Use registry.last_active_contexts instead of registry.active_contexts() in help system. (#1587) Per aegis, it is extra bad to be calling methods on the registry and upcoming Talon versions will break this usage. Could likely remove our own cache now, but wanted to get this potential break-fix in first. --- core/help/help.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/help/help.py b/core/help/help.py index 70efd5f183..b77920b1a1 100644 --- a/core/help/help.py +++ b/core/help/help.py @@ -69,7 +69,7 @@ def update_title(): if selected_context is None: refresh_context_command_map(show_enabled_contexts_only) else: - update_active_contexts_cache(registry.active_contexts()) + update_active_contexts_cache(registry.last_active_contexts) @imgui.open(y=0) @@ -396,7 +396,7 @@ def update_active_contexts_cache(active_contexts): def refresh_context_command_map(enabled_only=False): - active_contexts = registry.active_contexts() + active_contexts = registry.last_active_contexts local_context_map = {} local_display_name_to_context_name_map = {} @@ -659,7 +659,7 @@ def help_selected_context(m: str): refresh_context_command_map() else: selected_context_page = 1 - update_active_contexts_cache(registry.active_contexts()) + update_active_contexts_cache(registry.last_active_contexts) selected_context = m hide_all_help_guis() @@ -761,7 +761,7 @@ def help_refresh(): if selected_context is None: refresh_context_command_map(show_enabled_contexts_only) else: - update_active_contexts_cache(registry.active_contexts()) + update_active_contexts_cache(registry.last_active_contexts) def help_hide(): """Hides the help""" From b4e7a3644820b37b3a85791799d5c2f0960b7327 Mon Sep 17 00:00:00 2001 From: maxbruening <56445556+maxbruening@users.noreply.github.com> Date: Sun, 3 Nov 2024 00:05:10 +0100 Subject: [PATCH 15/99] Extending the pages tag (#1455) This pull request aims to extend the pages tag and applies the new commands for foxit, Sumatra, and the adobe pdf reader. Currently, the pages tag seems to be used by document viewers, mostly pdf readers. This type of software typically has the ability to rotate pages, and also to go back and forth when users click on an internal link. this pull request adds these functions to the pages tag. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Andreas Arvidsson Co-authored-by: Jeff Knaus --- apps/adobe/adobe_acrobat_reader_dc_win.py | 6 ++++++ apps/foxit_reader/foxit_reader.py | 6 ++++++ apps/foxit_reader/foxit_reader.talon | 5 ----- apps/sumatrapdf/sumatrapdf.py | 6 ++++++ tags/pages/pages.py | 6 ++++++ tags/pages/pages.talon | 4 ++++ 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/adobe/adobe_acrobat_reader_dc_win.py b/apps/adobe/adobe_acrobat_reader_dc_win.py index 6a77674935..846ec5c91a 100644 --- a/apps/adobe/adobe_acrobat_reader_dc_win.py +++ b/apps/adobe/adobe_acrobat_reader_dc_win.py @@ -53,3 +53,9 @@ def page_jump(number: int): def page_final(): actions.key("end") + + def page_rotate_right(): + actions.key("shift-ctrl-0") + + def page_rotate_left(): + actions.key("shift-ctrl-1") diff --git a/apps/foxit_reader/foxit_reader.py b/apps/foxit_reader/foxit_reader.py index 29131695c1..ca0c44158a 100644 --- a/apps/foxit_reader/foxit_reader.py +++ b/apps/foxit_reader/foxit_reader.py @@ -62,3 +62,9 @@ def page_jump(number: int): def page_final(): # actions.key("fn-right") actions.key("end") + + def page_rotate_right(): + actions.key("shift-ctrl-keypad_plus") + + def page_rotate_left(): + actions.key("shift-ctrl-keypad_minus") diff --git a/apps/foxit_reader/foxit_reader.talon b/apps/foxit_reader/foxit_reader.talon index 1ce5026597..839088911c 100644 --- a/apps/foxit_reader/foxit_reader.talon +++ b/apps/foxit_reader/foxit_reader.talon @@ -4,8 +4,3 @@ tag(): user.tabs tag(): user.pages tab close all: key(ctrl-shift-w) - -[page] rotate right: key("shift-ctrl-keypad_equals") -[page] rotate left: key("shift-ctrl-keypad_minus") - -go back: key(alt-left) diff --git a/apps/sumatrapdf/sumatrapdf.py b/apps/sumatrapdf/sumatrapdf.py index f1c5a10727..8dfd023c91 100644 --- a/apps/sumatrapdf/sumatrapdf.py +++ b/apps/sumatrapdf/sumatrapdf.py @@ -56,6 +56,12 @@ def page_jump(number: int): def page_final(): actions.key("end") + def page_rotate_right(): + actions.key("shift-ctrl-keypad_plus") + + def page_rotate_left(): + actions.key("shift-ctrl-keypad_minus") + # user.tabs def tab_jump(number: int): if number < 9: diff --git a/tags/pages/pages.py b/tags/pages/pages.py index c83022c5be..276a230f27 100644 --- a/tags/pages/pages.py +++ b/tags/pages/pages.py @@ -24,3 +24,9 @@ def page_jump(number: int): def page_final(): """Go to final page""" + + def page_rotate_right(): + """Rotates the document 90 degrees to the right""" + + def page_rotate_left(): + """Rotates the document 90 degrees to the left""" diff --git a/tags/pages/pages.talon b/tags/pages/pages.talon index 71c08c0429..4e25e9953b 100644 --- a/tags/pages/pages.talon +++ b/tags/pages/pages.talon @@ -1,6 +1,10 @@ tag: user.pages - +tag(): user.navigation + page next: user.page_next() page last: user.page_previous() go page : user.page_jump(number) go page final: user.page_final() +rotate right: user.page_rotate_right() +rotate left: user.page_rotate_left() From 742d3aa147689e084e4a4a8a8b0de95ec5536c2d Mon Sep 17 00:00:00 2001 From: Jeff Knaus Date: Sat, 9 Nov 2024 10:11:05 -0700 Subject: [PATCH 16/99] Reimplement PR #1192 (#1588) Reimplement #1192 using the new compound edit actions --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- core/edit/edit.talon | 21 --------------------- core/edit/edit_command.py | 2 ++ core/edit/edit_command_modifiers.py | 2 ++ core/edit/edit_command_modifiers.talon-list | 8 ++++++-- 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/core/edit/edit.talon b/core/edit/edit.talon index 13c15189fe..bfb3131f3f 100644 --- a/core/edit/edit.talon +++ b/core/edit/edit.talon @@ -47,11 +47,6 @@ select down: edit.extend_line_down() select word left: edit.extend_word_left() select word right: edit.extend_word_right() -select way left: edit.extend_line_start() -select way right: edit.extend_line_end() -select way up: edit.extend_file_start() -select way down: edit.extend_file_end() - # Indentation indent [more]: edit.indent_more() (indent less | out dent): edit.indent_less() @@ -76,22 +71,6 @@ clear word right: edit.extend_word_right() edit.delete() -clear way left: - edit.extend_line_start() - edit.delete() - -clear way right: - edit.extend_line_end() - edit.delete() - -clear way up: - edit.extend_file_start() - edit.delete() - -clear way down: - edit.extend_file_end() - edit.delete() - # Copy copy that: edit.copy() copy word left: user.copy_word_left() diff --git a/core/edit/edit_command.py b/core/edit/edit_command.py index a4cb58e01b..f1f7d2180f 100644 --- a/core/edit/edit_command.py +++ b/core/edit/edit_command.py @@ -9,10 +9,12 @@ ("goBefore", "line"): actions.edit.line_start, ("goBefore", "paragraph"): actions.edit.paragraph_start, ("goBefore", "document"): actions.edit.file_start, + ("goBefore", "fileStart"): actions.edit.file_start, # Go after ("goAfter", "line"): actions.edit.line_end, ("goAfter", "paragraph"): actions.edit.paragraph_end, ("goAfter", "document"): actions.edit.file_end, + ("goAfter", "fileEnd"): actions.edit.file_end, # Delete ("delete", "word"): actions.edit.delete_word, ("delete", "line"): actions.edit.delete_line, diff --git a/core/edit/edit_command_modifiers.py b/core/edit/edit_command_modifiers.py index 4b0f9bfe79..2dc647b7fc 100644 --- a/core/edit/edit_command_modifiers.py +++ b/core/edit/edit_command_modifiers.py @@ -24,6 +24,8 @@ def edit_modifier(m) -> EditModifier: "line": actions.edit.select_line, "lineEnd": actions.user.select_line_end, "lineStart": actions.user.select_line_start, + "fileStart": actions.edit.extend_file_start, + "fileEnd": actions.edit.extend_file_end, } diff --git a/core/edit/edit_command_modifiers.talon-list b/core/edit/edit_command_modifiers.talon-list index 63f0ff59b1..5ce44c920a 100644 --- a/core/edit/edit_command_modifiers.talon-list +++ b/core/edit/edit_command_modifiers.talon-list @@ -1,10 +1,14 @@ list: user.edit_modifier - - all: document paragraph: paragraph word: word - line: line line start: lineStart +way left: lineStart line end: lineEnd +way right: lineEnd +file start: fileStart +way up: fileStart +file end: fileEnd +way down: fileEnd From a436667a15c05f993f0ca23ff3ce460b78a39049 Mon Sep 17 00:00:00 2001 From: David Vo Date: Sun, 10 Nov 2024 04:32:06 +1100 Subject: [PATCH 17/99] jetbrains: Clean up and fix logging and error handling (#1445) - Remove a bunch of un-useful prints - Instead of erroring silently when it fails to send the request to the IDE, notify the user - Remove the unused `extendCommands` variable --------- Co-authored-by: Jeff Knaus Co-authored-by: Phil Cohen --- apps/jetbrains/jetbrains.py | 67 +++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 37 deletions(-) diff --git a/apps/jetbrains/jetbrains.py b/apps/jetbrains/jetbrains.py index e433e76663..ea08bbaad5 100644 --- a/apps/jetbrains/jetbrains.py +++ b/apps/jetbrains/jetbrains.py @@ -1,16 +1,14 @@ import os import os.path import tempfile -import time from pathlib import Path +from typing import Optional import requests -from talon import Context, Module, actions, clip, ui +from talon import Context, Module, actions, app, clip, ui # Courtesy of https://github.com/anonfunc/talon-user/blob/master/apps/jetbrains.py -extendCommands = [] - # Each IDE gets its own port, as otherwise you wouldn't be able # to run two at the same time and switch between them. # Note that MPS and IntelliJ ultimate will conflict... @@ -59,17 +57,16 @@ } -def _get_nonce(port, file_prefix): +def _get_nonce(port: int, file_prefix: str) -> Optional[str]: file_name = file_prefix + str(port) try: with open(os.path.join(tempfile.gettempdir(), file_name)) as fh: return fh.read() - except FileNotFoundError as e: + except FileNotFoundError: try: - home = str(Path.home()) - with open(os.path.join(home, file_name)) as fh: + with open(Path.home() / file_name) as fh: return fh.read() - except FileNotFoundError as eb: + except FileNotFoundError: print(f"Could not find {file_name} in tmp or home") return None except OSError as e: @@ -77,37 +74,27 @@ def _get_nonce(port, file_prefix): return None -def send_idea_command(cmd): - print(f"Sending {cmd}") +def send_idea_command(cmd: str) -> str: active_app = ui.active_app() bundle = active_app.bundle or active_app.name port = port_mapping.get(bundle, None) + if not port: + raise Exception(f"unknown application {bundle}") nonce = _get_nonce(port, ".vcidea_") or _get_nonce(port, "vcidea_") - proxies = {"http": None, "https": None} - print(f"sending {bundle} {port} {nonce}") - if port and nonce: - response = requests.get( - f"http://localhost:{port}/{nonce}/{cmd}", - proxies=proxies, - timeout=(0.05, 3.05), - ) - response.raise_for_status() - return response.text - - -def get_idea_location(): - return send_idea_command("location").split() + if not nonce: + raise FileNotFoundError(f"Couldn't find IDEA nonce file for port {port}") + response = requests.get( + f"http://localhost:{port}/{nonce}/{cmd}", + proxies={"http": None, "https": None}, + timeout=(0.05, 3.05), + ) + response.raise_for_status() + return response.text -def idea_commands(commands): - command_list = commands.split(",") - print("executing jetbrains", commands) - global extendCommands - extendCommands = command_list - for cmd in command_list: - if cmd: - send_idea_command(cmd.strip()) - time.sleep(0.1) + +def get_idea_location() -> list[str]: + return send_idea_command("location").split() ctx = Context() @@ -148,7 +135,15 @@ def idea_commands(commands): class Actions: def idea(commands: str): """Send a command to Jetbrains product""" - idea_commands(commands) + command_list = commands.split(",") + try: + for cmd in command_list: + if cmd: + send_idea_command(cmd.strip()) + actions.sleep(0.1) + except Exception as e: + app.notify(e) + raise def idea_grab(times: int): """Copies specified number of words to the left""" @@ -162,8 +157,6 @@ def idea_grab(times: int): send_idea_command("action EditorPaste") finally: clip.set(old_clip) - global extendCommands - extendCommands = [] ctx.matches = r""" From c555c2eb526d17c9045875f74f2a11e6bf4f9593 Mon Sep 17 00:00:00 2001 From: maxbruening <56445556+maxbruening@users.noreply.github.com> Date: Sat, 9 Nov 2024 18:32:38 +0100 Subject: [PATCH 18/99] Implements "find" tag for webbrowsers (#1405) The goal of this PR is to add basic search functionality for webbrowsers based on the new "find" tag from the other already merged [PR 1404](https://github.com/talonhub/community/pull/1404). The code has been tested on windows, but not on mac or Linux. I left a commented out version for `def find()` in the PR as this alternative version may be more robust. I checked and tested that the implemented keyboard actions work in windows for chrome, firefox, opera, edge, and brave. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jeff Knaus --- tags/browser/browser.talon | 1 + 1 file changed, 1 insertion(+) diff --git a/tags/browser/browser.talon b/tags/browser/browser.talon index 0eb6ab6ebc..5862358912 100644 --- a/tags/browser/browser.talon +++ b/tags/browser/browser.talon @@ -1,5 +1,6 @@ tag: browser - +tag(): user.find tag(): user.navigation address bar | go address | go url: browser.focus_address() From b082ac69a3644a5ad6e839c88caddb5acb0d8299 Mon Sep 17 00:00:00 2001 From: Aaron Adams Date: Sun, 10 Nov 2024 01:38:06 +0800 Subject: [PATCH 19/99] Allow apps to force lang via code.language and add special cased files (#1451) This makes some tweaks to language modes: 1. It adds a list of special-cased files that are mixed into the determination of the language, so you can return a language for things even if the file extension itself doesn't imply the language. The languages included aren't actually implemented in community, so I can keep the list empty if preferred, but I figured I'd keep it populated at first to give an example of why it's useful. 2. Adds a command to show the currently forced language 3. #1256 semi-recently went the route of removing language-specific tags. This has the side effect (afaict) that an app that doesn't expose the file extension in its title can no longer force a language (previously it would just `tag: user.`). Given we removed the tags I assume we don't want them back, so this change goes a different route and adds an action that can be overridden to force a language. It's done in a way that the language being forced by an app will still be overridden by a user manually forcing a language with `force `. I've included an obsidian skeleton to demonstrate how to use it. Obsidian is a markdown editor that doesn't include file extensions in it's title. I took a look at search.talonvoice.com and see that some people are using tag.markdown (so are their community is not up to date, or they custom implemented tags again) and some others are re-implementing markdown commands. Since I tried to use this app recently, and I don't like forcing a language across my entire system and have to remember to clear it afterwards, I figured I should add a way to do it. I'm open to other ways to do this if there is a better idea. --------- Co-authored-by: David Vo Co-authored-by: Andreas Arvidsson --- apps/obsidian/obsidian.py | 16 ++++++++++++++++ apps/obsidian/obsidian.talon | 3 +++ core/modes/language_modes.py | 27 +++++++++++++++++++++++++-- core/modes/language_modes.talon | 3 ++- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 apps/obsidian/obsidian.py create mode 100644 apps/obsidian/obsidian.talon diff --git a/apps/obsidian/obsidian.py b/apps/obsidian/obsidian.py new file mode 100644 index 0000000000..bff0d02b4f --- /dev/null +++ b/apps/obsidian/obsidian.py @@ -0,0 +1,16 @@ +from talon import Context, Module + +mod = Module() +mod.apps.obsidian = "app.name: Obsidian" + +lang_ctx = Context() +lang_ctx.matches = r""" +app: obsidian +not tag: user.code_language_forced +""" + + +@lang_ctx.action_class("code") +class CodeActions: + def language(): + return "markdown" diff --git a/apps/obsidian/obsidian.talon b/apps/obsidian/obsidian.talon new file mode 100644 index 0000000000..bb3310325c --- /dev/null +++ b/apps/obsidian/obsidian.talon @@ -0,0 +1,3 @@ +app: obsidian +- +tag(): user.tabs diff --git a/core/modes/language_modes.py b/core/modes/language_modes.py index f069fd9600..887e8b2d6e 100644 --- a/core/modes/language_modes.py +++ b/core/modes/language_modes.py @@ -1,4 +1,4 @@ -from talon import Context, Module, actions +from talon import Context, Module, actions, app # Maps language mode names to the extensions that activate them. Only put things # here which have a supported language mode; that's why there are so many @@ -50,6 +50,19 @@ "html": "html", } +# Files without specific extensions but are associated with languages +special_file_map = { + "CMakeLists.txt": "cmake", + "Makefile": "make", + "Dockerfile": "docker", + "meson.build": "meson", + ".bashrc": "bash", + ".zshrc": "zsh", + "PKGBUILD": "pkgbuild", + ".vimrc": "vimscript", + "vimrc": "vimscript", +} + # Override speakable forms for language modes. If not present, a language mode's # name is used directly. language_name_overrides = { @@ -63,7 +76,6 @@ } mod = Module() - ctx = Context() ctx_forced = Context() @@ -96,6 +108,10 @@ @ctx.action_class("code") class CodeActions: def language(): + file_name = actions.win.filename() + if file_name in special_file_map: + return special_file_map[file_name] + file_extension = actions.win.file_ext() return extension_lang_map.get(file_extension, "") @@ -123,3 +139,10 @@ def code_clear_language_mode(): global forced_language forced_language = "" ctx.tags = [] + + def code_show_forced_language_mode(): + """Show the active language for this context""" + if forced_language: + app.notify(f"Forced language: {forced_language}") + else: + app.notify("No language forced") diff --git a/core/modes/language_modes.talon b/core/modes/language_modes.talon index b94097f867..ac916b9762 100644 --- a/core/modes/language_modes.talon +++ b/core/modes/language_modes.talon @@ -1,2 +1,3 @@ ^force {user.language_mode}$: user.code_set_language_mode(language_mode) -^clear language modes$: user.code_clear_language_mode() +show [forced] language mode: user.code_show_forced_language_mode() +^clear language mode$: user.code_clear_language_mode() From e52eb18e9b35580c5bf7f84b96f8530c9056acce Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 10 Nov 2024 13:12:25 +0100 Subject: [PATCH 20/99] Remove symbols from code formatters (#1591) Remove all the symbols from the code formatter capture. Before: `"hammer one plus"` -> `One+` Now: `"hammer one plus"` -> `OnePlus` Fixes #1575 --------- Co-authored-by: Nicholas Riley --- core/text/formatters.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/core/text/formatters.py b/core/text/formatters.py index 5804089afe..361cd58ffc 100644 --- a/core/text/formatters.py +++ b/core/text/formatters.py @@ -368,9 +368,6 @@ def code_formatters(m) -> str: @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=" ( | )*" ) def format_text(m) -> str: @@ -385,12 +382,10 @@ def format_text(m) -> str: return out -@mod.capture( - rule=" ( | )*" -) +@mod.capture(rule=" ") def format_code(m) -> str: """Formats code and returns a string""" - return format_text(m) + return format_phrase(m.text, m.code_formatters) class ImmuneString: @@ -401,14 +396,15 @@ def __init__(self, string): @mod.capture( - # Add anything else into this that you want to be able to speak during a - # formatter. + # Add anything else into this that you want to have inserted when + # using a prose formatter. rule="( | (numb | numeral) )" ) def formatter_immune(m) -> ImmuneString: - """Text that can be interspersed into a formatter, e.g. characters. + """Symbols and numbers that can be interspersed into a prose formatter + (i.e., not dictated immediately after the name of the formatter) - It will be inserted directly, without being formatted. + They will be inserted directly, without being formatted. """ if hasattr(m, "number"): From 6151b3587a80dc398ab67ceddd803a24683b38dd Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 10 Nov 2024 13:34:18 +0100 Subject: [PATCH 21/99] Added support for decimal numbers (#1590) `"numb fifty five point two"` -> `55.2` --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley --- core/numbers/numbers.py | 6 ++++++ core/numbers/numbers.talon | 1 + core/numbers/numbers_unprefixed.talon | 1 + 3 files changed, 8 insertions(+) diff --git a/core/numbers/numbers.py b/core/numbers/numbers.py index 20a807c0b6..b5a72e7776 100644 --- a/core/numbers/numbers.py +++ b/core/numbers/numbers.py @@ -199,6 +199,12 @@ def number_string(m) -> str: return parse_number(list(m)) +@mod.capture(rule=" ((point | dot) )+") +def number_decimal_string(m) -> str: + """Parses a decimal number phrase, returning that number as a string.""" + return ".".join(m.number_string_list) + + @ctx.capture("number", rule="") def number(m) -> int: """Parses a number phrase, returning it as an integer.""" diff --git a/core/numbers/numbers.talon b/core/numbers/numbers.talon index 4d349a6447..d3c669b8e0 100644 --- a/core/numbers/numbers.talon +++ b/core/numbers/numbers.talon @@ -1 +1,2 @@ numb : "{number_string}" +numb : "{number_decimal_string}" diff --git a/core/numbers/numbers_unprefixed.talon b/core/numbers/numbers_unprefixed.talon index 87c8d84ef1..993a625b3e 100644 --- a/core/numbers/numbers_unprefixed.talon +++ b/core/numbers/numbers_unprefixed.talon @@ -2,3 +2,4 @@ tag: user.unprefixed_numbers - : "{number_string}" +: "{number_decimal_string}" From 94582ac96ed881caac1e07f6c633af4e4345e67c Mon Sep 17 00:00:00 2001 From: Barry Jaspan Date: Tue, 12 Nov 2024 04:13:38 -0500 Subject: [PATCH 22/99] Add dictation auto formatting rules for time of day, currency, and percentages. (#1102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add simple auto-formatting rules for: * Currency: "twelve dollars" -> $12, "two euros fifty" -> €2.50. * Percentages: "fifty percent" -> 50%, "ten point three percent" -> 10.3%. * Time of day: "one twenty two pm" -> "1:22pm", "eleven o'clock" -> 11:00, "eleven am" -> 11am, "nineteen thirty seven" -> 19:37. There is some overlap between twenty-four hours times and years people are likely to say. e.g. "twenty twenty three" will be recognized as 20:23 and not "2023". However, without this change, it is recognized as "twenty twenty three" which is pretty much never right, and the existing command "numeral twenty twenty three" still gives "2023". --------- Co-authored-by: Barry Jaspan Co-authored-by: Phil Cohen Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Michael Arntzenius Co-authored-by: Nicholas Riley Co-authored-by: Jeff Knaus --- core/numbers/numbers.py | 34 ++++++++++++++ core/text/currency.talon-list | 8 ++++ core/text/text_and_dictation.py | 83 ++++++++++++++++++++++++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 core/text/currency.talon-list diff --git a/core/numbers/numbers.py b/core/numbers/numbers.py index b5a72e7776..55bfb773af 100644 --- a/core/numbers/numbers.py +++ b/core/numbers/numbers.py @@ -1,3 +1,4 @@ +import math from typing import Iterator, Union from talon import Context, Module @@ -17,12 +18,45 @@ scales_map = {n: 10 ** (3 * (i + 1)) for i, n in enumerate(scales[1:])} scales_map["hundred"] = 100 +# Maps number words to integers values that are used to compute numeric values. numbers_map = digits_map.copy() numbers_map.update(teens_map) numbers_map.update(tens_map) numbers_map.update(scales_map) +def get_spoken_form_under_one_hundred( + start, + end, + include_oh_variant_for_single_digits, + include_default_variant_for_single_digits, +): + """Helper function to get dictionary of spoken forms for non-negative numbers in the range [start, end] under 100""" + + result = {} + + for value in range(start, end + 1): + digit_index = value % 10 + if value < 10: + if include_oh_variant_for_single_digits: + result[f"oh {digit_list[digit_index]}"] = f"0{value}" + if include_default_variant_for_single_digits: + result[f"{digit_list[digit_index]}"] = f"{value}" + elif value < 20: + teens_index = value - 10 + result[f"{teens[teens_index]}"] = f"{value}" + elif value < 100: + tens_index = math.floor(value / 10) - 2 + if digit_index > 0: + spoken_form = f"{tens[tens_index]} {digit_list[digit_index]}" + else: + spoken_form = f"{tens[tens_index]}" + + result[spoken_form] = f"{value}" + + return result + + def parse_number(l: list[str]) -> str: """Parses a list of words into a number/digit string.""" l = list(scan_small_numbers(l)) diff --git a/core/text/currency.talon-list b/core/text/currency.talon-list new file mode 100644 index 0000000000..6c9f6f275b --- /dev/null +++ b/core/text/currency.talon-list @@ -0,0 +1,8 @@ +list: user.currency +- +dollar: $ +dollars: $ +euro: € +euros: € +pound: £ +pounds: £ diff --git a/core/text/text_and_dictation.py b/core/text/text_and_dictation.py index 6f0724e2e7..b60302bf2e 100644 --- a/core/text/text_and_dictation.py +++ b/core/text/text_and_dictation.py @@ -4,6 +4,8 @@ from talon import Context, Module, actions, grammar, settings, ui +from ..numbers.numbers import get_spoken_form_under_one_hundred + mod = Module() mod.setting( @@ -16,6 +18,14 @@ mod.list("prose_modifiers", desc="Modifiers that can be used within prose") mod.list("prose_snippets", desc="Snippets that can be used within prose") mod.list("phrase_ender", "List of commands that can be used to end a phrase") +mod.list("hours_twelve", desc="Names for hours up to 12") +mod.list("hours", desc="Names for hours up to 24") +mod.list("minutes", desc="Names for minutes, 01 up to 59") +mod.list( + "currency", + desc="Currency types (e.g., dollars, euros) that can be used within prose", +) + ctx = Context() # Maps spoken forms to DictationFormat method names (see DictationFormat below). ctx.lists["user.prose_modifiers"] = { @@ -36,6 +46,25 @@ "frowny": ":-(", } +ctx.lists["user.hours_twelve"] = get_spoken_form_under_one_hundred( + 1, + 12, + include_oh_variant_for_single_digits=True, + include_default_variant_for_single_digits=True, +) +ctx.lists["user.hours"] = get_spoken_form_under_one_hundred( + 1, + 23, + include_oh_variant_for_single_digits=True, + include_default_variant_for_single_digits=True, +) +ctx.lists["user.minutes"] = get_spoken_form_under_one_hundred( + 1, + 59, + include_oh_variant_for_single_digits=True, + include_default_variant_for_single_digits=False, +) + @mod.capture(rule="{user.prose_modifiers}") def prose_modifier(m) -> Callable: @@ -64,6 +93,56 @@ def prose_number(m) -> str: return str(m) +@mod.capture( + rule=" [(dot | point) ] percent [sign|sine]" +) +def prose_percent(m) -> str: + s = m.number_string + if hasattr(m, "digit_string"): + s += "." + m.digit_string + return s + "%" + + +@mod.capture( + rule=" {user.currency} [[and] [cents|pence]]" +) +def prose_currency(m) -> str: + s = m.currency + m.number_string_1 + if hasattr(m, "number_string_2"): + s += "." + m.number_string_2 + return s + + +@mod.capture(rule="am|pm") +def time_am_pm(m) -> str: + return str(m) + + +# this matches eg "twelve thirty-four" -> 12:34 and "twelve hundred" -> 12:00. hmmmmm. +@mod.capture( + rule="{user.hours} ({user.minutes} | o'clock | hundred hours) []" +) +def prose_time_hours_minutes(m) -> str: + t = m.hours + ":" + if hasattr(m, "minutes"): + t += m.minutes + else: + t += "00" + if hasattr(m, "time_am_pm"): + t += m.time_am_pm + return t + + +@mod.capture(rule="{user.hours_twelve} ") +def prose_time_hours_am_pm(m) -> str: + return m.hours_twelve + m.time_am_pm + + +@mod.capture(rule=" | ") +def prose_time(m) -> str: + return str(m) + + @mod.capture(rule="({user.vocabulary} | )") def word(m) -> str: """A single word, including user-defined vocabulary.""" @@ -82,7 +161,7 @@ def text(m) -> str: @mod.capture( - rule="({user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | )+" + rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | )+" ) def prose(m) -> str: """Mixed words and punctuation, auto-spaced & capitalized.""" @@ -91,7 +170,7 @@ def prose(m) -> str: @mod.capture( - rule="({user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | )+" + rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | )+" ) def raw_prose(m) -> str: """Mixed words and punctuation, auto-spaced & capitalized, without quote straightening and commands (for use in dictation mode).""" From 588990de484d32a41e776ef806372a6ea3311e74 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 13 Nov 2024 07:11:44 +0100 Subject: [PATCH 23/99] Refactor hide mouse cursor code (#1593) mouse.py is to large. I have extracted all the hide/show mouse cursor code into a separate python file. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- plugin/mouse/mouse.py | 80 ++------------------------------ plugin/mouse/mouse_cursor.py | 81 +++++++++++++++++++++++++++++++++ plugin/mouse/mouse_cursor.talon | 4 +- 3 files changed, 87 insertions(+), 78 deletions(-) create mode 100644 plugin/mouse/mouse_cursor.py diff --git a/plugin/mouse/mouse.py b/plugin/mouse/mouse.py index 4088b2fd22..01313830da 100644 --- a/plugin/mouse/mouse.py +++ b/plugin/mouse/mouse.py @@ -1,5 +1,3 @@ -import os - from talon import Context, Module, actions, app, clip, cron, ctrl, imgui, settings, ui from talon_plugins import eye_zoom_mouse @@ -13,39 +11,12 @@ control_mouse_forced = False hiss_scroll_up = 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() ctx = Context() mod.list( - "mouse_button", desc="List of mouse button words to mouse_click index parameter" -) -mod.tag( - "mouse_cursor_commands_enable", desc="Tag enables hide/show mouse cursor commands" + "mouse_button", + desc="List of mouse button words to mouse_click index parameter", ) mod.setting( "mouse_enable_pop_click", @@ -120,20 +91,12 @@ def zoom_close(): if eye_zoom_mouse.zoom_mouse.state == eye_zoom_mouse.STATE_OVERLAY: actions.tracking.zoom_cancel() - 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""" actions.tracking.control_zoom_toggle(True) if settings.get("user.mouse_wake_hides_cursor"): - show_cursor_helper(False) + actions.user.mouse_cursor_hide() def mouse_drag(button: int): """Press and hold/release a specific mouse button for dragging""" @@ -161,7 +124,7 @@ def mouse_sleep(): actions.tracking.control_toggle(False) actions.tracking.control1_toggle(False) - show_cursor_helper(True) + actions.user.mouse_cursor_show() stop_scroll() actions.user.mouse_drag_end() @@ -257,41 +220,6 @@ def hiss_scroll_down(): hiss_scroll_up = False -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 OSError: - print(f"Unable to show_cursor({str(show)})") - else: - ctrl.cursor_visible(show) - - @ctx.action_class("user") class UserActions: def noise_trigger_pop(): diff --git a/plugin/mouse/mouse_cursor.py b/plugin/mouse/mouse_cursor.py new file mode 100644 index 0000000000..d0ba602b3d --- /dev/null +++ b/plugin/mouse/mouse_cursor.py @@ -0,0 +1,81 @@ +import os + +from talon import Module, app, ctrl + +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.tag( + "mouse_cursor_commands_enable", + desc="Tag enables hide/show mouse cursor commands", +) + + +@mod.action_class +class Actions: + def mouse_cursor_show(): + """Shows the cursor""" + show_cursor_helper(True) + + def mouse_cursor_hide(): + """Hides the cursor""" + show_cursor_helper(False) + + +def show_cursor_helper(show: bool): + """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 OSError: + print(f"Unable to show_cursor({show})") + else: + ctrl.cursor_visible(show) diff --git a/plugin/mouse/mouse_cursor.talon b/plugin/mouse/mouse_cursor.talon index 7391597f37..cc23c7b764 100644 --- a/plugin/mouse/mouse_cursor.talon +++ b/plugin/mouse/mouse_cursor.talon @@ -1,4 +1,4 @@ tag: user.mouse_cursor_commands_enable - -curse yes: user.mouse_show_cursor() -curse no: user.mouse_hide_cursor() +curse yes: user.mouse_cursor_show() +curse no: user.mouse_cursor_hide() From c5f32adef6db9a722e949423f2203cb7d474594f Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 13 Nov 2024 07:11:55 +0100 Subject: [PATCH 24/99] Clean up ordinals (#1596) --- core/numbers/ordinals.py | 43 +++++++++++++--------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/core/numbers/ordinals.py b/core/numbers/ordinals.py index 1d5b9af426..4a68fee662 100644 --- a/core/numbers/ordinals.py +++ b/core/numbers/ordinals.py @@ -1,21 +1,5 @@ from talon import Context, Module - -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", @@ -52,6 +36,7 @@ def ordinal(n): # 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] @@ -60,28 +45,28 @@ def ordinal(n): 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 + ordinal_small[word] = str(n) + ordinal_numbers[word] = str(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.list("ordinals", "List of ordinals (1-99)") +mod.list("ordinals_small", "List of small ordinals (1-20)") + +ctx.lists["user.ordinals"] = ordinal_numbers +ctx.lists["user.ordinals_small"] = ordinal_small -@mod.capture(rule="{self.ordinals}") +@mod.capture(rule="{user.ordinals}") def ordinals(m) -> int: - """Returns a single ordinal as a digit""" - return int(ordinal_numbers[m[0]]) + """Returns a single ordinal as an integer""" + return int(m.ordinals) -@mod.capture(rule="{self.ordinals_small}") +@mod.capture(rule="{user.ordinals_small}") def ordinals_small(m) -> int: - """Returns a single ordinal as a digit""" - return int(ordinal_numbers[m[0]]) + """Returns a single small ordinal as an integer""" + return int(m.ordinals_small) From 899e5844104232a620fad62797cdfc6a9a58e2b5 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 14 Nov 2024 07:17:19 +0100 Subject: [PATCH 25/99] Support double digit number small (#1595) Fixes #951 --- core/numbers/numbers.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/core/numbers/numbers.py b/core/numbers/numbers.py index 55bfb773af..e3e6c32487 100644 --- a/core/numbers/numbers.py +++ b/core/numbers/numbers.py @@ -28,8 +28,10 @@ def get_spoken_form_under_one_hundred( start, end, - include_oh_variant_for_single_digits, - include_default_variant_for_single_digits, + *, + include_oh_variant_for_single_digits=False, + include_default_variant_for_single_digits=False, + include_double_digits=False, ): """Helper function to get dictionary of spoken forms for non-negative numbers in the range [start, end] under 100""" @@ -38,8 +40,10 @@ def get_spoken_form_under_one_hundred( for value in range(start, end + 1): digit_index = value % 10 if value < 10: + # oh prefix digit: "oh five"-> `05` if include_oh_variant_for_single_digits: result[f"oh {digit_list[digit_index]}"] = f"0{value}" + # default digit: "five" -> `5` if include_default_variant_for_single_digits: result[f"{digit_list[digit_index]}"] = f"{value}" elif value < 20: @@ -53,6 +57,14 @@ def get_spoken_form_under_one_hundred( spoken_form = f"{tens[tens_index]}" result[spoken_form] = f"{value}" + else: + raise ValueError(f"Value {value} is not in the range [0, 100)") + + # double digits: "five one" -> `51` + if include_double_digits and value > 9: + tens_index = math.floor(value / 10) + spoken_form = f"{digit_list[tens_index]} {digit_list[digit_index]}" + result[spoken_form] = f"{value}" return result @@ -203,16 +215,14 @@ def split_list(value, l: list) -> Iterator: number_word_leading = f"({'|'.join(leading_words)})" -# Numbers used in `number_small` capture -number_small_list = [*digit_list, *teens] -for ten in tens: - number_small_list.append(ten) - number_small_list.extend(f"{ten} {digit}" for digit in digit_list[1:]) -number_small_map = {n: i for i, n in enumerate(number_small_list)} - -mod.list("number_small", desc="List of small numbers") +mod.list("number_small", "List of small (0-99) numbers") mod.tag("unprefixed_numbers", desc="Dont require prefix when saying a number") -ctx.lists["self.number_small"] = number_small_map.keys() +ctx.lists["user.number_small"] = get_spoken_form_under_one_hundred( + 0, + 99, + include_default_variant_for_single_digits=True, + include_double_digits=True, +) # TODO: allow things like "double eight" for 88 @@ -253,7 +263,7 @@ def number_signed(m): @ctx.capture("number_small", rule="{user.number_small}") def number_small(m) -> int: - return number_small_map[m.number_small] + return int(m.number_small) @mod.capture(rule=f"[negative|minus] ") From 760ef80fb466278af3952183abc07d7472d7cfe8 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 16 Nov 2024 07:34:12 +0100 Subject: [PATCH 26/99] Make draft editor more reliable (#1600) Make submission of draft editor more reliable Don't close the draft if we didn't manage to get the document text --- plugin/draft_editor/draft_editor.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/plugin/draft_editor/draft_editor.py b/plugin/draft_editor/draft_editor.py index dbb1c5fe15..4b9365d7c3 100644 --- a/plugin/draft_editor/draft_editor.py +++ b/plugin/draft_editor/draft_editor.py @@ -106,18 +106,28 @@ def get_editor_app() -> ui.App: def close_editor(submit_draft: bool) -> None: global last_draft - remove_tag("user.draft_editor_active") + actions.edit.select_all() + if submit_draft: + actions.sleep("50ms") last_draft = actions.edit.selected_text() + + if not last_draft: + actions.app.notify("Failed to get draft document text") + return + + remove_tag("user.draft_editor_active") + actions.edit.delete() actions.app.tab_close() + if submit_draft: try: actions.user.switcher_focus_window(original_window) except Exception: app.notify( - "Failed to focus on window to submit draft, manually focus on intended destination and use draft submit again" + "Failed to focus on window to submit draft, manually focus intended destination and use 'draft submit' again" ) else: actions.sleep("300ms") @@ -126,4 +136,4 @@ def close_editor(submit_draft: bool) -> None: try: actions.user.switcher_focus_window(original_window) except Exception: - app.notify("Failed to focus on previous window, leaving editor open") + app.notify("Failed to focus previous window, leaving editor open") From cb54f258f73a744da2d14af54ae1e5ddef121132 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 16 Nov 2024 07:39:48 +0100 Subject: [PATCH 27/99] Extract code languages into separate file (#1599) Fixes #1585 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- core/modes/code_languages.py | 77 ++++++++++++++++++++++++++++ core/modes/language_modes.py | 99 +++++------------------------------- core/snippets/snippets.py | 8 +-- 3 files changed, 93 insertions(+), 91 deletions(-) create mode 100644 core/modes/code_languages.py diff --git a/core/modes/code_languages.py b/core/modes/code_languages.py new file mode 100644 index 0000000000..e13f06abe4 --- /dev/null +++ b/core/modes/code_languages.py @@ -0,0 +1,77 @@ +class Language: + id: str + spoken_forms: list[str] + extensions: list[str] + + def __init__(self, id: str, spoken_form: str | list[str], extensions: list[str]): + self.id = id + self.spoken_forms = ( + [spoken_form] if isinstance(spoken_form, str) else spoken_form + ) + self.extensions = extensions + + +# Maps code language identifiers, names and file extensions. Only put languages +# here which have a supported language mode; that's why there are so many +# commented out entries. +code_languages = [ + # Language("assembly", "assembly", ["asm", "s"]), + # Language("bash", "bash", ["sh", "bashbook"]), + Language("batch", "batch", ["bat"]), + Language("c", "see", ["c", "h"]), + # Language("cmake", "see make", ["cmake"]), + # Language("cplusplus", "see plus plus", ["cpp", "hpp"]), + Language("csharp", "see sharp", ["cs"]), + Language("css", "c s s", ["css"]), + # Language("elisp", "elisp", ["el"]), + Language("elixir", "elixir", ["ex"]), + # Language("elm", "elm", ["elm"]), + Language("gdb", "g d b", ["gdb"]), + Language("go", ["go lang", "go language"], ["go"]), + # html doesn't actually have a language mode, but we do have snippets. + Language("html", "html", ["html"]), + Language("java", "java", ["java"]), + Language("javascript", "java script", ["js"]), + Language("javascriptreact", "java script react", ["jsx"]), + # Language("json", "json", ["json"]), + # Language("jsonl", "json lines", ["jsonl"]), + Language("kotlin", "kotlin", ["kt"]), + Language("lua", "lua", ["lua"]), + Language("markdown", "mark down", ["md"]), + # Language("perl", "perl", ["pl"]), + Language("php", "p h p", ["php"]), + # Language("powershell", "power shell", ["ps1"]), + Language("protobuf", "proto buf", ["proto"]), + Language("python", "python", ["py"]), + Language("r", "are language", ["r"]), + # Language("racket", "racket", ["rkt"]), + Language("ruby", "ruby", ["rb"]), + Language("rust", "rust", ["rs"]), + Language("scala", "scala", ["scala"]), + Language("scss", "scss", ["scss"]), + # Language("snippets", "snippets", ["snippets"]), + Language("sql", "sql", ["sql"]), + Language("stata", "stata", ["do", "ado"]), + Language("talon", "talon", ["talon"]), + Language("talonlist", "talon list", ["talon-list"]), + Language("terraform", "terraform", ["tf"]), + Language("tex", ["tech", "lay tech", "latex"], ["tex"]), + Language("typescript", "type script", ["ts"]), + Language("typescriptreact", "type script react", ["tsx"]), + # Language("vba", "vba", ["vba"]), + Language("vimscript", "vim script", ["vim", "vimrc"]), +] + +# Files without specific extensions but are associated with languages +# Maps full filename to language identifiers +code_special_file_map = { + "CMakeLists.txt": "cmake", + "Makefile": "make", + "Dockerfile": "docker", + "meson.build": "meson", + ".bashrc": "bash", + ".zshrc": "zsh", + "PKGBUILD": "pkgbuild", + ".vimrc": "vimscript", + "vimrc": "vimscript", +} diff --git a/core/modes/language_modes.py b/core/modes/language_modes.py index 887e8b2d6e..d2bc734518 100644 --- a/core/modes/language_modes.py +++ b/core/modes/language_modes.py @@ -1,79 +1,6 @@ from talon import Context, Module, actions, app -# Maps language mode names to the extensions that activate them. Only put things -# here which have a supported language mode; that's why there are so many -# commented out entries. TODO: make this a csv file? -language_extensions = { - # 'assembly': 'asm s', - # 'bash': 'bashbook sh', - "batch": "bat", - "c": "c h", - # 'cmake': 'cmake', - # "cplusplus": "cpp hpp", - "csharp": "cs", - "css": "css", - # 'elisp': 'el', - # 'elm': 'elm', - "gdb": "gdb", - "go": "go", - "java": "java", - "javascript": "js", - "javascriptreact": "jsx", - # "json": "json", - "elixir": "ex", - "kotlin": "kt", - "lua": "lua", - "markdown": "md", - # 'perl': 'pl', - "php": "php", - # 'powershell': 'ps1', - "python": "py", - "protobuf": "proto", - "r": "r", - # 'racket': 'rkt', - "ruby": "rb", - "rust": "rs", - "scala": "scala", - "scss": "scss", - # 'snippets': 'snippets', - "sql": "sql", - "stata": "do ado", - "talon": "talon", - "talonlist": "talon-list", - "terraform": "tf", - "tex": "tex", - "typescript": "ts", - "typescriptreact": "tsx", - # 'vba': 'vba', - "vimscript": "vim vimrc", - # html doesn't actually have a language mode, but we do have snippets. - "html": "html", -} - -# Files without specific extensions but are associated with languages -special_file_map = { - "CMakeLists.txt": "cmake", - "Makefile": "make", - "Dockerfile": "docker", - "meson.build": "meson", - ".bashrc": "bash", - ".zshrc": "zsh", - "PKGBUILD": "pkgbuild", - ".vimrc": "vimscript", - "vimrc": "vimscript", -} - -# Override speakable forms for language modes. If not present, a language mode's -# name is used directly. -language_name_overrides = { - "cplusplus": ["see plus plus"], - "csharp": ["see sharp"], - "css": ["c s s"], - "gdb": ["g d b"], - "go": ["go", "go lang", "go language"], - "r": ["are language"], - "tex": ["tech", "lay tech", "latex"], -} +from .code_languages import code_languages, code_special_file_map mod = Module() ctx = Context() @@ -87,21 +14,19 @@ mod.tag("code_language_forced", "This tag is active when a language mode is forced") mod.list("language_mode", desc="Name of a programming language mode.") -ctx.lists["self.language_mode"] = { - name: language - for language in language_extensions - for name in language_name_overrides.get(language, [language]) +# Maps spoken forms to language ids +ctx.lists["user.language_mode"] = { + spoken_form: language.id + for language in code_languages + for spoken_form in language.spoken_forms } -# Maps extension to languages. +# Maps extension to language ids extension_lang_map = { - "." + ext: language - for language, extensions in language_extensions.items() - for ext in extensions.split() + f".{ext}": lang.id for lang in code_languages for ext in lang.extensions } -language_ids = set(language_extensions.keys()) - +language_ids = {lang.id for lang in code_languages} forced_language = "" @@ -109,8 +34,8 @@ class CodeActions: def language(): file_name = actions.win.filename() - if file_name in special_file_map: - return special_file_map[file_name] + if file_name in code_special_file_map: + return code_special_file_map[file_name] file_extension = actions.win.file_ext() return extension_lang_map.get(file_extension, "") @@ -127,7 +52,7 @@ class Actions: def code_set_language_mode(language: str): """Sets the active language mode, and disables extension matching""" global forced_language - assert language in language_extensions + assert language in language_ids forced_language = language # Update tags to force a context refresh. Otherwise `code.language` will not update. # Necessary to first set an empty list otherwise you can't move from one forced language to another. diff --git a/core/snippets/snippets.py b/core/snippets/snippets.py index e04f10ebfa..cb57e29777 100644 --- a/core/snippets/snippets.py +++ b/core/snippets/snippets.py @@ -4,7 +4,7 @@ from talon import Context, Module, actions, app, fs, settings -from ..modes.language_modes import language_ids +from ..modes.code_languages import code_languages from .snippet_types import InsertionSnippet, Snippet, WrapperSnippet from .snippets_parser import create_snippets_from_file @@ -30,10 +30,10 @@ snippets_map = {} # Create a context for each defined language -for lang in language_ids: +for lang in code_languages: ctx = Context() - ctx.matches = f"code.language: {lang}" - context_map[lang] = ctx + ctx.matches = f"code.language: {lang.id}" + context_map[lang.id] = ctx def get_setting_dir(): From d049c53509cbdd4778b438ea631a51e032fb83b5 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 16 Nov 2024 07:40:25 +0100 Subject: [PATCH 28/99] Support leading whitespace in formatters (#1601) Fixes #616 --- core/text/formatters.py | 15 +++++++++++---- test/test_formatters.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/text/formatters.py b/core/text/formatters.py index 361cd58ffc..1e9bd86a4c 100644 --- a/core/text/formatters.py +++ b/core/text/formatters.py @@ -149,7 +149,7 @@ def _title_case_words(self, words: list[str]) -> list[str]: class CapitalizeFormatter(Formatter): def format(self, text: str) -> str: - return re.sub(r"^\S+", lambda m: capitalize_first(m.group()), text) + return re.sub(r"^\s*\S+", lambda m: capitalize_first(m.group()), text) def unformat(self, text: str) -> str: return unformat_upper(text) @@ -159,8 +159,13 @@ class SentenceFormatter(Formatter): def format(self, text: str) -> str: """Capitalize first word if it's already all lower case""" words = [x for x in re.split(r"(\s+)", text) if x] - if words and words[0].islower(): - words[0] = words[0].capitalize() + for i in range(len(words)): + word = words[i] + if word.isspace(): + continue + if word.islower(): + words[i] = word.capitalize() + break return "".join(words) def unformat(self, text: str) -> str: @@ -168,7 +173,9 @@ def unformat(self, text: str) -> str: def capitalize_first(text: str) -> str: - return text[:1].upper() + text[1:] + stripped = text.lstrip() + prefix = text[: len(text) - len(stripped)] + return prefix + stripped[:1].upper() + stripped[1:] def capitalize(text: str) -> str: diff --git a/test/test_formatters.py b/test/test_formatters.py index 4a6603aa30..5fbb13a96c 100644 --- a/test/test_formatters.py +++ b/test/test_formatters.py @@ -26,6 +26,10 @@ def test_capitalize(): assert result == "Hello world" + result = formatters.Actions.formatted_text(" hello world", "CAPITALIZE") + + assert result == " Hello world" + result = formatters.Actions.formatted_text("hEllo wOrld", "CAPITALIZE") assert result == "HEllo wOrld" @@ -37,6 +41,12 @@ def test_capitalize_first_word(): assert result == "Hello world" + result = formatters.Actions.formatted_text( + " hello world", "CAPITALIZE_FIRST_WORD" + ) + + assert result == " Hello world" + result = formatters.Actions.formatted_text( "hEllo wOrld", "CAPITALIZE_FIRST_WORD" ) @@ -50,6 +60,12 @@ def test_capitalize_all_words(): assert result == "Hello World" + result = formatters.Actions.formatted_text( + " hello world", "CAPITALIZE_ALL_WORDS" + ) + + assert result == " Hello World" + result = formatters.Actions.formatted_text( "hEllo wOrld", "CAPITALIZE_ALL_WORDS" ) From abdf0f3e0fa75c8125b2d749680cdc694c10d99e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 16 Nov 2024 07:40:56 +0100 Subject: [PATCH 29/99] Refactor mouse scrolling code (#1594) mouse.py is to large so I have extracted all the code related to mouse growling into a separate file. I also took the liberty to clean up the code an simplify as much as I could. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- plugin/mouse/mouse.py | 292 +++++------------------------------ plugin/mouse/mouse_scroll.py | 215 ++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 253 deletions(-) create mode 100644 plugin/mouse/mouse_scroll.py diff --git a/plugin/mouse/mouse.py b/plugin/mouse/mouse.py index 01313830da..5ee4db412c 100644 --- a/plugin/mouse/mouse.py +++ b/plugin/mouse/mouse.py @@ -1,16 +1,6 @@ -from talon import Context, Module, actions, app, clip, cron, ctrl, imgui, settings, ui +from talon import Context, Module, actions, clip, ctrl, settings, ui from talon_plugins import eye_zoom_mouse -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 -hiss_scroll_up = False - mod = Module() ctx = Context() @@ -36,52 +26,12 @@ default=False, desc="When enabled, pop stops mouse drag", ) -mod.setting( - "mouse_enable_hiss_scroll", - type=bool, - default=False, - desc="Hiss noise scrolls down when enabled", -) mod.setting( "mouse_wake_hides_cursor", type=bool, default=False, desc="When enabled, mouse wake will hide the cursor. mouse_wake enables zoom mouse.", ) -mod.setting( - "mouse_hide_mouse_gui", - type=bool, - default=False, - desc="When enabled, the 'Scroll Mouse' GUI will not be shown.", -) -mod.setting( - "mouse_continuous_scroll_amount", - type=int, - default=80, - desc="The default amount used when scrolling continuously", -) -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)", -) -mod.setting( - "mouse_wheel_horizontal_amount", - type=int, - default=40, - desc="The amount to scroll left/right", -) - -continuous_scroll_mode = "" - - -@imgui.open(x=700, y=0) -def gui_wheel(gui: imgui.GUI): - gui.text(f"Scroll mode: {continuous_scroll_mode}") - gui.line() - if gui.button("Wheel Stop [stop scrolling]"): - actions.user.mouse_scroll_stop() @mod.action_class @@ -104,19 +54,23 @@ def mouse_drag(button: int): actions.user.mouse_drag_end() # Start drag - ctrl.mouse_click(button=button, down=True) + actions.mouse_drag(button) - def mouse_drag_end(): + def mouse_drag_end() -> bool: """Releases any held mouse buttons""" - for button in ctrl.mouse_buttons_down(): - ctrl.mouse_click(button=button, up=True) + buttons = ctrl.mouse_buttons_down() + if buttons: + for button in buttons: + actions.mouse_release(button) + return True + return False def mouse_drag_toggle(button: int): """If the button is held down, release the button, else start dragging""" - if button in list(ctrl.mouse_buttons_down()): - ctrl.mouse_click(button=button, up=True) + if button in ctrl.mouse_buttons_down(): + actions.mouse_release(button) else: - actions.user.mouse_drag(button=button) + actions.mouse_drag(button) def mouse_sleep(): """Disables control mouse, zoom mouse, and re-enables cursor""" @@ -125,80 +79,9 @@ def mouse_sleep(): actions.tracking.control1_toggle(False) actions.user.mouse_cursor_show() - stop_scroll() + actions.user.mouse_scroll_stop() actions.user.mouse_drag_end() - def mouse_scroll_down(amount: float = 1): - """Scrolls down""" - mouse_scroll(amount * settings.get("user.mouse_wheel_down_amount"))() - - def mouse_scroll_down_continuous(): - """Scrolls down continuously""" - global continuous_scroll_mode - continuous_scroll_mode = "scroll down continuous" - mouse_scroll(settings.get("user.mouse_continuous_scroll_amount"))() - - if scroll_job is None: - start_scroll() - - if not settings.get("user.mouse_hide_mouse_gui"): - gui_wheel.show() - - def mouse_scroll_up(amount: float = 1): - """Scrolls up""" - mouse_scroll(-amount * settings.get("user.mouse_wheel_down_amount"))() - - def mouse_scroll_up_continuous(): - """Scrolls up continuously""" - global continuous_scroll_mode - continuous_scroll_mode = "scroll up continuous" - mouse_scroll(-settings.get("user.mouse_continuous_scroll_amount"))() - - if scroll_job is None: - start_scroll() - if not settings.get("user.mouse_hide_mouse_gui"): - gui_wheel.show() - - def mouse_scroll_left(amount: float = 1): - """Scrolls left""" - actions.mouse_scroll( - 0, -amount * settings.get("user.mouse_wheel_horizontal_amount") - ) - - def mouse_scroll_right(amount: float = 1): - """Scrolls right""" - actions.mouse_scroll( - 0, amount * settings.get("user.mouse_wheel_horizontal_amount") - ) - - def mouse_scroll_stop(): - """Stops scrolling""" - stop_scroll() - - def mouse_gaze_scroll(): - """Starts gaze scroll""" - global continuous_scroll_mode - # this calls stop_scroll, which resets continuous_scroll_mode - start_cursor_scrolling() - - continuous_scroll_mode = "gaze scroll" - - if not settings.get("user.mouse_hide_mouse_gui"): - gui_wheel.show() - - # enable 'control mouse' if eye tracker is present and not enabled already - global control_mouse_forced - if not actions.tracking.control_enabled(): - actions.tracking.control_toggle(True) - control_mouse_forced = True - - def mouse_gaze_scroll_toggle(): - """If not scrolling, start gaze scroll, else stop scrolling.""" - if continuous_scroll_mode == "": - actions.user.mouse_gaze_scroll() - else: - actions.user.mouse_scroll_stop() - def copy_mouse_position(): """Copy the current mouse position coordinates""" position = ctrl.mouse_pos() @@ -209,138 +92,41 @@ def mouse_move_center_active_window(): rect = ui.active_window().rect ctrl.mouse_move(rect.left + (rect.width / 2), rect.top + (rect.height / 2)) - def hiss_scroll_up(): - """Change mouse hiss scroll direction to up""" - global hiss_scroll_up - hiss_scroll_up = True - - def hiss_scroll_down(): - """Change mouse hiss scroll direction to down""" - global hiss_scroll_up - hiss_scroll_up = False - @ctx.action_class("user") class UserActions: def noise_trigger_pop(): - if ( - settings.get("user.mouse_enable_pop_stops_drag") - and ctrl.mouse_buttons_down() - ): - actions.user.mouse_drag_end() - elif settings.get("user.mouse_enable_pop_stops_scroll") and ( - gaze_job or scroll_job - ): - # Allow pop to stop scroll - stop_scroll() - else: - # Otherwise respect the mouse_enable_pop_click setting - setting_val = settings.get("user.mouse_enable_pop_click") - - is_using_eye_tracker = ( - actions.tracking.control_zoom_enabled() - or actions.tracking.control_enabled() - or actions.tracking.control1_enabled() - ) - should_click = ( - setting_val == 2 and not actions.tracking.control_zoom_enabled() - ) or ( - setting_val == 1 - and is_using_eye_tracker - and not actions.tracking.control_zoom_enabled() - ) - if should_click: - ctrl.mouse_click(button=0, hold=16000) - - def noise_trigger_hiss(active: bool): - if settings.get("user.mouse_enable_hiss_scroll"): - if active: - if hiss_scroll_up: - actions.user.mouse_scroll_up_continuous() - else: - actions.user.mouse_scroll_down_continuous() - else: - actions.user.mouse_scroll_stop() - - -def mouse_scroll(amount): - def scroll(): - global scroll_amount - if continuous_scroll_mode: - if (scroll_amount >= 0) == (amount >= 0): - scroll_amount += amount - else: - scroll_amount = amount - actions.mouse_scroll(y=int(amount)) - - return scroll + dont_click = False + # Allow pop to stop drag + if settings.get("user.mouse_enable_pop_stops_drag"): + if actions.user.mouse_drag_end(): + dont_click = True -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): - actions.mouse_scroll(by_lines=False, y=int(scroll_amount / 10)) + # Allow pop to stop scroll + if settings.get("user.mouse_enable_pop_stops_scroll"): + if actions.user.mouse_scroll_stop(): + dont_click = True - -def start_scroll(): - global scroll_job - scroll_job = cron.interval("60ms", scroll_continuous_helper) - - -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!") + if dont_click: 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}") + # Otherwise respect the mouse_enable_pop_click setting + setting_val = settings.get("user.mouse_enable_pop_click") + is_using_eye_tracker = ( + actions.tracking.control_zoom_enabled() + or actions.tracking.control_enabled() + or actions.tracking.control1_enabled() + ) -def stop_scroll(): - global scroll_amount, scroll_job, gaze_job, continuous_scroll_mode - 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: - actions.tracking.control_toggle(False) - control_mouse_forced = False - - scroll_job = None - gaze_job = None - gui_wheel.hide() - - continuous_scroll_mode = "" - + should_click = ( + setting_val == 2 and not actions.tracking.control_zoom_enabled() + ) or ( + setting_val == 1 + and is_using_eye_tracker + and not actions.tracking.control_zoom_enabled() + ) -def start_cursor_scrolling(): - global scroll_job, gaze_job - stop_scroll() - gaze_job = cron.interval("60ms", gaze_scroll) + if should_click: + ctrl.mouse_click(button=0, hold=16000) diff --git a/plugin/mouse/mouse_scroll.py b/plugin/mouse/mouse_scroll.py new file mode 100644 index 0000000000..0c68311e60 --- /dev/null +++ b/plugin/mouse/mouse_scroll.py @@ -0,0 +1,215 @@ +from typing import Literal + +from talon import Context, Module, actions, app, cron, ctrl, imgui, settings, ui +from talon_plugins import eye_zoom_mouse + +continuous_scroll_mode = "" +scroll_job = None +gaze_job = None +scroll_dir: Literal[-1, 1] = 1 +hiss_scroll_up = False +control_mouse_forced = False + +mod = Module() +ctx = Context() + + +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)", +) +mod.setting( + "mouse_wheel_horizontal_amount", + type=int, + default=40, + desc="The amount to scroll left/right", +) +mod.setting( + "mouse_continuous_scroll_amount", + type=int, + default=80, + desc="The default amount used when scrolling continuously", +) +mod.setting( + "mouse_enable_hiss_scroll", + type=bool, + default=False, + desc="Hiss noise scrolls down when enabled", +) +mod.setting( + "mouse_hide_mouse_gui", + type=bool, + default=False, + desc="When enabled, the 'Scroll Mouse' GUI will not be shown.", +) + + +@imgui.open(x=700, y=0) +def gui_wheel(gui: imgui.GUI): + gui.text(f"Scroll mode: {continuous_scroll_mode}") + gui.line() + if gui.button("Wheel Stop [stop scrolling]"): + actions.user.mouse_scroll_stop() + + +@mod.action_class +class Actions: + def mouse_scroll_up(amount: float = 1): + """Scrolls up""" + y = amount * settings.get("user.mouse_wheel_down_amount") + actions.mouse_scroll(-y) + + def mouse_scroll_down(amount: float = 1): + """Scrolls down""" + y = amount * settings.get("user.mouse_wheel_down_amount") + actions.mouse_scroll(y) + + def mouse_scroll_left(amount: float = 1): + """Scrolls left""" + x = amount * settings.get("user.mouse_wheel_horizontal_amount") + actions.mouse_scroll(0, -x) + + def mouse_scroll_right(amount: float = 1): + """Scrolls right""" + x = amount * settings.get("user.mouse_wheel_horizontal_amount") + actions.mouse_scroll(0, x) + + def mouse_scroll_up_continuous(): + """Scrolls up continuously""" + mouse_scroll_continuous(-1) + + def mouse_scroll_down_continuous(): + """Scrolls down continuously""" + mouse_scroll_continuous(1) + + def mouse_gaze_scroll(): + """Starts gaze scroll""" + global gaze_job, continuous_scroll_mode, control_mouse_forced + + if eye_zoom_mouse.zoom_mouse.state != eye_zoom_mouse.STATE_IDLE: + return + + continuous_scroll_mode = "gaze scroll" + gaze_job = cron.interval("16ms", scroll_gaze_helper) + + if not settings.get("user.mouse_hide_mouse_gui"): + gui_wheel.show() + + # enable 'control mouse' if eye tracker is present and not enabled already + if not actions.tracking.control_enabled(): + actions.tracking.control_toggle(True) + control_mouse_forced = True + + def mouse_gaze_scroll_toggle(): + """If not scrolling, start gaze scroll, else stop scrolling.""" + if continuous_scroll_mode == "": + actions.user.mouse_gaze_scroll() + else: + actions.user.mouse_scroll_stop() + + def mouse_scroll_stop() -> bool: + """Stops scrolling""" + global scroll_job, gaze_job, continuous_scroll_mode, control_mouse_forced + + continuous_scroll_mode = "" + return_value = False + + if scroll_job: + cron.cancel(scroll_job) + scroll_job = None + return_value = True + + if gaze_job: + cron.cancel(gaze_job) + gaze_job = None + return_value = True + + if control_mouse_forced: + actions.tracking.control_toggle(False) + control_mouse_forced = False + + gui_wheel.hide() + + return return_value + + def hiss_scroll_up(): + """Change mouse hiss scroll direction to up""" + global hiss_scroll_up + hiss_scroll_up = True + + def hiss_scroll_down(): + """Change mouse hiss scroll direction to down""" + global hiss_scroll_up + hiss_scroll_up = False + + +@ctx.action_class("user") +class UserActions: + def noise_trigger_hiss(active: bool): + if settings.get("user.mouse_enable_hiss_scroll"): + if active: + if hiss_scroll_up: + actions.user.mouse_scroll_up_continuous() + else: + actions.user.mouse_scroll_down_continuous() + else: + actions.user.mouse_scroll_stop() + + +def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): + global scroll_job, scroll_dir, continuous_scroll_mode + + if eye_zoom_mouse.zoom_mouse.state != eye_zoom_mouse.STATE_IDLE: + return + + if scroll_job: + # Issuing a scroll in the same direction aborts scrolling + if scroll_dir == new_scroll_dir: + cron.cancel(scroll_job) + scroll_job = None + continuous_scroll_mode = "" + else: + scroll_dir = new_scroll_dir + else: + scroll_dir = new_scroll_dir + continuous_scroll_mode = "scroll down continuous" + scroll_continuous_helper() + scroll_job = cron.interval("16ms", scroll_continuous_helper) + + if not settings.get("user.mouse_hide_mouse_gui"): + gui_wheel.show() + + +def scroll_continuous_helper(): + scroll_amount = settings.get("user.mouse_continuous_scroll_amount") + y = scroll_amount * scroll_dir / 10 + actions.mouse_scroll(y) + + +def scroll_gaze_helper(): + x, y = ctrl.mouse_pos() + + # The window containing the mouse + window = get_window_containing(x, y) + + if window is None: + return + + rect = window.rect + midpoint = rect.center.y + amount = ((y - midpoint) / (rect.height / 10)) ** 3 + actions.mouse_scroll(amount) + + +def get_window_containing(x: float, y: float): + # 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): + return ui.active_window() + + for window in ui.windows(): + if window.rect.contains(x, y): + return window + + return None From 4c0b2ba9ae13ae28b32b44691e7079d6bb92b015 Mon Sep 17 00:00:00 2001 From: Aaron Adams Date: Sat, 16 Nov 2024 23:49:41 +0800 Subject: [PATCH 30/99] Avoid hardcoded xdg-open path and resolve incoming paths (#1531) This fixes using xdg-open while on nixos and also resolves paths passed in, so if someone uses ~/ or similar in their path it'll work. Co-authored-by: Jeff Knaus --- core/edit_text_file.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/edit_text_file.py b/core/edit_text_file.py index cab76c9635..a064230884 100644 --- a/core/edit_text_file.py +++ b/core/edit_text_file.py @@ -1,5 +1,6 @@ import os import subprocess +from pathlib import Path from talon import Context, Module, app @@ -72,7 +73,9 @@ def edit_text_file(path): class MacActions: def edit_text_file(path): # -t means try to open in a text editor. - open_with_subprocess(path, ["/usr/bin/open", "-t", path]) + open_with_subprocess( + path, ["/usr/bin/open", "-t", Path(path).expanduser().resolve()] + ) @linuxctx.action_class("self") @@ -81,7 +84,11 @@ def edit_text_file(path): # we use xdg-open for this even though it might not open a text # editor. we could use $EDITOR, but that might be something that # requires a terminal (eg nano, vi). - open_with_subprocess(path, ["/usr/bin/xdg-open", path]) + try: + open_with_subprocess(path, ["xdg-open", Path(path).expanduser().resolve()]) + except FileNotFoundError: + app.notify(f"xdg-open missing. Could not open file for editing: {path}") + raise # Helper for linux and mac. From 924c0ee96aa229ff798e050bb484bf053a423cac Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 16 Nov 2024 19:57:09 +0100 Subject: [PATCH 31/99] Added setting for scroll acceleration (#1604) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- plugin/mouse/mouse_scroll.py | 21 +++++++++++++++++++-- settings.talon | 3 +++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/plugin/mouse/mouse_scroll.py b/plugin/mouse/mouse_scroll.py index 0c68311e60..856445afea 100644 --- a/plugin/mouse/mouse_scroll.py +++ b/plugin/mouse/mouse_scroll.py @@ -1,3 +1,4 @@ +import time from typing import Literal from talon import Context, Module, actions, app, cron, ctrl, imgui, settings, ui @@ -7,6 +8,7 @@ scroll_job = None gaze_job = None scroll_dir: Literal[-1, 1] = 1 +scroll_start_ts: float = 0 hiss_scroll_up = False control_mouse_forced = False @@ -32,6 +34,12 @@ default=80, desc="The default amount used when scrolling continuously", ) +mod.setting( + "mouse_continuous_scroll_acceleration", + type=float, + default=1, + desc="The maximum (linear) acceleration factor when scrolling continuously. 1=constant speed/no acceleration", +) mod.setting( "mouse_enable_hiss_scroll", type=bool, @@ -159,7 +167,7 @@ def noise_trigger_hiss(active: bool): def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): - global scroll_job, scroll_dir, continuous_scroll_mode + global scroll_job, scroll_dir, scroll_start_ts, continuous_scroll_mode if eye_zoom_mouse.zoom_mouse.state != eye_zoom_mouse.STATE_IDLE: return @@ -170,10 +178,13 @@ def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): cron.cancel(scroll_job) scroll_job = None continuous_scroll_mode = "" + # Issuing a scroll in the reverse direction resets acceleration else: scroll_dir = new_scroll_dir + scroll_start_ts = time.perf_counter() else: scroll_dir = new_scroll_dir + scroll_start_ts = time.perf_counter() continuous_scroll_mode = "scroll down continuous" scroll_continuous_helper() scroll_job = cron.interval("16ms", scroll_continuous_helper) @@ -184,7 +195,13 @@ def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): def scroll_continuous_helper(): scroll_amount = settings.get("user.mouse_continuous_scroll_amount") - y = scroll_amount * scroll_dir / 10 + acceleration_setting = settings.get("user.mouse_continuous_scroll_acceleration") + acceleration_speed = ( + 1 + min((time.perf_counter() - scroll_start_ts) / 0.5, acceleration_setting - 1) + if acceleration_setting > 1 + else 1 + ) + y = scroll_amount * acceleration_speed * scroll_dir / 10 actions.mouse_scroll(y) diff --git a/settings.talon b/settings.talon index 443252eb3d..3a8fba3ce4 100644 --- a/settings.talon +++ b/settings.talon @@ -24,6 +24,9 @@ settings(): # Set the scroll amount for continuous scroll/gaze scroll user.mouse_continuous_scroll_amount = 80 + # Set the maximum acceleration factor when scrolling continuously. 1=constant speed/no acceleration. + user.mouse_continuous_scroll_acceleration = 1 + # If `true`, stop continuous scroll/gaze scroll with a pop user.mouse_enable_pop_stops_scroll = true From bb240ba1cf958bd3e2c7791e5a57d09d205adc81 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 16 Nov 2024 20:13:33 +0100 Subject: [PATCH 32/99] Additional mouse clean up (#1603) Co-authored-by: Phil Cohen --- plugin/mouse/mouse.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/mouse/mouse.py b/plugin/mouse/mouse.py index 5ee4db412c..9e63cdf61c 100644 --- a/plugin/mouse/mouse.py +++ b/plugin/mouse/mouse.py @@ -1,4 +1,4 @@ -from talon import Context, Module, actions, clip, ctrl, settings, ui +from talon import Context, Module, actions, ctrl, settings, ui from talon_plugins import eye_zoom_mouse mod = Module() @@ -84,13 +84,13 @@ def mouse_sleep(): def copy_mouse_position(): """Copy the current mouse position coordinates""" - position = ctrl.mouse_pos() - clip.set_text(repr(position)) + x, y = actions.mouse_x(), actions.mouse_y() + actions.clip.set_text(f"{x}, {y}") def mouse_move_center_active_window(): - """move the mouse cursor to the center of the currently 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)) + actions.mouse_move(rect.center.x, rect.center.y) @ctx.action_class("user") From 5286e2b8b30ca52ad2f2ccd242926dbad259edda Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 23 Nov 2024 18:40:27 +0100 Subject: [PATCH 33/99] Remove deprecated insert cursor action (#1611) --- core/edit/insert_between.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/core/edit/insert_between.py b/core/edit/insert_between.py index ba957cde74..75885092fb 100644 --- a/core/edit/insert_between.py +++ b/core/edit/insert_between.py @@ -1,4 +1,4 @@ -from talon import Module, actions, app +from talon import Module, actions mod = Module() @@ -10,14 +10,3 @@ def insert_between(before: str, after: str): actions.insert(before + after) for _ in after: actions.edit.left() - - # This is deprecated, please use insert_between instead. - def insert_cursor(text: str): - """Insert a string. Leave the cursor wherever [|] is in the text""" - if "[|]" in text: - actions.user.insert_between(*text.split("[|]", 1)) - else: - actions.insert(text) - app.notify( - "insert_cursor is deprecated, please update your code to use insert_between" - ) From c2365bb41365c73355ec2bbc2af8c4c1610e2dc2 Mon Sep 17 00:00:00 2001 From: Jeff Knaus Date: Sat, 23 Nov 2024 20:10:56 -0700 Subject: [PATCH 34/99] Reimplement #636 (#1602) Reimplement https://github.com/talonhub/community/pull/636 Notes/questions: - Adds abbreviation to word, as in the original. I'm not entirely sure this is desirable? - Since no maintainers use dragon, adds overrides for dragon to omit abbreviations from various captures per previous discussion. This can be tested with wav2letter by changing the context to not speech.engine: dragon - Is "brief" a sufficient prefix for this? --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- core/abbreviate/abbreviate.py | 5 ++++ core/text/text_and_dictation.py | 43 +++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/core/abbreviate/abbreviate.py b/core/abbreviate/abbreviate.py index cb43d5c192..388744fee6 100644 --- a/core/abbreviate/abbreviate.py +++ b/core/abbreviate/abbreviate.py @@ -449,6 +449,11 @@ } +@mod.capture(rule="brief {user.abbreviation}") +def abbreviation(m) -> str: + return m.abbreviation + + @track_csv_list( "abbreviations.csv", headers=("Abbreviation", "Spoken Form"), default=abbreviations ) diff --git a/core/text/text_and_dictation.py b/core/text/text_and_dictation.py index b60302bf2e..c4e79693a0 100644 --- a/core/text/text_and_dictation.py +++ b/core/text/text_and_dictation.py @@ -27,6 +27,11 @@ ) ctx = Context() +ctx_dragon = Context() +ctx_dragon.matches = r""" +speech.engine: dragon +""" + # Maps spoken forms to DictationFormat method names (see DictationFormat below). ctx.lists["user.prose_modifiers"] = { "cap": "cap", @@ -143,12 +148,14 @@ def prose_time(m) -> str: return str(m) -@mod.capture(rule="({user.vocabulary} | )") +@mod.capture(rule="({user.vocabulary} | | )") def word(m) -> str: """A single word, including user-defined vocabulary.""" - try: + if hasattr(m, "vocabulary"): return m.vocabulary - except AttributeError: + elif hasattr(m, "abbreviation"): + return m.abbreviation + else: return " ".join( actions.dictate.replace_words(actions.dictate.parse_words(m.word)) ) @@ -161,7 +168,7 @@ def text(m) -> str: @mod.capture( - rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | )+" + rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | | )+" ) def prose(m) -> str: """Mixed words and punctuation, auto-spaced & capitalized.""" @@ -170,13 +177,39 @@ def prose(m) -> str: @mod.capture( - rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | )+" + rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | )+" ) def raw_prose(m) -> str: """Mixed words and punctuation, auto-spaced & capitalized, without quote straightening and commands (for use in dictation mode).""" return apply_formatting(m) +# For dragon, omit support for abbreviations +@ctx_dragon.capture("user.text", rule="({user.vocabulary} | )+") +def text_dragon(m) -> str: + """A sequence of words, including user-defined vocabulary.""" + return format_phrase(m) + + +@ctx_dragon.capture( + "user.prose", + rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | )+", +) +def prose_dragon(m) -> str: + """Mixed words and punctuation, auto-spaced & capitalized.""" + # Straighten curly quotes that were introduced to obtain proper spacing. + return apply_formatting(m).replace("“", '"').replace("”", '"') + + +@ctx_dragon.capture( + "user.raw_prose", + rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | )+", +) +def raw_prose_dragon(m) -> str: + """Mixed words and punctuation, auto-spaced & capitalized, without quote straightening and commands (for use in dictation mode).""" + return apply_formatting(m) + + # ---------- FORMATTING ---------- # def format_phrase(m): words = capture_to_words(m) From f27cb0192cedfe25468aa68f175cbc8b84db3f6c Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 24 Nov 2024 06:31:58 +0100 Subject: [PATCH 35/99] Make numbers more consistent (#1592) Makes numbers more consistent between prose formatters and the global number command. Increase consistency and additional delimiters for different locales Fixes #1245 --------- Co-authored-by: Jeff Knaus --- core/numbers/numbers.py | 50 +++++++++++++++++++++------ core/numbers/numbers.talon | 2 -- core/numbers/numbers_prefixed.talon | 1 + core/numbers/numbers_unprefixed.talon | 3 +- core/text/text_and_dictation.py | 26 ++------------ 5 files changed, 43 insertions(+), 39 deletions(-) delete mode 100644 core/numbers/numbers.talon create mode 100644 core/numbers/numbers_prefixed.talon diff --git a/core/numbers/numbers.py b/core/numbers/numbers.py index e3e6c32487..787ec79868 100644 --- a/core/numbers/numbers.py +++ b/core/numbers/numbers.py @@ -243,22 +243,50 @@ def number_string(m) -> str: return parse_number(list(m)) -@mod.capture(rule=" ((point | dot) )+") -def number_decimal_string(m) -> str: - """Parses a decimal number phrase, returning that number as a string.""" - return ".".join(m.number_string_list) - - @ctx.capture("number", rule="") def number(m) -> int: """Parses a number phrase, returning it as an integer.""" return int(m.number_string) -@ctx.capture("number_signed", rule=f"[negative|minus] ") -def number_signed(m): - number = m[-1] - return -number if (m[0] in ["negative", "minus"]) else number +@mod.capture(rule="[negative | minus] ") +def number_signed_string(m) -> str: + """Parses a (possibly negative) number phrase, returning that number as a string.""" + number = m.number_string + return f"-{number}" if (m[0] in ["negative", "minus"]) else number + + +@ctx.capture("number_signed", rule="") +def number_signed(m) -> int: + """Parses a (possibly negative) number phrase, returning that number as a integer.""" + return int(m.number_signed_string) + + +@mod.capture(rule=" ((dot | point) )+") +def number_prose_with_dot(m) -> str: + return ".".join(m.number_string_list) + + +@mod.capture(rule=" (comma )+") +def number_prose_with_comma(m) -> str: + return ",".join(m.number_string_list) + + +@mod.capture(rule=" (colon )+") +def number_prose_with_colon(m) -> str: + return ":".join(m.number_string_list) + + +@mod.capture( + rule=" | | | " +) +def number_prose_unprefixed(m) -> str: + return m[0] + + +@mod.capture(rule="(numb | numeral) ") +def number_prose_prefixed(m) -> str: + return m.number_prose_unprefixed @ctx.capture("number_small", rule="{user.number_small}") @@ -266,7 +294,7 @@ def number_small(m) -> int: return int(m.number_small) -@mod.capture(rule=f"[negative|minus] ") +@mod.capture(rule="[negative | minus] ") def number_signed_small(m) -> int: """Parses an integer between -99 and 99.""" number = m[-1] diff --git a/core/numbers/numbers.talon b/core/numbers/numbers.talon deleted file mode 100644 index d3c669b8e0..0000000000 --- a/core/numbers/numbers.talon +++ /dev/null @@ -1,2 +0,0 @@ -numb : "{number_string}" -numb : "{number_decimal_string}" diff --git a/core/numbers/numbers_prefixed.talon b/core/numbers/numbers_prefixed.talon new file mode 100644 index 0000000000..6dba7b3847 --- /dev/null +++ b/core/numbers/numbers_prefixed.talon @@ -0,0 +1 @@ +: "{number_prose_prefixed}" diff --git a/core/numbers/numbers_unprefixed.talon b/core/numbers/numbers_unprefixed.talon index 993a625b3e..f28957b42f 100644 --- a/core/numbers/numbers_unprefixed.talon +++ b/core/numbers/numbers_unprefixed.talon @@ -1,5 +1,4 @@ tag: user.unprefixed_numbers - -: "{number_string}" -: "{number_decimal_string}" +: "{number_prose_unprefixed}" diff --git a/core/text/text_and_dictation.py b/core/text/text_and_dictation.py index c4e79693a0..37909ad719 100644 --- a/core/text/text_and_dictation.py +++ b/core/text/text_and_dictation.py @@ -76,28 +76,6 @@ def prose_modifier(m) -> Callable: return getattr(DictationFormat, m.prose_modifiers) -@mod.capture(rule="(numb | numeral) ") -def prose_simple_number(m) -> str: - return m.number_string - - -@mod.capture(rule="(numb | numeral) (dot | point) ") -def prose_number_with_dot(m) -> str: - return m.number_string + "." + m.digit_string - - -@mod.capture(rule="(numb | numeral) colon ") -def prose_number_with_colon(m) -> str: - return m.number_string_1 + ":" + m.number_string_2 - - -@mod.capture( - rule=" | | " -) -def prose_number(m) -> str: - return str(m) - - @mod.capture( rule=" [(dot | point) ] percent [sign|sine]" ) @@ -168,7 +146,7 @@ def text(m) -> str: @mod.capture( - rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | | )+" + rule="({user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | | | )+" ) def prose(m) -> str: """Mixed words and punctuation, auto-spaced & capitalized.""" @@ -177,7 +155,7 @@ def prose(m) -> str: @mod.capture( - rule="( | {user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | )+" + rule="({user.vocabulary} | {user.punctuation} | {user.prose_snippets} | | | | | | )+" ) def raw_prose(m) -> str: """Mixed words and punctuation, auto-spaced & capitalized, without quote straightening and commands (for use in dictation mode).""" From b85932a6aa2372f553ff7be988aeae6d7127dcaa Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 24 Nov 2024 15:03:20 +0100 Subject: [PATCH 36/99] Remove unused modes (#1620) None of these modes are used in community and even if they were they should have been tags instead. Fixes #603 --- core/modes/modes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/modes/modes.py b/core/modes/modes.py index 60771afa30..d746dee11a 100644 --- a/core/modes/modes.py +++ b/core/modes/modes.py @@ -5,9 +5,6 @@ ctx_awake = Context() modes = { - "admin": "enable extra administration commands terminal (docker, etc)", - "debug": "a way to force debugger 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", } From 785d410cb9d0af64549bcb5e602511301399a45a Mon Sep 17 00:00:00 2001 From: Jacob Egner Date: Tue, 26 Nov 2024 19:38:05 -0600 Subject: [PATCH 37/99] remove redundant `proud ` command (#1623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove `proud ` command due to redundancy from `{user.word…_formatter} ` command --- core/text/text.talon | 1 - 1 file changed, 1 deletion(-) diff --git a/core/text/text.talon b/core/text/text.talon index cea870f145..d8cc39f306 100644 --- a/core/text/text.talon +++ b/core/text/text.talon @@ -19,7 +19,6 @@ phrase {user.phrase_ender}: word : user.add_phrase_to_history(word) insert(word) -proud : user.insert_formatted(word, "CAPITALIZE_FIRST_WORD") recent list: user.toggle_phrase_history() recent close: user.phrase_history_hide() recent repeat : From fccb3450a0b3324810baa0a4725f41a559b4ce35 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Wed, 27 Nov 2024 01:54:42 +0000 Subject: [PATCH 38/99] Update powershell to add option not to update the title (#1622) Currently when using powershell the title is forced to be updated after every directory change automatically This is updated by inserting a command into powershell which updates the title, which clutters your terminal quite quickly. The better solution is that the user updates their powershell prompt function so that this is done automatically, and then turn off the forced title refresh. This pull request adds the talon setting to turn off the automatic forced title refresh, and adds a readme on how to do this. Additionally I've changed open file to actually automatically open the file. If the user just wants to insert the string of the file they can already use select file command. I'm not sure if it's best to add the powershell specific readme file or add the documentation in the project readme, or elsewhere, so that can move. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- apps/powershell/README.md | 16 ++++++++++++++++ apps/powershell/powershell_win.py | 19 ++++++++++++++----- apps/powershell/powershell_win.talon | 2 ++ 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 apps/powershell/README.md diff --git a/apps/powershell/README.md b/apps/powershell/README.md new file mode 100644 index 0000000000..5baaa7013b --- /dev/null +++ b/apps/powershell/README.md @@ -0,0 +1,16 @@ +# Powershell + +By default the windows powershell does not display the current address in the title. To fix this add a prompt function to the powershell profile file. If the powershell profile file doesn't exist then create it. + +To find the profile file run `$profile` in windows powershell. + +The prompt function to add: + +``` +function prompt { + $Host.UI.RawUI.WindowTitle = 'Windows PowerShell: ' + $(get-location) + "$pwd" + '> ' +} +``` + +Then you can set the setting `user.powershell_always_refresh_title` to false. diff --git a/apps/powershell/powershell_win.py b/apps/powershell/powershell_win.py index a592e58a4b..81f4fbe1ef 100644 --- a/apps/powershell/powershell_win.py +++ b/apps/powershell/powershell_win.py @@ -1,4 +1,4 @@ -from talon import Context, Module, actions, ui +from talon import Context, Module, actions, settings, ui ctx = Context() mod = Module() @@ -11,6 +11,13 @@ directories_to_remap = {} directories_to_exclude = {} +mod.setting( + "powershell_always_refresh_title", + type=bool, + default=True, + desc="If the title is refreshed after every directory move", +) + @ctx.action_class("edit") class EditActions: @@ -29,7 +36,8 @@ def file_manager_refresh_title(): def file_manager_open_parent(): actions.insert("cd ..") actions.key("enter") - actions.user.file_manager_refresh_title() + if settings.get("user.powershell_always_refresh_title"): + actions.user.file_manager_refresh_title() def file_manager_current_path(): path = ui.active_window().title @@ -46,7 +54,8 @@ def file_manager_open_directory(path: str): """opens the directory that's already visible in the view""" actions.insert(f'cd "{path}"') actions.key("enter") - actions.user.file_manager_refresh_title() + if settings.get("user.powershell_always_refresh_title"): + actions.user.file_manager_refresh_title() def file_manager_select_directory(path: str): """selects the directory""" @@ -58,8 +67,8 @@ def file_manager_new_folder(name: str): def file_manager_open_file(path: str): """opens the file""" - actions.insert(path) - # actions.key("enter") + actions.insert(f'./"{path}"') + actions.key("enter") def file_manager_select_file(path: str): """selects the file""" diff --git a/apps/powershell/powershell_win.talon b/apps/powershell/powershell_win.talon index d3e92f9b42..5bd1742cc6 100644 --- a/apps/powershell/powershell_win.talon +++ b/apps/powershell/powershell_win.talon @@ -14,3 +14,5 @@ tag(): user.generic_windows_shell tag(): user.git tag(): user.anaconda # tag(): user.kubectl + +tag(): user.file_manager From 0aa079923488afdbe4cd363a13ab0090ce43d469 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Thu, 28 Nov 2024 20:00:53 +0100 Subject: [PATCH 39/99] Added action to get active Talon list (#1616) Fixes #1613 --- core/help/help.py | 3 +-- core/system_paths.py | 4 ++-- lang/tags/functions_common.py | 20 +++++++++++++------- lang/tags/libraries_gui.py | 11 ++++++++--- plugin/talon_helpers/talon_helpers.py | 5 +++++ 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/core/help/help.py b/core/help/help.py index b77920b1a1..07a78edc47 100644 --- a/core/help/help.py +++ b/core/help/help.py @@ -2,7 +2,6 @@ import math import re from collections import defaultdict -from functools import cmp_to_key from itertools import islice from typing import Any, Iterable, Tuple @@ -546,7 +545,7 @@ def draw_list_commands(gui: imgui.GUI): global total_page_count global selected_context_page - talon_list = registry.lists[selected_list][-1] + talon_list = actions.user.talon_get_active_registry_list(selected_list) # numpages = math.ceil(len(talon_list) / SIZE) pages_list = [] diff --git a/core/system_paths.py b/core/system_paths.py index 8cf1ae6db2..bdcb15c80c 100644 --- a/core/system_paths.py +++ b/core/system_paths.py @@ -7,7 +7,7 @@ from pathlib import Path -from talon import Context, Module, actions, app, registry +from talon import Module, actions, app mod = Module() mod.list("system_paths", desc="List of system paths") @@ -15,7 +15,7 @@ def on_ready(): # If user.system_paths defined otherwise, don't generate a file - if registry.lists["user.system_paths"][0]: + if actions.user.talon_get_active_registry_list("user.system_paths"): return hostname = actions.user.talon_get_hostname() diff --git a/lang/tags/functions_common.py b/lang/tags/functions_common.py index d581ee7525..19ce6c90b3 100644 --- a/lang/tags/functions_common.py +++ b/lang/tags/functions_common.py @@ -37,8 +37,11 @@ def code_toggle_functions(): 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): + talon_list = actions.user.talon_get_active_registry_list( + "user.code_common_function" + ) actions.user.code_insert_function( - registry.lists["user.code_common_function"][0][function_list[number]], + talon_list[function_list[number]], selection, ) @@ -52,7 +55,10 @@ def code_insert_function(text: str, selection: str): def update_function_list_and_freeze(): global function_list if "user.code_common_function" in registry.lists: - function_list = sorted(registry.lists["user.code_common_function"][0].keys()) + talon_list = actions.user.talon_get_active_registry_list( + "user.code_common_function" + ) + function_list = sorted(talon_list.keys()) else: function_list = [] @@ -65,12 +71,12 @@ 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_common_function"][0]: - gui.text( - f"{i}. {entry}: {registry.lists['user.code_common_function'][0][entry]}" - ) + talon_list = actions.user.talon_get_active_registry_list( + "user.code_common_function" + ) + if entry in talon_list: + gui.text(f"{i}. {entry}: {talon_list[entry]}") gui.spacer() if gui.button("Toggle funk (close window)"): diff --git a/lang/tags/libraries_gui.py b/lang/tags/libraries_gui.py index 797ab6182e..9c406a16ce 100644 --- a/lang/tags/libraries_gui.py +++ b/lang/tags/libraries_gui.py @@ -36,8 +36,11 @@ def code_toggle_libraries(): 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): + talon_list = actions.user.talon_get_active_registry_list( + "user.code_libraries" + ) actions.user.code_insert_library( - registry.lists["user.code_libraries"][0][library_list[number]], + talon_list[library_list[number]], selection, ) @@ -54,7 +57,8 @@ def gui_libraries(gui: imgui.GUI): gui.line() for i, entry in enumerate(library_list, 1): - gui.text(f"{i}. {entry}: {registry.lists['user.code_libraries'][0][entry]}") + talon_list = actions.user.talon_get_active_registry_list("user.code_libraries") + gui.text(f"{i}. {entry}: {talon_list[entry]}") gui.spacer() if gui.button("Toggle libraries close"): @@ -64,7 +68,8 @@ def gui_libraries(gui: imgui.GUI): 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()) + talon_list = actions.user.talon_get_active_registry_list("user.code_libraries") + library_list = sorted(talon_list.keys()) else: library_list = [] diff --git a/plugin/talon_helpers/talon_helpers.py b/plugin/talon_helpers/talon_helpers.py index 32f1b6ea34..1f3ab296a7 100644 --- a/plugin/talon_helpers/talon_helpers.py +++ b/plugin/talon_helpers/talon_helpers.py @@ -7,6 +7,7 @@ from talon import Module, actions, app, clip, registry, scope, speech_system, ui from talon.grammar import Phrase +from talon.scripting.types import ListTypeFull pp = pprint.PrettyPrinter() @@ -159,3 +160,7 @@ def talon_debug_app_windows(app: str): apps = ui.apps(name=app, background=False) for app in apps: pp.pprint(app.windows()) + + def talon_get_active_registry_list(name: str) -> ListTypeFull: + """Returns the active list from the Talon registry""" + return registry.lists[name][-1] From 81aa9dd6a29b3f83396e0f18a300d73403636bd3 Mon Sep 17 00:00:00 2001 From: Schwa Aresty Date: Sat, 30 Nov 2024 08:59:42 -0800 Subject: [PATCH 40/99] Like python, return is a keyword in kotlin (#1625) --- lang/kotlin/kotlin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/kotlin/kotlin.py b/lang/kotlin/kotlin.py index f644317a63..5a3d84fa1b 100644 --- a/lang/kotlin/kotlin.py +++ b/lang/kotlin/kotlin.py @@ -20,6 +20,7 @@ "abstract": "abstract ", "interface": "interface ", "final": "final ", + "return": "return ", } From 4104c4bb3aad988321f13b82f48f1336d3014d09 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 30 Nov 2024 18:04:51 +0100 Subject: [PATCH 41/99] Added tag for draft editor application running (#1619) Fixes #751 --- plugin/draft_editor/draft_editor.py | 28 +++++++++++++++----------- plugin/draft_editor/draft_editor.talon | 4 ++-- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/plugin/draft_editor/draft_editor.py b/plugin/draft_editor/draft_editor.py index 4b9365d7c3..5f816b8a85 100644 --- a/plugin/draft_editor/draft_editor.py +++ b/plugin/draft_editor/draft_editor.py @@ -2,6 +2,10 @@ mod = Module() mod.tag("draft_editor_active", "Indicates whether the draft editor has been activated") +mod.tag( + "draft_editor_app_running", + "Indicates that the draft editor app currently is running", +) mod.tag( "draft_editor_app_focused", "Indicates that the draft editor app currently has focus", @@ -12,13 +16,15 @@ def add_tag(tag: str): - tags.add(tag) - ctx.tags = list(tags) + if tag not in tags: + tags.add(tag) + ctx.tags = list(tags) def remove_tag(tag: str): - tags.discard(tag) - ctx.tags = list(tags) + if tag in tags: + tags.discard(tag) + ctx.tags = list(tags) default_names = ["Visual Studio Code", "Code", "VSCodium", "Codium", "code-oss"] @@ -36,15 +42,13 @@ def get_editor_names(): return names_csv.split(", ") if names_csv else default_names -@mod.scope -def scope(): +def handle_app_running(): editor_names = get_editor_names() - for app in ui.apps(background=False): if app.name in editor_names: - return {"draft_editor_running": True} - - return {"draft_editor_running": False} + add_tag("user.draft_editor_app_running") + return + remove_tag("user.draft_editor_app_running") def handle_app_activate(app): @@ -54,8 +58,8 @@ def handle_app_activate(app): remove_tag("user.draft_editor_app_focused") -ui.register("app_launch", scope.update) -ui.register("app_close", scope.update) +ui.register("app_launch", handle_app_running) +ui.register("app_close", handle_app_running) ui.register("app_activate", handle_app_activate) diff --git a/plugin/draft_editor/draft_editor.talon b/plugin/draft_editor/draft_editor.talon index 63360da5d9..409d1bccb2 100644 --- a/plugin/draft_editor/draft_editor.talon +++ b/plugin/draft_editor/draft_editor.talon @@ -1,5 +1,5 @@ -user.draft_editor_running: True -not tag: user.draft_editor_app_focused +tag: user.draft_editor_app_running +and not tag: user.draft_editor_app_focused - draft this: user.draft_editor_open() From 12a4b68f52f0ca5d6005043bb35aaea78bd7f419 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 30 Nov 2024 18:05:19 +0100 Subject: [PATCH 42/99] Don't show re formatters in formatter help (#1618) Fixes #1614 --- core/text/formatters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/text/formatters.py b/core/text/formatters.py index 1e9bd86a4c..18b2f24686 100644 --- a/core/text/formatters.py +++ b/core/text/formatters.py @@ -469,9 +469,10 @@ def formatters_reformat_selection(formatters: str): def get_formatters_words() -> dict: """Returns words currently used as formatters, and a demonstration string using those formatters""" + formatter_names = code_formatter_names | prose_formatter_names formatters_help_demo = {} - for phrase in sorted(all_phrase_formatters): - name = all_phrase_formatters[phrase] + for phrase in sorted(formatter_names): + name = formatter_names[phrase] demo = format_text_without_adding_to_history("one two three", name) if phrase in prose_formatter_names: phrase += " *" From c73476fc82276c700138708e9028b49257ab87cf Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 30 Nov 2024 18:23:36 +0100 Subject: [PATCH 43/99] Added draft editor argument (#1626) --- plugin/draft_editor/draft_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/draft_editor/draft_editor.py b/plugin/draft_editor/draft_editor.py index 5f816b8a85..87f1b1845e 100644 --- a/plugin/draft_editor/draft_editor.py +++ b/plugin/draft_editor/draft_editor.py @@ -42,7 +42,7 @@ def get_editor_names(): return names_csv.split(", ") if names_csv else default_names -def handle_app_running(): +def handle_app_running(_app): editor_names = get_editor_names() for app in ui.apps(background=False): if app.name in editor_names: From a6e57490728c245fc3dd13c02efbc630f7cc10cf Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 30 Nov 2024 12:29:25 -0500 Subject: [PATCH 44/99] =?UTF-8?q?Implement=20app=20Expos=C3=A9=20with=20ac?= =?UTF-8?q?cessibility=20since=20the=20CoreDock=20notification=20broke=20(?= =?UTF-8?q?#1610)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Obsoletes #1608. --------- Co-authored-by: Tom Craig Co-authored-by: Phil Cohen Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- apps/dock/dock.py | 28 ++++++++++++++++++++++++++++ apps/dock/dock.talon | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/dock/dock.py b/apps/dock/dock.py index 7174ac51a6..99f8fa92e2 100644 --- a/apps/dock/dock.py +++ b/apps/dock/dock.py @@ -1,3 +1,6 @@ +from pathlib import Path +from typing import Optional + from talon import Context, Module, actions, clip, ui ctx = Context() @@ -13,9 +16,34 @@ class Actions: def dock_send_notification(notification: str): """Send a CoreDock notification to the macOS Dock using SPI""" + def dock_app_expose(app: Optional[ui.App] = None): + """Activate macOS app Exposé via its Dock item (for the frontmost app if not specified)""" + @ctx.action_class("user") class UserActions: + def dock_app_expose(app=None): + if app is None: + app = ui.active_app() + + app_name = Path(app.path).stem + dock_items = ui.apps(bundle="com.apple.dock")[0].children.find( + AXSubrole="AXApplicationDockItem", AXTitle=app_name, max_depth=1 + ) + match len(dock_items): + case 1: + dock_items[0].perform("AXShowExpose") + case 0: + actions.app.notify( + body=f"No dock icon for “{app_name}”", + title="Unable to activate App Exposé", + ) + case _: + actions.app.notify( + body=f"Multiple dock icons for “{app_name}”", + title="Unable to activate App Exposé", + ) + def dock_send_notification(notification: str): from talon.mac.dock import dock_notify diff --git a/apps/dock/dock.talon b/apps/dock/dock.talon index 23879a9d58..896c00230e 100644 --- a/apps/dock/dock.talon +++ b/apps/dock/dock.talon @@ -1,5 +1,5 @@ os: mac - ^desktop$: user.dock_send_notification("com.apple.showdesktop.awake") -^window$: user.dock_send_notification("com.apple.expose.front.awake") +^window$: user.dock_app_expose() ^launch pad$: user.dock_send_notification("com.apple.launchpad.toggle") From 54b5cc171402e547fd840566ff7e7d28dc1c65ef Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 30 Nov 2024 18:38:54 +0100 Subject: [PATCH 45/99] Delimiter pairs (#1612) 1. Adds a Talon list defining delimiter pairs 2. Adds a voice command to insert delimiter pairs 3. Adds a voice command to wrap selection with delimiter pairs 4. Extends compound edit command to wrap with delimiter pairs These are the spoken forms I personal use (except for padding). Do we want to use something else? What do we do with all the existing symbol commands? https://github.com/talonhub/community/blob/bb240ba1cf958bd3e2c7791e5a57d09d205adc81/plugin/symbols/symbols.talon#L10-L45 Fixes #546 --------- Co-authored-by: Phil Cohen Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jeff Knaus Co-authored-by: Nicholas Riley --- BREAKING_CHANGES.txt | 2 + core/edit/delimiter_pair.py | 27 ++++++++ core/edit/delimiter_pair.talon-list | 25 ++++++++ core/edit/edit.talon | 2 +- core/edit/edit_command_actions.py | 50 ++++++++++++--- core/edit/insert_between.py | 2 +- plugin/symbols/symbols.talon | 43 +++---------- plugin/symbols/symbols_deprecated.talon | 83 +++++++++++++++++++++++++ 8 files changed, 189 insertions(+), 45 deletions(-) create mode 100644 core/edit/delimiter_pair.py create mode 100644 core/edit/delimiter_pair.talon-list create mode 100644 plugin/symbols/symbols_deprecated.talon diff --git a/BREAKING_CHANGES.txt b/BREAKING_CHANGES.txt index ba6219fd1e..5cd41b12c2 100644 --- a/BREAKING_CHANGES.txt +++ b/BREAKING_CHANGES.txt @@ -7,6 +7,8 @@ and when the change was applied given the delay between changes being submitted and the time they were reviewed and merged. --- +* 2024-11-24 Deprecated a bunch of symbol commands to insert delimited pairs + ("", '', []) in favor of the new `delimiter_pair` Talon list file. * 2024-09-07 Removed `get_list_from_csv` from `user_settings.py`. Please use the new `track_csv_list` decorator, which leverages Talon's `talon.watch` API for robustness on Talon launch. diff --git a/core/edit/delimiter_pair.py b/core/edit/delimiter_pair.py new file mode 100644 index 0000000000..f960968f8c --- /dev/null +++ b/core/edit/delimiter_pair.py @@ -0,0 +1,27 @@ +from talon import Module, actions + +mod = Module() + +mod.list("delimiter_pair", "List of matching pair delimiters") + + +@mod.capture(rule="{user.delimiter_pair}") +def delimiter_pair(m) -> list[str]: + pair = m.delimiter_pair.split() + assert len(pair) == 2 + # "space" requires a special written form because Talon lists are whitespace insensitive + open = pair[0] if pair[0] != "space" else " " + close = pair[1] if pair[1] != "space" else " " + return [open, close] + + +@mod.action_class +class Actions: + def delimiter_pair_insert(pair: list[str]): + """Insert a delimiter pair leaving the cursor in the middle""" + actions.user.insert_between(pair[0], pair[1]) + + def delimiter_pair_wrap_selection(pair: list[str]): + """Wrap selection with delimiter pair """ + selected = actions.edit.selected_text() + actions.insert(f"{pair[0]}{selected}{pair[1]}") diff --git a/core/edit/delimiter_pair.talon-list b/core/edit/delimiter_pair.talon-list new file mode 100644 index 0000000000..a634042bc3 --- /dev/null +++ b/core/edit/delimiter_pair.talon-list @@ -0,0 +1,25 @@ +list: user.delimiter_pair +- + +# Format: +# SPOKEN_FORM: LEFT_DELIMITER RIGHT_DELIMITER +# Use the literal symbols for delimiters (except for whitespace, which is "space") +# +# Examples: +# round: ( ) +# pad: space space + +round: ( ) +box: [ ] +diamond: < > +curly: { } +twin: "' '" +quad: '" "' +skis: ` ` +percentages: % % +pad: space space + +escaped quad: '\\" \\"' +escaped twin: "\\' \\'" +escaped round: \( \) +escaped box: \[ \] diff --git a/core/edit/edit.talon b/core/edit/edit.talon index bfb3131f3f..418bc8f37f 100644 --- a/core/edit/edit.talon +++ b/core/edit/edit.talon @@ -125,7 +125,7 @@ new line above: edit.line_insert_up() new line below | slap: edit.line_insert_down() # Insert padding with optional symbols -(pad | padding): user.insert_between(" ", " ") +padding: user.insert_between(" ", " ") (pad | padding) +: insert(" ") user.insert_many(symbol_key_list) diff --git a/core/edit/edit_command_actions.py b/core/edit/edit_command_actions.py index 9239ea5a0f..9a89255f19 100644 --- a/core/edit/edit_command_actions.py +++ b/core/edit/edit_command_actions.py @@ -1,36 +1,68 @@ from dataclasses import dataclass -from typing import Callable +from typing import Callable, Union from talon import Module, actions @dataclass -class EditAction: +class EditSimpleAction: + """ "Simple" actions are actions that don't require any arguments, only a type (select, copy, delete, etc.)""" + type: str + def __str__(self): + return self.type + @dataclass -class EditInsertAction(EditAction): +class EditInsertAction: type = "insert" text: str + def __str__(self): + return self.type + + +@dataclass +class EditWrapAction: + type = "wrapWithDelimiterPair" + pair: list[str] + + def __str__(self): + return self.type + @dataclass -class EditFormatAction(EditAction): +class EditFormatAction: type = "applyFormatter" formatters: str + def __str__(self): + return self.type + + +EditAction = Union[ + EditSimpleAction, + EditInsertAction, + EditWrapAction, + EditFormatAction, +] mod = Module() mod.list("edit_action", desc="Actions for the edit command") @mod.capture(rule="{user.edit_action}") -def edit_simple_action(m) -> EditAction: - return EditAction(m.edit_action) +def edit_simple_action(m) -> EditSimpleAction: + return EditSimpleAction(m.edit_action) -@mod.capture(rule="") +@mod.capture(rule=" wrap") +def edit_wrap_action(m) -> EditWrapAction: + return EditWrapAction(m.delimiter_pair) + + +@mod.capture(rule=" | ") def edit_action(m) -> EditAction: return m[0] @@ -62,6 +94,10 @@ def run_action_callback(action: EditAction): assert isinstance(action, EditInsertAction) actions.insert(action.text) + case "wrapWithDelimiterPair": + assert isinstance(action, EditWrapAction) + return lambda: actions.user.delimiter_pair_wrap_selection(action.pair) + case "applyFormatter": assert isinstance(action, EditFormatAction) actions.user.formatters_reformat_selection(action.formatters) diff --git a/core/edit/insert_between.py b/core/edit/insert_between.py index 75885092fb..667e8f5d60 100644 --- a/core/edit/insert_between.py +++ b/core/edit/insert_between.py @@ -7,6 +7,6 @@ class module_actions: def insert_between(before: str, after: str): """Insert `before + after`, leaving cursor between `before` and `after`. Not entirely reliable if `after` contains newlines.""" - actions.insert(before + after) + actions.insert(f"{before}{after}") for _ in after: actions.edit.left() diff --git a/plugin/symbols/symbols.talon b/plugin/symbols/symbols.talon index 251407cc6b..a44fc5a3db 100644 --- a/plugin/symbols/symbols.talon +++ b/plugin/symbols/symbols.talon @@ -1,44 +1,15 @@ new line: "\n" double dash: "--" triple quote: "'''" -(triple grave | triple back tick | gravy): insert("```") +triple grave | triple back tick | gravy: "```" (dot dot | dotdot): ".." ellipsis: "..." (comma and | spamma): ", " arrow: "->" dub arrow: "=>" -empty dub string: user.insert_between('"', '"') -empty escaped (dub string | dub quotes): user.insert_between('\\"', '\\"') -empty string: user.insert_between("'", "'") -empty escaped string: user.insert_between("\\'", "\\'") -(inside parens | args): user.insert_between("(", ")") -inside (squares | brackets | square brackets | list): user.insert_between("[", "]") -inside (braces | curly brackets): user.insert_between("{", "}") -inside percent: user.insert_between("%", "%") -inside (quotes | string): user.insert_between("'", "'") -inside (double quotes | dub quotes): user.insert_between('"', '"') -inside (graves | back ticks): user.insert_between("`", "`") -angle that: - text = edit.selected_text() - user.paste("<{text}>") -(square | bracket | square bracket) that: - text = edit.selected_text() - user.paste("[{text}]") -(brace | curly bracket) 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}'") -(double quote | dub quote) that: - text = edit.selected_text() - user.paste('"{text}"') -(grave | back tick) that: - text = edit.selected_text() - user.paste("`{text}`") + +# Insert delimiter pairs +: user.delimiter_pair_insert(delimiter_pair) + +# Wrap selection with delimiter pairs + that: user.delimiter_pair_wrap_selection(delimiter_pair) diff --git a/plugin/symbols/symbols_deprecated.talon b/plugin/symbols/symbols_deprecated.talon new file mode 100644 index 0000000000..ea2a3c8587 --- /dev/null +++ b/plugin/symbols/symbols_deprecated.talon @@ -0,0 +1,83 @@ +empty dub string: + user.deprecate_command("2024-11-24", "empty dub string", "quad") + user.insert_between('"', '"') + +empty escaped (dub string | dub quotes): + user.deprecate_command("2024-11-24", "empty escaped (dub string | dub quotes)", "escaped quad") + user.insert_between('\\"', '\\"') + +empty string: + user.deprecate_command("2024-11-24", "empty string", "twin") + user.insert_between("'", "'") + +empty escaped string: + user.deprecate_command("2024-11-24", "empty escaped string", "escaped twin") + user.insert_between("\\'", "\\'") + +inside (parens | args): + user.deprecate_command("2024-11-24", "inside (parens | args)", "round") + user.insert_between("(", ")") + +inside (squares | brackets | square brackets | list): + user.deprecate_command("2024-11-24", "inside (squares | brackets | square brackets | list)", "box") + user.insert_between("[", "]") + +inside (braces | curly brackets): + user.deprecate_command("2024-11-24", "inside (braces | curly brackets)", "curly") + user.insert_between("{", "}") + +inside percent: + user.deprecate_command("2024-11-24", "inside percent", "percentages") + user.insert_between("%", "%") + +inside (quotes | string): + user.deprecate_command("2024-11-24", "inside (quotes | string)", "twin") + user.insert_between("'", "'") + +inside (double quotes | dub quotes): + user.deprecate_command("2024-11-24", "inside (double quotes | dub quotes)", "quad") + user.insert_between('"', '"') + +inside (graves | back ticks): + user.deprecate_command("2024-11-24", "inside (graves | back ticks)", "skis") + user.insert_between("`", "`") + +angle that: + user.deprecate_command("2024-11-24", "angle that", "diamond that") + text = edit.selected_text() + user.paste("<{text}>") + +(square | bracket | square bracket) that: + user.deprecate_command("2024-11-24", "(square | bracket | square bracket) that", "box that") + text = edit.selected_text() + user.paste("[{text}]") + +(brace | curly bracket) that: + user.deprecate_command("2024-11-24", "(brace | curly bracket) that", "curly that") + text = edit.selected_text() + user.paste("{{{text}}}") + +(parens | args) that: + user.deprecate_command("2024-11-24", "(parens | args) that", "round that") + text = edit.selected_text() + user.paste("({text})") + +percent that: + user.deprecate_command("2024-11-24", "percent that", "percentages that") + text = edit.selected_text() + user.paste("%{text}%") + +quote that: + user.deprecate_command("2024-11-24", "quote that", "twin that") + text = edit.selected_text() + user.paste("'{text}'") + +(double quote | dub quote) that: + user.deprecate_command("2024-11-24", "(double quote | dub quote) that", "quad that") + text = edit.selected_text() + user.paste('"{text}"') + +(grave | back tick) that: + user.deprecate_command("2024-11-24", "(grave | back tick) that", "skis that") + text = edit.selected_text() + user.paste("`{text}`") From e383b9b4920f8bcbccfe83411e252a62f5ac93dc Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 30 Nov 2024 18:46:01 +0100 Subject: [PATCH 46/99] Cleaned up edit text file (#1617) Tested on windows and works fine --------- Co-authored-by: Nicholas Riley Co-authored-by: Jeff Knaus --- core/edit_text_file.py | 104 ------------------ core/edit_text_file.talon | 4 - core/edit_text_file/edit_text_file.py | 80 ++++++++++++++ core/edit_text_file/edit_text_file.talon | 4 + core/edit_text_file/edit_text_file.talon-list | 15 +++ 5 files changed, 99 insertions(+), 108 deletions(-) delete mode 100644 core/edit_text_file.py delete mode 100644 core/edit_text_file.talon create mode 100644 core/edit_text_file/edit_text_file.py create mode 100644 core/edit_text_file/edit_text_file.talon create mode 100644 core/edit_text_file/edit_text_file.talon-list diff --git a/core/edit_text_file.py b/core/edit_text_file.py deleted file mode 100644 index a064230884..0000000000 --- a/core/edit_text_file.py +++ /dev/null @@ -1,104 +0,0 @@ -import os -import subprocess -from pathlib import Path - -from talon import Context, Module, app - -# path to community/knausj root directory -REPO_DIR = os.path.dirname(os.path.dirname(__file__)) -SETTINGS_DIR = os.path.join(REPO_DIR, "settings") - -mod = Module() -ctx = Context() -mod.list( - "edit_file", - desc="Absolute paths to frequently edited files (Talon list, CSV, etc.)", -) - -_edit_files = { - "additional words": os.path.join( - REPO_DIR, "core", "vocabulary", "vocabulary.talon-list" - ), - "alphabet": os.path.join(REPO_DIR, "core", "keys", "letter.talon-list"), - "homophones": os.path.join(REPO_DIR, "core", "homophones", "homophones.csv"), - "search engines": os.path.join( - REPO_DIR, "core", "websites_and_search_engines", "search_engine.talon-list" - ), - "unix utilities": os.path.join( - REPO_DIR, "tags", "terminal", "unix_utility.talon-list" - ), - "websites": os.path.join( - REPO_DIR, "core", "websites_and_search_engines", "website.talon-list" - ), -} - -_settings_csvs = { - name: os.path.join(SETTINGS_DIR, file_name) - for name, file_name in { - "abbreviations": "abbreviations.csv", - "file extensions": "file_extensions.csv", - "words to replace": "words_to_replace.csv", - }.items() -} - -_edit_files.update(_settings_csvs) -ctx.lists["self.edit_file"] = _edit_files - - -@mod.action_class -class ModuleActions: - def edit_text_file(path: str): - """Tries to open a file in the user's preferred text editor.""" - - -winctx, linuxctx, macctx = Context(), Context(), Context() -winctx.matches = "os: windows" -linuxctx.matches = "os: linux" -macctx.matches = "os: mac" - - -@winctx.action_class("self") -class WinActions: - def edit_text_file(path): - # If there's no applications registered that can open the given type - # of file, 'edit' will fail, but 'open' always gives the user a - # choice between applications. - try: - os.startfile(path, "edit") - except OSError: - os.startfile(path, "open") - - -@macctx.action_class("self") -class MacActions: - def edit_text_file(path): - # -t means try to open in a text editor. - open_with_subprocess( - path, ["/usr/bin/open", "-t", Path(path).expanduser().resolve()] - ) - - -@linuxctx.action_class("self") -class LinuxActions: - def edit_text_file(path): - # we use xdg-open for this even though it might not open a text - # editor. we could use $EDITOR, but that might be something that - # requires a terminal (eg nano, vi). - try: - open_with_subprocess(path, ["xdg-open", Path(path).expanduser().resolve()]) - except FileNotFoundError: - app.notify(f"xdg-open missing. Could not open file for editing: {path}") - raise - - -# Helper for linux and mac. -def open_with_subprocess(path, args): - """Tries to open a file using the given subprocess arguments.""" - try: - return subprocess.run(args, timeout=0.5, check=True) - except subprocess.TimeoutExpired: - app.notify(f"Timeout trying to open file for editing: {path}") - raise - except subprocess.CalledProcessError: - app.notify(f"Could not open file for editing: {path}") - raise diff --git a/core/edit_text_file.talon b/core/edit_text_file.talon deleted file mode 100644 index 690c462c3e..0000000000 --- a/core/edit_text_file.talon +++ /dev/null @@ -1,4 +0,0 @@ -customize {user.edit_file}: - user.edit_text_file(edit_file) - sleep(500ms) - edit.file_end() diff --git a/core/edit_text_file/edit_text_file.py b/core/edit_text_file/edit_text_file.py new file mode 100644 index 0000000000..ce4764fbe5 --- /dev/null +++ b/core/edit_text_file/edit_text_file.py @@ -0,0 +1,80 @@ +import os +import subprocess +from pathlib import Path + +from talon import Context, Module, app + +# Path to community root directory +REPO_DIR = Path(__file__).parent.parent.parent + +mod = Module() +mod.list( + "edit_text_file", + desc="Paths to frequently edited files (Talon list, CSV, etc.)", +) + +ctx_win, ctx_linux, ctx_mac = Context(), Context(), Context() +ctx_win.matches = "os: windows" +ctx_linux.matches = "os: linux" +ctx_mac.matches = "os: mac" + + +@mod.action_class +class Actions: + def edit_text_file(file: str): + """Tries to open a file in the user's preferred text editor.""" + + +@ctx_win.action_class("user") +class WinActions: + def edit_text_file(file: str): + path = get_full_path(file) + # If there's no applications registered that can open the given type + # of file, 'edit' will fail, but 'open' always gives the user a + # choice between applications. + try: + os.startfile(path, "edit") + except OSError: + os.startfile(path, "open") + + +@ctx_mac.action_class("user") +class MacActions: + def edit_text_file(file: str): + path = get_full_path(file) + # -t means try to open in a text editor. + open_with_subprocess(path, ["/usr/bin/open", "-t", path.expanduser().resolve()]) + + +@ctx_linux.action_class("user") +class LinuxActions: + def edit_text_file(file: str): + path = get_full_path(file) + # we use xdg-open for this even though it might not open a text + # editor. we could use $EDITOR, but that might be something that + # requires a terminal (eg nano, vi). + try: + open_with_subprocess(path, ["xdg-open", path.expanduser().resolve()]) + except FileNotFoundError: + app.notify(f"xdg-open missing. Could not open file for editing: {path}") + raise + + +# Helper for linux and mac. +def open_with_subprocess(path: Path, args: list[str | Path]): + """Tries to open a file using the given subprocess arguments.""" + try: + subprocess.run(args, timeout=0.5, check=True) + except subprocess.TimeoutExpired: + app.notify(f"Timeout trying to open file for editing: {path}") + raise + except subprocess.CalledProcessError: + app.notify(f"Could not open file for editing: {path}") + raise + + +def get_full_path(file: str) -> Path: + path = Path(file) + if not path.is_absolute(): + path = REPO_DIR / path + return path.resolve() diff --git a/core/edit_text_file/edit_text_file.talon b/core/edit_text_file/edit_text_file.talon new file mode 100644 index 0000000000..6cd042054a --- /dev/null +++ b/core/edit_text_file/edit_text_file.talon @@ -0,0 +1,4 @@ +customize {user.edit_text_file}: + user.edit_text_file(edit_text_file) + sleep(500ms) + edit.file_end() diff --git a/core/edit_text_file/edit_text_file.talon-list b/core/edit_text_file/edit_text_file.talon-list new file mode 100644 index 0000000000..1af4e871b8 --- /dev/null +++ b/core/edit_text_file/edit_text_file.talon-list @@ -0,0 +1,15 @@ +list: user.edit_text_file +- + +additional words: core/vocabulary/vocabulary.talon-list +vocabulary: core/vocabulary/vocabulary.talon-list +alphabet: core/keys/letter.talon-list +homophones: core/homophones/homophones.csv +search engines: core/websites_and_search_engines/search_engine.talon-list +websites: core/websites_and_search_engines/website.talon-list + +unix utilities: tags/terminal/unix_utility.talon-list + +abbreviations: settings/abbreviations.csv +file extensions: settings/file_extensions.csv +words to replace: settings/words_to_replace.csv From 9f4904559688529616ad7b64e8724eef9682c651 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 3 Dec 2024 17:59:33 +0100 Subject: [PATCH 47/99] Remove duplicate word command (#1631) --- core/text/text.talon | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/text/text.talon b/core/text/text.talon index d8cc39f306..aa2143396b 100644 --- a/core/text/text.talon +++ b/core/text/text.talon @@ -16,9 +16,6 @@ phrase {user.phrase_ender}: that: user.formatters_reformat_selection(user.formatters) {user.word_formatter} : user.insert_formatted(word, word_formatter) (pace | paste): user.insert_formatted(clip.text(), formatters) -word : - user.add_phrase_to_history(word) - insert(word) recent list: user.toggle_phrase_history() recent close: user.phrase_history_hide() recent repeat : From b120359d2103d4833d2184551b6ecca02acd343e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 3 Dec 2024 19:00:56 +0100 Subject: [PATCH 48/99] More robust cancel that checks `_ts` (#1632) Mimic and sometimes Dragon don't have this key. --------- Co-authored-by: Phil Cohen --- plugin/cancel/cancel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/cancel/cancel.py b/plugin/cancel/cancel.py index dcbbc79ec6..0aa0a9e7d6 100644 --- a/plugin/cancel/cancel.py +++ b/plugin/cancel/cancel.py @@ -45,7 +45,8 @@ def pre_phrase(phrase: Phrase): # Check if the phrase is before the threshold if ts_threshold != 0: - start = getattr(words[0], "start", phrase["_ts"]) + # NB: mimic() and Dragon don't have this key. + start = getattr(words[0], "start", None) or getattr(phrase, "_ts", ts_threshold) phrase_starts_before_threshold = start < ts_threshold ts_threshold = 0 # Start of phrase is before threshold timestamp From 2f1ae8cf74edcc946a9375b78310d486e4a91574 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 7 Dec 2024 10:51:43 +0100 Subject: [PATCH 49/99] Fix bug in cancel code (#1633) Accidentally used `getattr` instead of `dict.get` --- plugin/cancel/cancel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/cancel/cancel.py b/plugin/cancel/cancel.py index 0aa0a9e7d6..e913100c18 100644 --- a/plugin/cancel/cancel.py +++ b/plugin/cancel/cancel.py @@ -46,7 +46,7 @@ def pre_phrase(phrase: Phrase): # Check if the phrase is before the threshold if ts_threshold != 0: # NB: mimic() and Dragon don't have this key. - start = getattr(words[0], "start", None) or getattr(phrase, "_ts", ts_threshold) + start = getattr(words[0], "start", None) or phrase.get("_ts", ts_threshold) phrase_starts_before_threshold = start < ts_threshold ts_threshold = 0 # Start of phrase is before threshold timestamp From 3941dc284a5f48671995e2262e70f963c5cc06e7 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Tue, 10 Dec 2024 12:05:27 +0100 Subject: [PATCH 50/99] Improve comments for mode indicator settings (#1634) --- plugin/mode_indicator/mode_indicator.talon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/mode_indicator/mode_indicator.talon b/plugin/mode_indicator/mode_indicator.talon index 0e257a587e..257f457a8f 100644 --- a/plugin/mode_indicator/mode_indicator.talon +++ b/plugin/mode_indicator/mode_indicator.talon @@ -3,9 +3,9 @@ settings(): user.mode_indicator_show = false # 30pixels diameter user.mode_indicator_size = 30 - # Center horizontally + # Center horizontally. (0=left, 0.5=center, 1=right) user.mode_indicator_x = 0.5 - # Align top + # Align top. (0=top, 0.5=center, 1=bottom) user.mode_indicator_y = 0 # Slightly transparent user.mode_indicator_color_alpha = 0.75 From c7a0b3423e70fd00e9c9191dbd609b79413bebbc Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 14 Dec 2024 17:11:35 +0100 Subject: [PATCH 51/99] Extract code into separate rpc client (#1433) Refactor command client so it can be used by multiple applications at once. I need this for my clipboard manager that is active at the same time as vscode. This pull request basically does two things: 1. Splits the very large `command_client.py` in to several python files in the new `rpc_client` directory. This is done purely mechanically with as minimal changes as possible. 2. Introduces a new Talon action `rpc_client_run_command` that takes the communication directory name as well as the callback to press the keys that starts the request execution. This is what allows us to run multiple applications at once. --------- Co-authored-by: Nicholas Riley Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- apps/vscode/command_client/command_client.py | 232 +----------------- .../rpc_client/get_communication_dir_path.py | 21 ++ .../rpc_client/read_json_with_timeout.py | 52 ++++ .../rpc_client/robust_unlink.py | 25 ++ .../command_client/rpc_client/rpc_client.py | 100 ++++++++ .../vscode/command_client/rpc_client/types.py | 24 ++ .../rpc_client/write_request.py | 60 +++++ apps/vscode/command_client/vscode.py | 3 +- 8 files changed, 294 insertions(+), 223 deletions(-) create mode 100644 apps/vscode/command_client/rpc_client/get_communication_dir_path.py create mode 100644 apps/vscode/command_client/rpc_client/read_json_with_timeout.py create mode 100644 apps/vscode/command_client/rpc_client/robust_unlink.py create mode 100644 apps/vscode/command_client/rpc_client/rpc_client.py create mode 100644 apps/vscode/command_client/rpc_client/types.py create mode 100644 apps/vscode/command_client/rpc_client/write_request.py diff --git a/apps/vscode/command_client/command_client.py b/apps/vscode/command_client/command_client.py index 86d9681d18..324e90042b 100644 --- a/apps/vscode/command_client/command_client.py +++ b/apps/vscode/command_client/command_client.py @@ -1,24 +1,9 @@ -import json -import os -import time -from dataclasses import dataclass from pathlib import Path -from tempfile import gettempdir from typing import Any -from uuid import uuid4 from talon import Context, Module, actions, speech_system -# How old a request file needs to be before we declare it stale and are willing -# to remove it -STALE_TIMEOUT_MS = 60_000 - -# The amount of time to wait for application to perform a command, in seconds -RPC_COMMAND_TIMEOUT_SECONDS = 3.0 - -# When doing exponential back off waiting for application to perform a command, how -# long to sleep the first time -MINIMUM_SLEEP_TIME_SECONDS = 0.0005 +from .rpc_client.get_communication_dir_path import get_communication_dir_path # Indicates whether a pre-phrase signal was emitted during the course of the # current phrase @@ -42,77 +27,6 @@ def __repr__(self): return "" -class NoFileServerException(Exception): - pass - - -def write_json_exclusive(path: Path, body: Any): - """Writes jsonified object to file, failing if the file already exists - - Args: - path (Path): The path of the file to write - body (Any): The object to convert to json and write - """ - with path.open("x") as out_file: - out_file.write(json.dumps(body)) - - -@dataclass -class Request: - command_id: str - args: list[Any] - wait_for_finish: bool - return_command_output: bool - uuid: str - - def to_dict(self): - return { - "commandId": self.command_id, - "args": self.args, - "waitForFinish": self.wait_for_finish, - "returnCommandOutput": self.return_command_output, - "uuid": self.uuid, - } - - -def write_request(request: Request, path: Path): - """Converts the given request to json and writes it to the file, failing if - the file already exists unless it is stale in which case it replaces it - - Args: - request (Request): The request to serialize - path (Path): The path to write to - - Raises: - Exception: If another process has an active request file - """ - try: - write_json_exclusive(path, request.to_dict()) - request_file_exists = False - except FileExistsError: - request_file_exists = True - - if request_file_exists: - handle_existing_request_file(path) - write_json_exclusive(path, request.to_dict()) - - -def handle_existing_request_file(path): - stats = path.stat() - - modified_time_ms = stats.st_mtime_ns / 1e6 - current_time_ms = time.time() * 1e3 - time_difference_ms = abs(modified_time_ms - current_time_ms) - - if time_difference_ms < STALE_TIMEOUT_MS: - raise Exception( - "Found recent request file; another Talon process is probably running" - ) - - print("Removing stale request file") - robust_unlink(path) - - def run_command( command_id: str, *args, @@ -138,142 +52,15 @@ def run_command( # variable argument lists args = [x for x in args if x is not NotSet] - communication_dir_path = get_communication_dir_path() - - if not communication_dir_path.exists(): - if args or return_command_output: - raise Exception("Must use command-server extension for advanced commands") - raise NoFileServerException("Communication directory not found") - - request_path = communication_dir_path / "request.json" - response_path = communication_dir_path / "response.json" - - # Generate uuid that will be mirrored back to us by command server for - # sanity checking - uuid = str(uuid4()) - - request = Request( - command_id=command_id, - args=args, - wait_for_finish=wait_for_finish, - return_command_output=return_command_output, - uuid=uuid, + return actions.user.rpc_client_run_command( + actions.user.command_server_directory(), + actions.user.trigger_command_server_command_execution, + command_id, + args, + wait_for_finish, + return_command_output, ) - # First, write the request to the request file, which makes us the sole - # owner because all other processes will try to open it with 'x' - write_request(request, request_path) - - # We clear the response file if it does exist, though it shouldn't - if response_path.exists(): - print("WARNING: Found old response file") - robust_unlink(response_path) - - # Then, perform keystroke telling application to execute the command in the - # request file. Because only the active application instance will accept - # keypresses, we can be sure that the active application instance will be the - # one to execute the command. - actions.user.trigger_command_server_command_execution() - - try: - decoded_contents = read_json_with_timeout(response_path) - finally: - # NB: We remove response file first because we want to do this while we - # still own the request file - robust_unlink(response_path) - robust_unlink(request_path) - - if decoded_contents["uuid"] != uuid: - raise Exception("uuids did not match") - - for warning in decoded_contents["warnings"]: - print(f"WARNING: {warning}") - - if decoded_contents["error"] is not None: - raise Exception(decoded_contents["error"]) - - actions.sleep("25ms") - - return decoded_contents["returnValue"] - - -def get_communication_dir_path(): - """Returns directory that is used by command-server for communication - - Returns: - Path: The path to the communication dir - """ - suffix = "" - - # NB: We don't suffix on Windows, because the temp dir is user-specific - # anyways - if hasattr(os, "getuid"): - suffix = f"-{os.getuid()}" - - return Path(gettempdir()) / f"{actions.user.command_server_directory()}{suffix}" - - -def robust_unlink(path: Path): - """Unlink the given file if it exists, and if we're on windows and it is - currently in use, just rename it - - Args: - path (Path): The path to unlink - """ - try: - path.unlink(missing_ok=True) - except OSError as e: - if hasattr(e, "winerror") and e.winerror == 32: - graveyard_dir = get_communication_dir_path() / "graveyard" - graveyard_dir.mkdir(parents=True, exist_ok=True) - graveyard_path = graveyard_dir / str(uuid4()) - print( - f"WARNING: File {path} was in use when we tried to delete it; " - f"moving to graveyard at path {graveyard_path}" - ) - path.rename(graveyard_path) - else: - raise e - - -def read_json_with_timeout(path: Path) -> Any: - """Repeatedly tries to read a json object from the given path, waiting - until there is a trailing new line indicating that the write is complete - - Args: - path (str): The path to read from - - Raises: - Exception: If we timeout waiting for a response - - Returns: - Any: The json-decoded contents of the file - """ - timeout_time = time.perf_counter() + RPC_COMMAND_TIMEOUT_SECONDS - sleep_time = MINIMUM_SLEEP_TIME_SECONDS - while True: - try: - raw_text = path.read_text() - - if raw_text.endswith("\n"): - break - except FileNotFoundError: - # If not found, keep waiting - pass - - actions.sleep(sleep_time) - - time_left = timeout_time - time.perf_counter() - - if time_left < 0: - raise Exception("Timed out waiting for response") - - # NB: We use minimum sleep time here to ensure that we don't spin with - # small sleeps due to clock slip - sleep_time = max(min(sleep_time * 2, time_left), MINIMUM_SLEEP_TIME_SECONDS) - - return json.loads(raw_text) - @mod.action_class class Actions: @@ -382,7 +169,8 @@ def get_signal_path(name: str) -> Path: Returns: Path: The signal path """ - communication_dir_path = get_communication_dir_path() + dir_name = actions.user.command_server_directory() + communication_dir_path = get_communication_dir_path(dir_name) if not communication_dir_path.exists(): raise MissingCommunicationDir() diff --git a/apps/vscode/command_client/rpc_client/get_communication_dir_path.py b/apps/vscode/command_client/rpc_client/get_communication_dir_path.py new file mode 100644 index 0000000000..5a52b9dede --- /dev/null +++ b/apps/vscode/command_client/rpc_client/get_communication_dir_path.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path +from tempfile import gettempdir + + +def get_communication_dir_path(name: str) -> Path: + """Returns directory that is used by command-server for communication + + Args: + name (str): The name of the communication dir + Returns: + Path: The path to the communication dir + """ + suffix = "" + + # NB: We don't suffix on Windows, because the temp dir is user-specific + # anyways + if hasattr(os, "getuid"): + suffix = f"-{os.getuid()}" + + return Path(gettempdir()) / f"{name}{suffix}" diff --git a/apps/vscode/command_client/rpc_client/read_json_with_timeout.py b/apps/vscode/command_client/rpc_client/read_json_with_timeout.py new file mode 100644 index 0000000000..4413f6488c --- /dev/null +++ b/apps/vscode/command_client/rpc_client/read_json_with_timeout.py @@ -0,0 +1,52 @@ +import json +import time +from pathlib import Path +from typing import Any + +from talon import actions + +# The amount of time to wait for application to perform a command, in seconds +RPC_COMMAND_TIMEOUT_SECONDS = 3.0 + +# When doing exponential back off waiting for application to perform a command, how +# long to sleep the first time +MINIMUM_SLEEP_TIME_SECONDS = 0.0005 + + +def read_json_with_timeout(path: Path) -> Any: + """Repeatedly tries to read a json object from the given path, waiting + until there is a trailing new line indicating that the write is complete + + Args: + path (str): The path to read from + + Raises: + Exception: If we timeout waiting for a response + + Returns: + Any: The json-decoded contents of the file + """ + timeout_time = time.perf_counter() + RPC_COMMAND_TIMEOUT_SECONDS + sleep_time = MINIMUM_SLEEP_TIME_SECONDS + while True: + try: + raw_text = path.read_text() + + if raw_text.endswith("\n"): + break + except FileNotFoundError: + # If not found, keep waiting + pass + + actions.sleep(sleep_time) + + time_left = timeout_time - time.perf_counter() + + if time_left < 0: + raise Exception("Timed out waiting for response") + + # NB: We use minimum sleep time here to ensure that we don't spin with + # small sleeps due to clock slip + sleep_time = max(min(sleep_time * 2, time_left), MINIMUM_SLEEP_TIME_SECONDS) + + return json.loads(raw_text) diff --git a/apps/vscode/command_client/rpc_client/robust_unlink.py b/apps/vscode/command_client/rpc_client/robust_unlink.py new file mode 100644 index 0000000000..293ddfdb1e --- /dev/null +++ b/apps/vscode/command_client/rpc_client/robust_unlink.py @@ -0,0 +1,25 @@ +from pathlib import Path +from uuid import uuid4 + + +def robust_unlink(path: Path): + """Unlink the given file if it exists, and if we're on windows and it is + currently in use, just rename it + + Args: + path (Path): The path to unlink + """ + try: + path.unlink(missing_ok=True) + except OSError as e: + if hasattr(e, "winerror") and e.winerror == 32: + graveyard_dir = path.parent / "graveyard" + graveyard_dir.mkdir(parents=True, exist_ok=True) + graveyard_path = graveyard_dir / str(uuid4()) + print( + f"WARNING: File {path} was in use when we tried to delete it; " + f"moving to graveyard at path {graveyard_path}" + ) + path.rename(graveyard_path) + else: + raise e diff --git a/apps/vscode/command_client/rpc_client/rpc_client.py b/apps/vscode/command_client/rpc_client/rpc_client.py new file mode 100644 index 0000000000..689e8d40f1 --- /dev/null +++ b/apps/vscode/command_client/rpc_client/rpc_client.py @@ -0,0 +1,100 @@ +from typing import Any, Callable +from uuid import uuid4 + +from talon import Module, actions + +from .get_communication_dir_path import get_communication_dir_path +from .read_json_with_timeout import read_json_with_timeout +from .robust_unlink import robust_unlink +from .types import NoFileServerException, Request +from .write_request import write_request + +mod = Module() + + +@mod.action_class +class Actions: + def rpc_client_run_command( + dir_name: str, + trigger_command_execution: Callable, + command_id: str, + args: list[Any], + wait_for_finish: bool = False, + return_command_output: bool = False, + ): + """Runs a command, using command server if available + + Args: + dir_name (str): The name of the directory to use for communication. + trigger_command_execution (Callable): The function to call to trigger command execution. + command_id (str): The ID of the command to run. + args: The arguments to the command. + wait_for_finish (bool, optional): Whether to wait for the command to finish before returning. Defaults to False. + return_command_output (bool, optional): Whether to return the output of the command. Defaults to False. + + Raises: + Exception: If there is an issue with the file-based communication, or + application raises an exception + + Returns: + Object: The response from the command, if requested. + """ + communication_dir_path = get_communication_dir_path(dir_name) + + if not communication_dir_path.exists(): + if args or return_command_output: + raise Exception( + "Must use command-server extension for advanced commands" + ) + raise NoFileServerException("Communication directory not found") + + request_path = communication_dir_path / "request.json" + response_path = communication_dir_path / "response.json" + + # Generate uuid that will be mirrored back to us by command server for + # sanity checking + uuid = str(uuid4()) + + request = Request( + command_id=command_id, + args=args, + wait_for_finish=wait_for_finish, + return_command_output=return_command_output, + uuid=uuid, + ) + + # First, write the request to the request file, which makes us the sole + # owner because all other processes will try to open it with 'x' + write_request(request, request_path) + + # We clear the response file if it does exist, though it shouldn't + if response_path.exists(): + print("WARNING: Found old response file") + robust_unlink(response_path) + + # Then, perform keystroke telling application to execute the command in the + # request file. Because only the active application instance will accept + # keypresses, we can be sure that the active application instance will be the + # one to execute the command. + trigger_command_execution() + + try: + decoded_contents = read_json_with_timeout(response_path) + finally: + # NB: We remove response file first because we want to do this while we + # still own the request file + robust_unlink(response_path) + robust_unlink(request_path) + + if decoded_contents["uuid"] != uuid: + raise Exception("uuids did not match") + + for warning in decoded_contents["warnings"]: + print(f"WARNING: {warning}") + + if decoded_contents["error"] is not None: + raise Exception(decoded_contents["error"]) + + actions.sleep("25ms") + + return decoded_contents["returnValue"] diff --git a/apps/vscode/command_client/rpc_client/types.py b/apps/vscode/command_client/rpc_client/types.py new file mode 100644 index 0000000000..f84c5cc9c3 --- /dev/null +++ b/apps/vscode/command_client/rpc_client/types.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Request: + command_id: str + args: list[Any] + wait_for_finish: bool + return_command_output: bool + uuid: str + + def to_dict(self): + return { + "commandId": self.command_id, + "args": self.args, + "waitForFinish": self.wait_for_finish, + "returnCommandOutput": self.return_command_output, + "uuid": self.uuid, + } + + +class NoFileServerException(Exception): + pass diff --git a/apps/vscode/command_client/rpc_client/write_request.py b/apps/vscode/command_client/rpc_client/write_request.py new file mode 100644 index 0000000000..74b464513e --- /dev/null +++ b/apps/vscode/command_client/rpc_client/write_request.py @@ -0,0 +1,60 @@ +import json +import time +from pathlib import Path +from typing import Any + +from .robust_unlink import robust_unlink +from .types import Request + +# How old a request file needs to be before we declare it stale and are willing +# to remove it +STALE_TIMEOUT_MS = 60_000 + + +def write_request(request: Request, path: Path): + """Converts the given request to json and writes it to the file, failing if + the file already exists unless it is stale in which case it replaces it + + Args: + request (Request): The request to serialize + path (Path): The path to write to + + Raises: + Exception: If another process has an active request file + """ + try: + write_json_exclusive(path, request.to_dict()) + request_file_exists = False + except FileExistsError: + request_file_exists = True + + if request_file_exists: + handle_existing_request_file(path) + write_json_exclusive(path, request.to_dict()) + + +def write_json_exclusive(path: Path, body: Any): + """Writes jsonified object to file, failing if the file already exists + + Args: + path (Path): The path of the file to write + body (Any): The object to convert to json and write + """ + with path.open("x") as out_file: + out_file.write(json.dumps(body)) + + +def handle_existing_request_file(path): + stats = path.stat() + + modified_time_ms = stats.st_mtime_ns / 1e6 + current_time_ms = time.time() * 1e3 + time_difference_ms = abs(modified_time_ms - current_time_ms) + + if time_difference_ms < STALE_TIMEOUT_MS: + raise Exception( + "Found recent request file; another Talon process is probably running" + ) + + print("Removing stale request file") + robust_unlink(path) diff --git a/apps/vscode/command_client/vscode.py b/apps/vscode/command_client/vscode.py index 29905db9b9..fce2814e73 100644 --- a/apps/vscode/command_client/vscode.py +++ b/apps/vscode/command_client/vscode.py @@ -2,7 +2,8 @@ from talon import Context, Module, actions -from .command_client import NoFileServerException, NotSet, run_command +from .command_client import NotSet, run_command +from .rpc_client.types import NoFileServerException mod = Module() From d54d0a6c13cac23ce6369239df0eeab1d14e43f0 Mon Sep 17 00:00:00 2001 From: Jeff Knaus Date: Sat, 14 Dec 2024 09:15:08 -0700 Subject: [PATCH 52/99] Add address bar tag (#1627) Implement common grammar for applications that support address bars I think it makes sense for this to be separate from the navigation tag, as not all applications support both. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley --- apps/finder/finder.py | 9 +++++++ apps/finder/finder.talon | 3 ++- apps/windows_explorer/windows_explorer.py | 10 ++++++++ apps/windows_explorer/windows_explorer.talon | 3 +++ .../websites_and_search_engines.py | 12 +++++++++- tags/address/address.py | 24 +++++++++++++++++++ tags/address/address.talon | 6 +++++ tags/browser/browser.py | 12 ++++++++++ tags/browser/browser.talon | 8 ++----- tags/file_manager/file_manager.py | 9 +++++++ tags/file_manager/file_manager.talon | 2 +- 11 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 tags/address/address.py create mode 100644 tags/address/address.talon diff --git a/apps/finder/finder.py b/apps/finder/finder.py index fc83818175..74d25a4c3b 100644 --- a/apps/finder/finder.py +++ b/apps/finder/finder.py @@ -75,3 +75,12 @@ def file_manager_select_file(path: str): """selects the file""" actions.key("home") actions.insert(path) + + def address_focus(): + actions.key("cmd-shift-g") + + def address_copy_address(): + actions.key("alt-cmd-c") + + def address_navigate(address: str): + actions.user.file_manager_open_directory(address) diff --git a/apps/finder/finder.talon b/apps/finder/finder.talon index 8e406b5397..9400148c6f 100644 --- a/apps/finder/finder.talon +++ b/apps/finder/finder.talon @@ -1,7 +1,9 @@ os: mac app: finder - +tag(): user.address tag(): user.file_manager +tag(): user.navigation tag(): user.tabs preferences: key(cmd-,) options: key(cmd-j) @@ -21,7 +23,6 @@ column view: key(cmd-3) list view: key(cmd-2) gallery view: key(cmd-4) -copy path: key(alt-cmd-c) trash it: key(cmd-backspace) hide [finder]: key(cmd-h) diff --git a/apps/windows_explorer/windows_explorer.py b/apps/windows_explorer/windows_explorer.py index 88cd59b5c9..22ecbd6f83 100644 --- a/apps/windows_explorer/windows_explorer.py +++ b/apps/windows_explorer/windows_explorer.py @@ -141,3 +141,13 @@ def file_manager_select_file(path: str): def file_manager_open_volume(volume: str): """file_manager_open_volume""" actions.user.file_manager_open_directory(volume) + + def address_focus(): + actions.key("ctrl-l") + + def address_copy_address(): + actions.key("ctrl-l") + actions.edit.copy() + + def address_navigate(address: str): + actions.user.file_manager_open_directory(address) diff --git a/apps/windows_explorer/windows_explorer.talon b/apps/windows_explorer/windows_explorer.talon index ef7e83e95d..503a158e4c 100644 --- a/apps/windows_explorer/windows_explorer.talon +++ b/apps/windows_explorer/windows_explorer.talon @@ -1,7 +1,10 @@ app: windows_explorer app: windows_file_browser - +tag(): user.address tag(): user.file_manager +tag(): user.navigation + ^go $: user.file_manager_open_volume("{letter}:") go app data: user.file_manager_open_directory("%AppData%") go program files: user.file_manager_open_directory("%programfiles%") diff --git a/core/websites_and_search_engines/websites_and_search_engines.py b/core/websites_and_search_engines/websites_and_search_engines.py index 1b5f934cd9..40637ec9d4 100644 --- a/core/websites_and_search_engines/websites_and_search_engines.py +++ b/core/websites_and_search_engines/websites_and_search_engines.py @@ -1,7 +1,7 @@ import webbrowser from urllib.parse import quote_plus -from talon import Module +from talon import Context, Module mod = Module() mod.list("website", desc="A website.") @@ -10,6 +10,11 @@ desc="A search engine. Any instance of %s will be replaced by query text", ) +ctx_browser = Context() +ctx_browser.matches = r""" +tag: browser +""" + @mod.action_class class Actions: @@ -21,3 +26,8 @@ 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) + + +@ctx_browser.capture("user.address", rule="{user.website}") +def address(m) -> str: + return m.website diff --git a/tags/address/address.py b/tags/address/address.py new file mode 100644 index 0000000000..86abca5af9 --- /dev/null +++ b/tags/address/address.py @@ -0,0 +1,24 @@ +from talon import Module + +mod = Module() +mod.tag( + "address", + desc="Application with a mechanism to browse or navigate by address; eg an address bar or Finder's go-to-folder functionality", +) + + +@mod.capture +def address(m) -> str: + """Captures an address; this capture must be implemented the context which desires to support the grammar""" + + +@mod.action_class +class Actions: + def address_focus(): + """Focuses the address input field""" + + def address_copy_address(): + """Copies the current address""" + + def address_navigate(address: str): + """Navigates to the desired address""" diff --git a/tags/address/address.talon b/tags/address/address.talon new file mode 100644 index 0000000000..fb484d5c86 --- /dev/null +++ b/tags/address/address.talon @@ -0,0 +1,6 @@ +tag: user.address +- +go [to] : user.address_navigate(address) +address copy | copy path | url copy | copy address | copy url: + user.address_copy_address() +address bar | go address | go url: user.address_focus() diff --git a/tags/browser/browser.py b/tags/browser/browser.py index a121f66877..322cb25a86 100644 --- a/tags/browser/browser.py +++ b/tags/browser/browser.py @@ -57,6 +57,18 @@ def tab_duplicate(): actions.user.paste(url_address) actions.key("enter") + def address_focus(): + actions.browser.focus_address() + + def address_copy_address(): + """Copies the current address""" + actions.browser.focus_address() + actions.sleep("100ms") + actions.edit.copy() + + def address_navigate(address: str): + actions.browser.go(address) + @ctx.action_class("browser") class BrowserActions: diff --git a/tags/browser/browser.talon b/tags/browser/browser.talon index 5862358912..fe9c7e52c6 100644 --- a/tags/browser/browser.talon +++ b/tags/browser/browser.talon @@ -1,16 +1,12 @@ tag: browser - +tag(): user.address tag(): user.find tag(): user.navigation -address bar | go address | go url: browser.focus_address() go page | page focus: browser.focus_page() -address copy | url copy | copy address | copy url: - browser.focus_address() - sleep(50ms) - edit.copy() + go home: browser.go_home() -go to {user.website}: browser.go(website) go private: browser.open_private_window() bookmark it: browser.bookmark() diff --git a/tags/file_manager/file_manager.py b/tags/file_manager/file_manager.py index 525790121b..e9cdc75312 100644 --- a/tags/file_manager/file_manager.py +++ b/tags/file_manager/file_manager.py @@ -6,6 +6,10 @@ mod = Module() ctx = Context() +ctx_file_manager = Context() +ctx_file_manager.matches = r""" +tag: user.file_manager +""" 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") @@ -69,6 +73,11 @@ ctx.lists["self.file_manager_files"] = [] +@ctx_file_manager.capture("user.address", rule="{user.system_paths}") +def address(m) -> str: + return str(m) + + @mod.action_class class Actions: def file_manager_current_path() -> str: diff --git a/tags/file_manager/file_manager.talon b/tags/file_manager/file_manager.talon index 7d5f65d51f..56e45927fa 100644 --- a/tags/file_manager/file_manager.talon +++ b/tags/file_manager/file_manager.talon @@ -1,12 +1,12 @@ tag: user.file_manager - +tag(): user.address tag(): user.navigation title force: user.file_manager_refresh_title() manager show: user.file_manager_toggle_pickers() manager close: user.file_manager_hide_pickers() manager refresh: user.file_manager_update_lists() -go : user.file_manager_open_directory(system_path) (go parent | daddy): user.file_manager_open_parent() ^follow {user.file_manager_directories}$: user.file_manager_open_directory(file_manager_directories) From 17af9d40f36329541c1306f265b8ff9af92bb2a1 Mon Sep 17 00:00:00 2001 From: Jeff Knaus Date: Sat, 14 Dec 2024 09:27:40 -0700 Subject: [PATCH 53/99] Move formatters to talon lists (#1609) Move formatters to talon lists for ease of customization --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Phil Cohen Co-authored-by: Nicholas Riley Co-authored-by: Andreas Arvidsson --- core/text/code_formatter.talon-list | 18 +++++ core/text/formatters.py | 100 ++++++++++++--------------- core/text/prose_formatter.talon-list | 6 ++ core/text/reformatter.talon-list | 5 ++ core/text/word_formatter.talon-list | 6 ++ test/stubs/talon/__init__.py | 7 ++ 6 files changed, 85 insertions(+), 57 deletions(-) create mode 100644 core/text/code_formatter.talon-list create mode 100644 core/text/prose_formatter.talon-list create mode 100644 core/text/reformatter.talon-list create mode 100644 core/text/word_formatter.talon-list diff --git a/core/text/code_formatter.talon-list b/core/text/code_formatter.talon-list new file mode 100644 index 0000000000..224099d26b --- /dev/null +++ b/core/text/code_formatter.talon-list @@ -0,0 +1,18 @@ +list: user.code_formatter +- +all cap: ALL_CAPS +all down: ALL_LOWERCASE +camel: PRIVATE_CAMEL_CASE +dotted: DOT_SEPARATED +dub string: DOUBLE_QUOTED_STRING +dunder: DOUBLE_UNDERSCORE +hammer: PUBLIC_CAMEL_CASE +kebab: DASH_SEPARATED +packed: DOUBLE_COLON_SEPARATED +padded: SPACE_SURROUNDED_STRING +slasher: ALL_SLASHES +conga: SLASH_SEPARATED +smash: NO_SPACES +snake: SNAKE_CASE +string: SINGLE_QUOTED_STRING +constant: ALL_CAPS,SNAKE_CASE diff --git a/core/text/formatters.py b/core/text/formatters.py index 18b2f24686..8a49e55736 100644 --- a/core/text/formatters.py +++ b/core/text/formatters.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from typing import Callable, Optional, Union -from talon import Context, Module, actions, app +from talon import Context, Module, actions, app, registry from talon.grammar import Phrase @@ -247,62 +247,14 @@ def de_camel(text: str) -> str: formatters_dict = {f.id: f for f in formatter_list} - -# Mapping from spoken phrases to formatter names -code_formatter_names = { - "all cap": "ALL_CAPS", - "all down": "ALL_LOWERCASE", - "camel": "PRIVATE_CAMEL_CASE", - "dotted": "DOT_SEPARATED", - "dub string": "DOUBLE_QUOTED_STRING", - "dunder": "DOUBLE_UNDERSCORE", - "hammer": "PUBLIC_CAMEL_CASE", - "kebab": "DASH_SEPARATED", - "packed": "DOUBLE_COLON_SEPARATED", - "padded": "SPACE_SURROUNDED_STRING", - "slasher": "ALL_SLASHES", - "conga": "SLASH_SEPARATED", - "smash": "NO_SPACES", - "snake": "SNAKE_CASE", - "string": "SINGLE_QUOTED_STRING", - "constant": "ALL_CAPS,SNAKE_CASE", -} -prose_formatter_names = { - "say": "NOOP", - "speak": "NOOP", - "sentence": "CAPITALIZE_FIRST_WORD", - "title": "CAPITALIZE_ALL_WORDS", -} -reformatter_names = { - "cap": "CAPITALIZE", - "list": "COMMA_SEPARATED", - "unformat": "REMOVE_FORMATTING", -} -word_formatter_names = { - "word": "ALL_LOWERCASE", - "trot": "TRAILING_SPACE,ALL_LOWERCASE", - "proud": "CAPITALIZE_FIRST_WORD", - "leap": "TRAILING_SPACE,CAPITALIZE_FIRST_WORD", -} - - -all_phrase_formatters = code_formatter_names | prose_formatter_names | reformatter_names - mod = Module() -mod.list("formatters", desc="list of all formatters (code and prose)") +mod.list("reformatter", desc="list of all reformatters") mod.list("code_formatter", desc="list of formatters typically applied to code") mod.list( "prose_formatter", desc="list of prose formatters (words to start dictating prose)" ) mod.list("word_formatter", "List of word formatters") -ctx = Context() -ctx.lists["self.formatters"] = all_phrase_formatters -ctx.lists["self.code_formatter"] = code_formatter_names -ctx.lists["self.prose_formatter"] = prose_formatter_names -ctx.lists["user.word_formatter"] = word_formatter_names - - # The last phrase spoken, without & with formatting. Used for reformatting. last_phrase = "" last_phrase_formatted = "" @@ -362,10 +314,12 @@ def shrink_to_string_inside(text: str) -> tuple[str, str, str]: return text, "", "" -@mod.capture(rule="{self.formatters}+") +@mod.capture( + rule="({user.code_formatter} | {user.prose_formatter} | {user.reformatter})+" +) def formatters(m) -> str: "Returns a comma-separated string of formatters e.g. 'SNAKE,DUBSTRING'" - return ",".join(m.formatters_list) + return ",".join(list(m)) @mod.capture(rule="{self.code_formatter}+") @@ -421,6 +375,30 @@ def formatter_immune(m) -> ImmuneString: return ImmuneString(str(value)) +def get_formatters_and_prose_formatters( + include_reformatters: bool, +) -> tuple[dict[str, str], dict[str, str]]: + """Returns dictionary of non-word formatters and a dictionary of all prose formatters""" + formatters = {} + prose_formatters = {} + formatters.update( + actions.user.talon_get_active_registry_list("user.code_formatter") + ) + formatters.update( + actions.user.talon_get_active_registry_list("user.prose_formatter") + ) + + if include_reformatters: + formatters.update( + actions.user.talon_get_active_registry_list("user.reformatter") + ) + + prose_formatters.update( + actions.user.talon_get_active_registry_list("user.prose_formatter") + ) + return formatters, prose_formatters + + @mod.action_class class Actions: def formatted_text(phrase: Union[str, Phrase], formatters: str) -> str: @@ -469,10 +447,14 @@ def formatters_reformat_selection(formatters: str): def get_formatters_words() -> dict: """Returns words currently used as formatters, and a demonstration string using those formatters""" - formatter_names = code_formatter_names | prose_formatter_names formatters_help_demo = {} - for phrase in sorted(formatter_names): - name = formatter_names[phrase] + formatters, prose_formatters = get_formatters_and_prose_formatters( + include_reformatters=False + ) + prose_formatter_names = prose_formatters.keys() + + for phrase in sorted(formatters): + name = formatters[phrase] demo = format_text_without_adding_to_history("one two three", name) if phrase in prose_formatter_names: phrase += " *" @@ -482,8 +464,12 @@ def get_formatters_words() -> dict: def get_reformatters_words() -> dict: """Returns words currently used as re-formatters, and a demonstration string using those re-formatters""" formatters_help_demo = {} - for phrase in sorted(all_phrase_formatters): - name = all_phrase_formatters[phrase] + formatters, prose_formatters = get_formatters_and_prose_formatters( + include_reformatters=True + ) + prose_formatter_names = prose_formatters.keys() + for phrase in sorted(formatters): + name = formatters[phrase] demo = format_text_without_adding_to_history("one_two_three", name, True) if phrase in prose_formatter_names: phrase += " *" diff --git a/core/text/prose_formatter.talon-list b/core/text/prose_formatter.talon-list new file mode 100644 index 0000000000..45c901ad68 --- /dev/null +++ b/core/text/prose_formatter.talon-list @@ -0,0 +1,6 @@ +list: user.prose_formatter +- +say: NOOP +speak: NOOP +sentence: CAPITALIZE_FIRST_WORD +title: CAPITALIZE_ALL_WORDS diff --git a/core/text/reformatter.talon-list b/core/text/reformatter.talon-list new file mode 100644 index 0000000000..0e63755f5c --- /dev/null +++ b/core/text/reformatter.talon-list @@ -0,0 +1,5 @@ +list: user.reformatter +- +cap: CAPITALIZE +list: COMMA_SEPARATED +unformat: REMOVE_FORMATTING diff --git a/core/text/word_formatter.talon-list b/core/text/word_formatter.talon-list new file mode 100644 index 0000000000..2957a8a7b1 --- /dev/null +++ b/core/text/word_formatter.talon-list @@ -0,0 +1,6 @@ +list: user.word_formatter +- +word: ALL_LOWERCASE +trot: TRAILING_SPACE,ALL_LOWERCASE +proud: CAPITALIZE_FIRST_WORD +leap: TRAILING_SPACE,CAPITALIZE_FIRST_WORD diff --git a/test/stubs/talon/__init__.py b/test/stubs/talon/__init__.py index f7001e4910..448db23b43 100644 --- a/test/stubs/talon/__init__.py +++ b/test/stubs/talon/__init__.py @@ -176,6 +176,12 @@ class Settings: """ +class Registry: + """ + Implements something like Talon's registry + """ + + class Resource: """ Implements something like the talon resource system @@ -203,6 +209,7 @@ class App: ui = UI() settings = Settings() resource = Resource() +registry = Registry() # Indicate to test files that they should load since we're running in test mode test_mode = True From 2a650709429d6dffeb4ab6692b7494a4f7cf15c5 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 14 Dec 2024 17:57:20 +0100 Subject: [PATCH 54/99] Added format action to compound edit command (#1635) `"camel format line"` `"snake format token"` Co-authored-by: Nicholas Riley --- core/edit/edit_command_actions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/edit/edit_command_actions.py b/core/edit/edit_command_actions.py index 9a89255f19..7701e5c670 100644 --- a/core/edit/edit_command_actions.py +++ b/core/edit/edit_command_actions.py @@ -62,7 +62,14 @@ def edit_wrap_action(m) -> EditWrapAction: return EditWrapAction(m.delimiter_pair) -@mod.capture(rule=" | ") +@mod.capture(rule=" format") +def edit_format_action(m) -> EditFormatAction: + return EditFormatAction(m.formatters) + + +@mod.capture( + rule=" | | " +) def edit_action(m) -> EditAction: return m[0] From 5b849b00756ed4d18f01242b37299a71dd00bf05 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 14 Dec 2024 17:15:44 +0000 Subject: [PATCH 55/99] Fix select/copy/cut way left/right (#1636) Currently edit modifier "way left" and "way right" uses actions.user.select_line_start/end This action based upon it's comment description should be the same as actions.edit.extend_line_start/end but they have different behaviour. Behaviour when using the current user action: before doing an extend line action it does a edit.left action, leading to a different selection than intended. Prior to the move to the compound edit command `select way left` was using the actions.edit.extend_line_ action I'm also not sure if the older action is actually necessary when we have the edit versions? Co-authored-by: Nicholas Riley --- core/edit/edit_command_modifiers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/edit/edit_command_modifiers.py b/core/edit/edit_command_modifiers.py index 2dc647b7fc..9be6dafccd 100644 --- a/core/edit/edit_command_modifiers.py +++ b/core/edit/edit_command_modifiers.py @@ -22,8 +22,8 @@ def edit_modifier(m) -> EditModifier: "paragraph": actions.edit.select_paragraph, "word": actions.edit.select_word, "line": actions.edit.select_line, - "lineEnd": actions.user.select_line_end, - "lineStart": actions.user.select_line_start, + "lineEnd": actions.edit.extend_line_end, + "lineStart": actions.edit.extend_line_start, "fileStart": actions.edit.extend_file_start, "fileEnd": actions.edit.extend_file_end, } From 1e3f2e1683c3bacd4cee20d85942db5fe24e8c43 Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 14 Dec 2024 23:17:20 -0500 Subject: [PATCH 56/99] Restore no-op behavior for word formatter (#1640) --- core/text/word_formatter.talon-list | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/text/word_formatter.talon-list b/core/text/word_formatter.talon-list index 2957a8a7b1..87d9c9e3ee 100644 --- a/core/text/word_formatter.talon-list +++ b/core/text/word_formatter.talon-list @@ -1,6 +1,7 @@ list: user.word_formatter - -word: ALL_LOWERCASE -trot: TRAILING_SPACE,ALL_LOWERCASE + +word: NOOP +trot: TRAILING_SPACE proud: CAPITALIZE_FIRST_WORD leap: TRAILING_SPACE,CAPITALIZE_FIRST_WORD From beb5376eb0286f6a92f247777e17e9cf832996bb Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sun, 15 Dec 2024 12:31:02 -0500 Subject: [PATCH 57/99] Remove nonfunctional [code_]libraries_gui support (#1639) --- README.md | 1 - lang/c/c.py | 2 +- lang/c/c.talon | 1 - lang/lua/lua.py | 3 -- lang/lua/lua.talon | 1 - lang/python/python.talon | 1 - lang/r/r.talon | 1 - lang/rust/rust.py | 4 +- lang/rust/rust.talon | 1 - lang/stata/stata.py | 4 +- lang/stata/stata.talon | 1 - lang/tags/libraries.py | 11 +++++ lang/tags/libraries_gui.py | 85 -------------------------------- lang/tags/libraries_gui.talon | 8 --- lang/tags/library_gui_open.talon | 6 --- 15 files changed, 15 insertions(+), 115 deletions(-) delete mode 100644 lang/tags/libraries_gui.py delete mode 100644 lang/tags/libraries_gui.talon delete mode 100644 lang/tags/library_gui_open.talon diff --git a/README.md b/README.md index f8bd6bbfd7..94d1cf0eff 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,6 @@ Python, C#, Talon and JavaScript language support is broken up into multiple tag - `lang/tags/functions_common.{talon,py}` - common functions (also includes a GUI for picking functions) - `lang/tags/imperative.{talon,py}` - statements (e.g., `if`, `while`, `switch`) - `lang/tags/libraries.{talon,py}` - libraries and imports -- `lang/tags/libraries_gui.{talon,py}` - graphical helper for common libraries - `lang/tags/object_oriented.{talon,py}` - objects and classes (e.g., `this`) - `lang/tags/operators_array.{talon,py}` - array operators (e.g., Ruby's `x[0]`) - `lang/tags/operators_assignment.{talon,py}` - assignment operators (e.g., C++'s `x += 5`) diff --git a/lang/c/c.py b/lang/c/c.py index b485d0c732..debd44f085 100644 --- a/lang/c/c.py +++ b/lang/c/c.py @@ -391,4 +391,4 @@ def code_private_static_function(text: str): actions.user.code_insert_function(result, None) def code_insert_library(text: str, selection: str): - actions.user.paste(f"include <{text}>") + actions.user.paste(f"#include <{text}>") diff --git a/lang/c/c.talon b/lang/c/c.talon index 40200f7c58..6af850bd78 100644 --- a/lang/c/c.talon +++ b/lang/c/c.talon @@ -10,7 +10,6 @@ tag(): user.code_data_null tag(): user.code_functions tag(): user.code_functions_common tag(): user.code_libraries -tag(): user.code_libraries_gui tag(): user.code_operators_array tag(): user.code_operators_assignment tag(): user.code_operators_bitwise diff --git a/lang/lua/lua.py b/lang/lua/lua.py index 8205c12c97..dc125741a9 100644 --- a/lang/lua/lua.py +++ b/lang/lua/lua.py @@ -207,9 +207,6 @@ def code_insert_function(text: str, selection: str): def code_import(): actions.user.insert_between("local ", " = require('')") - ## - # code_libraries_gui - ## def code_insert_library(text: str, selection: str): actions.insert(f"local {selection} = require('{selection}')") diff --git a/lang/lua/lua.talon b/lang/lua/lua.talon index cfbbd26d05..53e2d80fe8 100644 --- a/lang/lua/lua.talon +++ b/lang/lua/lua.talon @@ -10,7 +10,6 @@ tag(): user.code_data_null tag(): user.code_functions tag(): user.code_functions_common tag(): user.code_libraries -tag(): user.code_libraries_gui tag(): user.code_operators_array tag(): user.code_operators_assignment tag(): user.code_operators_bitwise diff --git a/lang/python/python.talon b/lang/python/python.talon index 6584965cb3..692e058728 100644 --- a/lang/python/python.talon +++ b/lang/python/python.talon @@ -11,7 +11,6 @@ tag(): user.code_functions tag(): user.code_functions_common tag(): user.code_keywords tag(): user.code_libraries -tag(): user.code_libraries_gui tag(): user.code_operators_array tag(): user.code_operators_assignment tag(): user.code_operators_bitwise diff --git a/lang/r/r.talon b/lang/r/r.talon index 042132ea01..f435c3547e 100644 --- a/lang/r/r.talon +++ b/lang/r/r.talon @@ -9,7 +9,6 @@ tag(): user.code_data_null tag(): user.code_functions tag(): user.code_functions_common tag(): user.code_libraries -tag(): user.code_libraries_gui tag(): user.code_operators_assignment tag(): user.code_operators_bitwise tag(): user.code_operators_math diff --git a/lang/rust/rust.py b/lang/rust/rust.py index dcb0e85c6d..469d1a777a 100644 --- a/lang/rust/rust.py +++ b/lang/rust/rust.py @@ -178,7 +178,7 @@ def code_comment_documentation_block_inner(): } -# tag: libraries_gui +# tag: libraries ctx.lists["user.code_libraries"] = { "eye oh": "std::io", "file system": "std::fs", @@ -346,8 +346,6 @@ def code_insert_function(text: str, selection: str): def code_import(): actions.auto_insert("use ") - # tag: libraries_gui - def code_insert_library(text: str, selection: str): actions.user.paste(f"use {text}") diff --git a/lang/rust/rust.talon b/lang/rust/rust.talon index dd2911c2a4..c715d1e53f 100644 --- a/lang/rust/rust.talon +++ b/lang/rust/rust.talon @@ -14,7 +14,6 @@ tag(): user.code_data_null tag(): user.code_functions tag(): user.code_functions_common tag(): user.code_libraries -tag(): user.code_libraries_gui tag(): user.code_operators_array tag(): user.code_operators_assignment diff --git a/lang/stata/stata.py b/lang/stata/stata.py index 30821c33f7..11baea8859 100644 --- a/lang/stata/stata.py +++ b/lang/stata/stata.py @@ -28,7 +28,7 @@ "esttab": "esttab", } -# libraries_gui.py +# libraries.py ctx.lists["user.code_libraries"] = { "estout": "estout", } @@ -110,7 +110,7 @@ def code_next(): def code_import(): actions.auto_insert("ssc install ") - # libraries_gui.py + # libraries.py def code_insert_library(text: str, selection: str): actions.auto_insert("ssc install ") actions.user.paste(text + selection) diff --git a/lang/stata/stata.talon b/lang/stata/stata.talon index 13975e6792..26ce3d1974 100644 --- a/lang/stata/stata.talon +++ b/lang/stata/stata.talon @@ -8,7 +8,6 @@ tag(): user.code_comment_line tag(): user.code_functions tag(): user.code_functions_common tag(): user.code_libraries -tag(): user.code_libraries_gui tag(): user.code_operators_array tag(): user.code_operators_assignment diff --git a/lang/tags/libraries.py b/lang/tags/libraries.py index b5717fca93..38f846a6e9 100644 --- a/lang/tags/libraries.py +++ b/lang/tags/libraries.py @@ -8,8 +8,19 @@ desc="Tag for enabling commands for importing libraries", ) +mod.list("code_libraries", desc="List of libraries for active language") + + +@mod.capture(rule="{user.code_libraries}") +def code_libraries(m) -> str: + """Returns a type""" + return m.code_libraries + @mod.action_class class Actions: def code_import(): """import/using equivalent""" + + def code_insert_library(text: str, selection: str): + """Inserts a library and positions the cursor appropriately""" diff --git a/lang/tags/libraries_gui.py b/lang/tags/libraries_gui.py deleted file mode 100644 index 9c406a16ce..0000000000 --- a/lang/tags/libraries_gui.py +++ /dev/null @@ -1,85 +0,0 @@ -from talon import Context, Module, actions, imgui, registry - -ctx = Context() -mod = Module() - -mod.list("code_libraries", desc="List of libraries for active language") -mod.tag( - "code_libraries_gui_showing", desc="Active when the library picker GUI is showing" -) - -# global -library_list = [] - - -@mod.capture(rule="{user.code_libraries}") -def code_libraries(m) -> str: - """Returns a type""" - return m.code_libraries - - -mod.tag("code_libraries_gui", desc="Tag for enabling GUI support for common libraries") - - -@mod.action_class -class Actions: - def code_toggle_libraries(): - """GUI: List libraries for active language""" - global library_list - if gui_libraries.showing: - library_list = [] - gui_libraries.hide() - ctx.tags.discard("user.code_libraries_gui_showing") - 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): - talon_list = actions.user.talon_get_active_registry_list( - "user.code_libraries" - ) - actions.user.code_insert_library( - talon_list[library_list[number]], - selection, - ) - - # TODO: clarify the relation between `code_insert_library` - # and `code_import` - - def code_insert_library(text: str, selection: str): - """Inserts a library and positions the cursor appropriately""" - - -@imgui.open() -def gui_libraries(gui: imgui.GUI): - gui.text("Libraries") - gui.line() - - for i, entry in enumerate(library_list, 1): - talon_list = actions.user.talon_get_active_registry_list("user.code_libraries") - gui.text(f"{i}. {entry}: {talon_list[entry]}") - - gui.spacer() - if gui.button("Toggle libraries close"): - actions.user.code_toggle_libraries_hide() - - -def update_library_list_and_freeze(): - global library_list - if "user.code_libraries" in registry.lists: - talon_list = actions.user.talon_get_active_registry_list("user.code_libraries") - library_list = sorted(talon_list.keys()) - else: - library_list = [] - - gui_libraries.show() - ctx.tags.add("user.code_libraries_gui_showing") - - -def commands_updated(_): - if gui_libraries.showing: - update_library_list_and_freeze() - - -registry.register("update_commands", commands_updated) diff --git a/lang/tags/libraries_gui.talon b/lang/tags/libraries_gui.talon deleted file mode 100644 index 3ed8b8399f..0000000000 --- a/lang/tags/libraries_gui.talon +++ /dev/null @@ -1,8 +0,0 @@ -tag: user.code_libraries_gui -- -# NOTE: This file does not define any commands, as the commands vary from -# language to language, e.g., Python uses 'import', C uses 'include', -# R uses 'library', etcetera. - -# TODO: If this ever becomes possible, we should abstract over these commands -# using a variable which can be set to the context-specific word. diff --git a/lang/tags/library_gui_open.talon b/lang/tags/library_gui_open.talon deleted file mode 100644 index 6558dbce58..0000000000 --- a/lang/tags/library_gui_open.talon +++ /dev/null @@ -1,6 +0,0 @@ -tag: user.code_libraries_gui_showing -- -# The show functions for this have language specific names, e.g. toggle imports for Python -# but let's use a generic name for the close one. Having it behind this tag allows it to be closed -# even if your editor isn't visible. -toggle libraries close: user.code_toggle_libraries() From a09ddf8f954370298e3e2d69fbb37415da5704fe Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 21 Dec 2024 00:34:08 +0100 Subject: [PATCH 58/99] Moved formatters to separate folder (#1643) --- core/{text => formatters}/code_formatter.talon-list | 0 core/{text => formatters}/formatters.py | 0 core/{text => formatters}/prose_formatter.talon-list | 0 core/{text => formatters}/reformatter.talon-list | 0 core/{text => formatters}/word_formatter.talon-list | 0 test/test_formatters.py | 2 +- 6 files changed, 1 insertion(+), 1 deletion(-) rename core/{text => formatters}/code_formatter.talon-list (100%) rename core/{text => formatters}/formatters.py (100%) rename core/{text => formatters}/prose_formatter.talon-list (100%) rename core/{text => formatters}/reformatter.talon-list (100%) rename core/{text => formatters}/word_formatter.talon-list (100%) diff --git a/core/text/code_formatter.talon-list b/core/formatters/code_formatter.talon-list similarity index 100% rename from core/text/code_formatter.talon-list rename to core/formatters/code_formatter.talon-list diff --git a/core/text/formatters.py b/core/formatters/formatters.py similarity index 100% rename from core/text/formatters.py rename to core/formatters/formatters.py diff --git a/core/text/prose_formatter.talon-list b/core/formatters/prose_formatter.talon-list similarity index 100% rename from core/text/prose_formatter.talon-list rename to core/formatters/prose_formatter.talon-list diff --git a/core/text/reformatter.talon-list b/core/formatters/reformatter.talon-list similarity index 100% rename from core/text/reformatter.talon-list rename to core/formatters/reformatter.talon-list diff --git a/core/text/word_formatter.talon-list b/core/formatters/word_formatter.talon-list similarity index 100% rename from core/text/word_formatter.talon-list rename to core/formatters/word_formatter.talon-list diff --git a/test/test_formatters.py b/test/test_formatters.py index 5fbb13a96c..39fa41dcd5 100644 --- a/test/test_formatters.py +++ b/test/test_formatters.py @@ -5,7 +5,7 @@ from talon import actions - from core.text import formatters + from core.formatters import formatters def setup_function(): actions.reset_test_actions() From 5f080edfd50be2494742f0f746ee873d493eccb5 Mon Sep 17 00:00:00 2001 From: Timo <24251362+timo95@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:34:35 +0100 Subject: [PATCH 59/99] Add missing jetbrains port mapping (#1644) The other ce mappings don't work on windows. --- apps/jetbrains/jetbrains.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/jetbrains/jetbrains.py b/apps/jetbrains/jetbrains.py index ea08bbaad5..b76cc63e9b 100644 --- a/apps/jetbrains/jetbrains.py +++ b/apps/jetbrains/jetbrains.py @@ -31,6 +31,7 @@ "google-android-studio": 8652, "idea64.exe": 8653, "IntelliJ IDEA": 8653, + "IntelliJ IDEA Community Edition": 8654, "jetbrains-appcode": 8655, "jetbrains-clion": 8657, "jetbrains-datagrip": 8664, From 7e90a2ee91bc6e9e54b76bd5b4d4b9274372bdc3 Mon Sep 17 00:00:00 2001 From: Jacob Egner Date: Fri, 20 Dec 2024 17:42:05 -0600 Subject: [PATCH 60/99] comment for Cursor mac bundle id (#1646) Looks like Cursor's bundle id of com.todesktop.230313mzl4w4u92 will not change: https://forum.cursor.com/t/cursor-bundle-identifier/779 --------- Co-authored-by: Jeff Knaus --- apps/vscode/vscode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index 245479423a..902b24c870 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -5,6 +5,7 @@ ctx = Context() mac_ctx = Context() mod = Module() +# com.todesktop.230313mzl4w4u92 is for Cursor - https://www.cursor.com/ mod.apps.vscode = """ os: mac and app.bundle: com.microsoft.VSCode From 45195f4e1acfa04210c41a1fd12554595e3c55f4 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 21 Dec 2024 18:28:43 +0100 Subject: [PATCH 61/99] Remove `/ 10` from the scroll speed calculation (#1642) Fixes #1605 --- plugin/mouse/mouse_scroll.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/mouse/mouse_scroll.py b/plugin/mouse/mouse_scroll.py index 856445afea..6d79bcb0d5 100644 --- a/plugin/mouse/mouse_scroll.py +++ b/plugin/mouse/mouse_scroll.py @@ -31,7 +31,7 @@ mod.setting( "mouse_continuous_scroll_amount", type=int, - default=80, + default=8, desc="The default amount used when scrolling continuously", ) mod.setting( @@ -201,7 +201,7 @@ def scroll_continuous_helper(): if acceleration_setting > 1 else 1 ) - y = scroll_amount * acceleration_speed * scroll_dir / 10 + y = scroll_amount * acceleration_speed * scroll_dir actions.mouse_scroll(y) From 3e0ca4fe225734be2be160fae6a350aee4885de1 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 28 Dec 2024 18:22:36 +0100 Subject: [PATCH 62/99] Remove dependency on `talon_plugins` (#1657) `talon_plugins` has been removed in the latest beta. As far as I know there is no replacement to check if we are zoomed in or not. Fixes https://github.com/talonhub/community/issues/1637 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- BREAKING_CHANGES.txt | 1 + core/deprecations.py | 7 +++++-- plugin/mouse/mouse.py | 9 ++++++--- plugin/mouse/mouse.talon | 18 +++++++++--------- plugin/mouse/mouse_scroll.py | 7 ------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/BREAKING_CHANGES.txt b/BREAKING_CHANGES.txt index 5cd41b12c2..c3968715eb 100644 --- a/BREAKING_CHANGES.txt +++ b/BREAKING_CHANGES.txt @@ -7,6 +7,7 @@ and when the change was applied given the delay between changes being submitted and the time they were reviewed and merged. --- +* 2024-12-26 Deprecated action `user.zoom_close` in favor of `tracking.zoom_cancel`. * 2024-11-24 Deprecated a bunch of symbol commands to insert delimited pairs ("", '', []) in favor of the new `delimiter_pair` Talon list file. * 2024-09-07 Removed `get_list_from_csv` from `user_settings.py`. Please diff --git a/core/deprecations.py b/core/deprecations.py index 00abe45f97..9659a1d361 100644 --- a/core/deprecations.py +++ b/core/deprecations.py @@ -161,18 +161,21 @@ def deprecate_capture(time_deprecated: str, name: str): ) warnings.warn(msg, DeprecationWarning, stacklevel=3) - def deprecate_action(time_deprecated: str, name: str): + def deprecate_action(time_deprecated: str, name: str, replacement: str = ""): """ Notify the user that the given action is deprecated and should - not be used into the future. + not be used into the future; the action `replacement` should be used + instead. """ id = f"action.{name}.{time_deprecated}" deprecate_notify(id, f"The `{name}` action is deprecated. See log for more.") + replacement_msg = f' Instead, use: "{replacement}".' if replacement else "" msg = ( f"The `{name}` action is deprecated since {time_deprecated}." + f"{replacement_msg}" f' See {os.path.join(REPO_DIR, "BREAKING_CHANGES.txt")}' f"{calculate_rule_info()}" ) diff --git a/plugin/mouse/mouse.py b/plugin/mouse/mouse.py index 9e63cdf61c..48211abd04 100644 --- a/plugin/mouse/mouse.py +++ b/plugin/mouse/mouse.py @@ -1,5 +1,4 @@ from talon import Context, Module, actions, ctrl, settings, ui -from talon_plugins import eye_zoom_mouse mod = Module() ctx = Context() @@ -38,8 +37,12 @@ class Actions: def zoom_close(): """Closes an in-progress zoom. Talon will move the cursor position but not click.""" - if eye_zoom_mouse.zoom_mouse.state == eye_zoom_mouse.STATE_OVERLAY: - actions.tracking.zoom_cancel() + actions.user.deprecate_action( + "2024-12-26", + "user.zoom_close", + "tracking.zoom_cancel", + ) + actions.tracking.zoom_cancel() def mouse_wake(): """Enable control mouse, zoom mouse, and disables cursor""" diff --git a/plugin/mouse/mouse.talon b/plugin/mouse/mouse.talon index 10f277439d..974f8025ba 100644 --- a/plugin/mouse/mouse.talon +++ b/plugin/mouse/mouse.talon @@ -5,7 +5,7 @@ camera overlay: tracking.control_debug_toggle() run calibration: tracking.calibrate() touch: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() mouse_click(0) # close the mouse grid if open user.grid_close() @@ -15,14 +15,14 @@ touch: righty: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() mouse_click(1) # close the mouse grid if open user.grid_close() mid click: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() mouse_click(2) # close the mouse grid user.grid_close() @@ -36,7 +36,7 @@ mid click: #super = windows key touch: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() key("{modifiers}:down") mouse_click(0) key("{modifiers}:up") @@ -44,7 +44,7 @@ mid click: user.grid_close() righty: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() key("{modifiers}:down") mouse_click(1) key("{modifiers}:up") @@ -52,14 +52,14 @@ mid click: user.grid_close() (dub click | duke): # close zoom if open - user.zoom_close() + tracking.zoom_cancel() mouse_click() mouse_click() # close the mouse grid user.grid_close() (trip click | trip lick): # close zoom if open - user.zoom_close() + tracking.zoom_cancel() mouse_click() mouse_click() mouse_click() @@ -67,13 +67,13 @@ mid click: user.grid_close() left drag | drag | drag start: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() user.mouse_drag(0) # close the mouse grid user.grid_close() right drag | righty drag: # close zoom if open - user.zoom_close() + tracking.zoom_cancel() user.mouse_drag(1) # close the mouse grid user.grid_close() diff --git a/plugin/mouse/mouse_scroll.py b/plugin/mouse/mouse_scroll.py index 6d79bcb0d5..d2bce05818 100644 --- a/plugin/mouse/mouse_scroll.py +++ b/plugin/mouse/mouse_scroll.py @@ -2,7 +2,6 @@ from typing import Literal from talon import Context, Module, actions, app, cron, ctrl, imgui, settings, ui -from talon_plugins import eye_zoom_mouse continuous_scroll_mode = "" scroll_job = None @@ -96,9 +95,6 @@ def mouse_gaze_scroll(): """Starts gaze scroll""" global gaze_job, continuous_scroll_mode, control_mouse_forced - if eye_zoom_mouse.zoom_mouse.state != eye_zoom_mouse.STATE_IDLE: - return - continuous_scroll_mode = "gaze scroll" gaze_job = cron.interval("16ms", scroll_gaze_helper) @@ -169,9 +165,6 @@ def noise_trigger_hiss(active: bool): def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): global scroll_job, scroll_dir, scroll_start_ts, continuous_scroll_mode - if eye_zoom_mouse.zoom_mouse.state != eye_zoom_mouse.STATE_IDLE: - return - if scroll_job: # Issuing a scroll in the same direction aborts scrolling if scroll_dir == new_scroll_dir: From b1e697208a5db730c7f36ed406908fe22e88b06e Mon Sep 17 00:00:00 2001 From: FireChickenProductivity <107892169+FireChickenProductivity@users.noreply.github.com> Date: Sat, 28 Dec 2024 10:31:09 -0700 Subject: [PATCH 63/99] Migrate simple text navigation lists (#1656) This migrates the navigation actions and before or after lists to list files. This does not migrate the navigation target names list. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- plugin/text_navigation/before_or_after.talon-list | 4 ++++ plugin/text_navigation/navigation_action.talon-list | 9 +++++++++ plugin/text_navigation/text_navigation.py | 13 ------------- 3 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 plugin/text_navigation/before_or_after.talon-list create mode 100644 plugin/text_navigation/navigation_action.talon-list diff --git a/plugin/text_navigation/before_or_after.talon-list b/plugin/text_navigation/before_or_after.talon-list new file mode 100644 index 0000000000..7b8b6120ab --- /dev/null +++ b/plugin/text_navigation/before_or_after.talon-list @@ -0,0 +1,4 @@ +list: user.before_or_after +- +before: BEFORE +after: AFTER diff --git a/plugin/text_navigation/navigation_action.talon-list b/plugin/text_navigation/navigation_action.talon-list new file mode 100644 index 0000000000..c5cbfa8c72 --- /dev/null +++ b/plugin/text_navigation/navigation_action.talon-list @@ -0,0 +1,9 @@ +list: user.navigation_action +- + +move: GO +extend: EXTEND +select: SELECT +clear: DELETE +cut: CUT +copy: COPY diff --git a/plugin/text_navigation/text_navigation.py b/plugin/text_navigation/text_navigation.py index 050345c97c..eaf7417788 100644 --- a/plugin/text_navigation/text_navigation.py +++ b/plugin/text_navigation/text_navigation.py @@ -27,19 +27,6 @@ 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]+", From 25cf36af08984a74acf4b4127a521621de5bfafa Mon Sep 17 00:00:00 2001 From: FireChickenProductivity <107892169+FireChickenProductivity@users.noreply.github.com> Date: Sat, 28 Dec 2024 10:34:17 -0700 Subject: [PATCH 64/99] Migrate prose snippets (#1655) closes #1653 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- core/text/prose_snippets.talon-list | 11 +++++++++++ core/text/text_and_dictation.py | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 core/text/prose_snippets.talon-list diff --git a/core/text/prose_snippets.talon-list b/core/text/prose_snippets.talon-list new file mode 100644 index 0000000000..7a4113bc99 --- /dev/null +++ b/core/text/prose_snippets.talon-list @@ -0,0 +1,11 @@ +list: user.prose_snippets +- +spacebar: " " +new line: "\n" +new paragraph: "\n\n" +# Curly quotes are used to obtain proper spacing for left and right quotes, but will later be straightened. +open quote: "“" +close quote: "”" +smiley: :-) +winky: ;-) +frowny: :-( diff --git a/core/text/text_and_dictation.py b/core/text/text_and_dictation.py index 37909ad719..9467c25d1f 100644 --- a/core/text/text_and_dictation.py +++ b/core/text/text_and_dictation.py @@ -39,17 +39,6 @@ "no caps": "no_cap", # "no caps" variant for Dragon "no space": "no_space", } -ctx.lists["user.prose_snippets"] = { - "spacebar": " ", - "new line": "\n", - "new paragraph": "\n\n", - # Curly quotes are used to obtain proper spacing for left and right quotes, but will later be straightened. - "open quote": "“", - "close quote": "”", - "smiley": ":-)", - "winky": ";-)", - "frowny": ":-(", -} ctx.lists["user.hours_twelve"] = get_spoken_form_under_one_hundred( 1, From a0be06da02c007552e6b83e5d6e7f0dde528c9ee Mon Sep 17 00:00:00 2001 From: FireChickenProductivity <107892169+FireChickenProductivity@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:44:10 -0700 Subject: [PATCH 65/99] Migrate prose modifiers to talon list (#1654) closes #1651 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Andreas Arvidsson --- core/text/prose_modifiers.talon-list | 8 ++++++++ core/text/text_and_dictation.py | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 core/text/prose_modifiers.talon-list diff --git a/core/text/prose_modifiers.talon-list b/core/text/prose_modifiers.talon-list new file mode 100644 index 0000000000..256412d2ba --- /dev/null +++ b/core/text/prose_modifiers.talon-list @@ -0,0 +1,8 @@ +list: user.prose_modifiers +- +# Maps spoken forms to DictationFormat method names (see DictationFormat in text_and_dictation.py). +cap: cap +no cap: no_cap +# no caps variant for Dragon +no caps: no_cap +no space: no_space diff --git a/core/text/text_and_dictation.py b/core/text/text_and_dictation.py index 9467c25d1f..ea465e300f 100644 --- a/core/text/text_and_dictation.py +++ b/core/text/text_and_dictation.py @@ -32,14 +32,6 @@ speech.engine: dragon """ -# Maps spoken forms to DictationFormat method names (see DictationFormat below). -ctx.lists["user.prose_modifiers"] = { - "cap": "cap", - "no cap": "no_cap", - "no caps": "no_cap", # "no caps" variant for Dragon - "no space": "no_space", -} - ctx.lists["user.hours_twelve"] = get_spoken_form_under_one_hundred( 1, 12, From 9d3d41172ca12ae76f3751ee554853009f56e19b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 4 Jan 2025 04:58:43 +0100 Subject: [PATCH 66/99] Change mouse_continuous_scroll_amount setting (#1663) https://github.com/talonhub/community/pull/1642 changed the default in the python file, but we missed the default in the settings file --- settings.talon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.talon b/settings.talon index 3a8fba3ce4..d4dac902e5 100644 --- a/settings.talon +++ b/settings.talon @@ -22,7 +22,7 @@ settings(): # user.help_sort_contexts_by_specificity = false # Set the scroll amount for continuous scroll/gaze scroll - user.mouse_continuous_scroll_amount = 80 + user.mouse_continuous_scroll_amount = 8 # Set the maximum acceleration factor when scrolling continuously. 1=constant speed/no acceleration. user.mouse_continuous_scroll_acceleration = 1 From 8e17a3221a68d805fa8fecc5656c969b55e2e031 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Sat, 4 Jan 2025 18:50:11 +0200 Subject: [PATCH 67/99] Update `formatters.py` reference (#1664) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94d1cf0eff..b03ab280ba 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Prose formatters (marked with \* in the help window) preserve hyphens and apostr Reformat existing text with one or more formatters by selecting it, then saying the formatter name(s) followed by `that`. Say `help reformat` to display how each formatter reformats `one_two_three`. -Formatter names (snake, dubstring) are defined [here](core/text/formatters.py#L245). Formatter-related commands are defined in [text.talon](core/text/text.talon#L8). +Formatter names (snake, dubstring) are defined [here](core/formatters/formatters.py#L245). Formatter-related commands are defined in [text.talon](core/text/text.talon#L8). ### Mouse commands @@ -144,7 +144,7 @@ Say `go up fifth` or `go up five times` to go up five lines. `select up third` w ### Window management -Global window managment commands are defined in [window_management.talon](core/windows_and_tabs/window_management.talon). +Global window management commands are defined in [window_management.talon](core/windows_and_tabs/window_management.talon). - `running list` toggles a window displaying words you can say to switch to running applications. To customize the spoken forms for an app (or hide an app entirely from the list), edit the `app_name_overrides_.csv` files in the [core/app_switcher](core/app_switcher) directory. - `focus chrome` will focus the Chrome application. From 01f626ea097c3464d969cc69e360791ffdb494f8 Mon Sep 17 00:00:00 2001 From: Jeff Knaus Date: Sat, 4 Jan 2025 10:15:04 -0700 Subject: [PATCH 68/99] Rename edit_text_file.talon-list to edit_text_file_list.talon-list (#1659) Fix for public release of talon: .talon-lists can't have the same name as a .talon file at the moment Co-authored-by: Nicholas Riley --- .../{edit_text_file.talon-list => edit_text_file_list.talon-list} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename core/edit_text_file/{edit_text_file.talon-list => edit_text_file_list.talon-list} (100%) diff --git a/core/edit_text_file/edit_text_file.talon-list b/core/edit_text_file/edit_text_file_list.talon-list similarity index 100% rename from core/edit_text_file/edit_text_file.talon-list rename to core/edit_text_file/edit_text_file_list.talon-list From b34ceea8f2faa5cb8982cb56f1c941122a257d85 Mon Sep 17 00:00:00 2001 From: FireChickenProductivity <107892169+FireChickenProductivity@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:30:15 -0700 Subject: [PATCH 69/99] Update comments referring to community as Knausj (#1666) closes #1665 --- apps/wsl/wsl.py | 2 +- core/user_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wsl/wsl.py b/apps/wsl/wsl.py index 5cea9511a3..ebc3ac7da7 100644 --- a/apps/wsl/wsl.py +++ b/apps/wsl/wsl.py @@ -310,7 +310,7 @@ def run_wslpath(args, in_path, in_distro=None): # Once the WSL distro is hung, every attempt to use it results in many repeated log messages like these: # # 2021-10-15 11:15:49 WARNING [watchdog] "talon.windows.ui._on_event" @30.0s (stalled) -# 2021-10-15 11:15:49 WARNING [watchdog] "user.knausj_talon.code.file_manager.win_event_handler" +# 2021-10-15 11:15:49 WARNING [watchdog] "user.community.code.file_manager.win_event_handler" # # These messages are from code used to detect the current path from the window title, and it every time the # focus shifts to a wsl context or the current path changes. This gets tiresome if you don't want to restart diff --git a/core/user_settings.py b/core/user_settings.py index 6e08cfe035..b1369c820f 100644 --- a/core/user_settings.py +++ b/core/user_settings.py @@ -6,7 +6,7 @@ from talon import resource # NOTE: This method requires this module to be one folder below the top-level -# community/knausj folder. +# community folder. SETTINGS_DIR = Path(__file__).parents[1] / "settings" SETTINGS_DIR.mkdir(exist_ok=True) From eed33df4a9d64be1f7e3de5665160668b61524fb Mon Sep 17 00:00:00 2001 From: Timo <24251362+timo95@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:50:39 +0100 Subject: [PATCH 70/99] jetbrains: Fix exception type error (#1667) Fixes TypeError: argument 'title': 'FileNotFoundError' object cannot be converted to 'PyString' --- apps/jetbrains/jetbrains.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/jetbrains/jetbrains.py b/apps/jetbrains/jetbrains.py index b76cc63e9b..b424b4269d 100644 --- a/apps/jetbrains/jetbrains.py +++ b/apps/jetbrains/jetbrains.py @@ -143,7 +143,7 @@ def idea(commands: str): send_idea_command(cmd.strip()) actions.sleep(0.1) except Exception as e: - app.notify(e) + app.notify(str(e)) raise def idea_grab(times: int): From e2886aa2789f8958dc925b4bf9fad38f2e909e5d Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Wed, 8 Jan 2025 07:27:19 +0000 Subject: [PATCH 71/99] Move command_client repo (#1669) We moved the deployed repo for people not on community into the `cursorless-dev` organization; updating deploy instructions. We also renamed `master` to `main` to be consistent with our other repos --- apps/vscode/command_client/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/vscode/command_client/README.md b/apps/vscode/command_client/README.md index 844d5849d2..44d2a76ddb 100644 --- a/apps/vscode/command_client/README.md +++ b/apps/vscode/command_client/README.md @@ -4,7 +4,7 @@ This directory contains the client code for communicating with the [VSCode comma ## Contributing -The source of truth is in https://github.com/talonhub/community/tree/main/apps/vscode/command_client, but the code is also maintained as a subtree at https://github.com/pokey/talon-vscode-command-client. +The source of truth is in https://github.com/talonhub/community/tree/main/apps/vscode/command_client, but the code is also maintained as a subtree at https://github.com/cursorless-dev/talon-vscode-command-client. To contribute, first open a PR on `community`. @@ -12,11 +12,11 @@ Once the PR is merged, you can push the changes to the subtree by running the fo ```sh git subtree split --prefix=apps/vscode/command_client --annotate="[split] " -b split -git push talon-vscode-command-client split:master +git push talon-vscode-command-client split:main ``` Note that you'll need to have set the upstream up the first time: ```sh -git remote add talon-vscode-command-client git@github.com:pokey/talon-vscode-command-client.git +git remote add talon-vscode-command-client git@github.com:cursorless-dev/talon-vscode-command-client.git ``` From 556ebc8e9c7f0b703f1b0717483d5ca5ad0e9883 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 8 Jan 2025 16:57:45 +0100 Subject: [PATCH 72/99] Update command client readme with new repo name (#1672) --- apps/vscode/command_client/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/vscode/command_client/README.md b/apps/vscode/command_client/README.md index 44d2a76ddb..e7678f190a 100644 --- a/apps/vscode/command_client/README.md +++ b/apps/vscode/command_client/README.md @@ -4,7 +4,7 @@ This directory contains the client code for communicating with the [VSCode comma ## Contributing -The source of truth is in https://github.com/talonhub/community/tree/main/apps/vscode/command_client, but the code is also maintained as a subtree at https://github.com/cursorless-dev/talon-vscode-command-client. +The source of truth is in https://github.com/talonhub/community/tree/main/apps/vscode/command_client, but the code is also maintained as a subtree at https://github.com/cursorless-dev/talon-command-client. To contribute, first open a PR on `community`. @@ -12,11 +12,11 @@ Once the PR is merged, you can push the changes to the subtree by running the fo ```sh git subtree split --prefix=apps/vscode/command_client --annotate="[split] " -b split -git push talon-vscode-command-client split:main +git push talon-command-client split:main ``` Note that you'll need to have set the upstream up the first time: ```sh -git remote add talon-vscode-command-client git@github.com:cursorless-dev/talon-vscode-command-client.git +git remote add talon-command-client git@github.com:cursorless-dev/talon-command-client.git ``` From 44027f9be254066d702a445e9a464075440c1970 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Wed, 8 Jan 2025 19:28:03 +0100 Subject: [PATCH 73/99] Simplify command client subtree push instructions (#1673) --- apps/vscode/command_client/README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/vscode/command_client/README.md b/apps/vscode/command_client/README.md index e7678f190a..33ecce11f5 100644 --- a/apps/vscode/command_client/README.md +++ b/apps/vscode/command_client/README.md @@ -12,11 +12,5 @@ Once the PR is merged, you can push the changes to the subtree by running the fo ```sh git subtree split --prefix=apps/vscode/command_client --annotate="[split] " -b split -git push talon-command-client split:main -``` - -Note that you'll need to have set the upstream up the first time: - -```sh -git remote add talon-command-client git@github.com:cursorless-dev/talon-command-client.git +git push git@github.com:cursorless-dev/talon-command-client.git split:main ``` From 90441da3686bf34e0a7fc5a9db025a34c3c15335 Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sat, 11 Jan 2025 12:13:55 -0500 Subject: [PATCH 74/99] Fix strings with invalid escapes that Python 3.13 doesn't like. (#1674) --- apps/kindle/kindle.py | 2 +- apps/nitro_reader/nitro_reader_5.py | 2 +- apps/sumatrapdf/sumatrapdf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/kindle/kindle.py b/apps/kindle/kindle.py index a43db7d949..95f31a52df 100644 --- a/apps/kindle/kindle.py +++ b/apps/kindle/kindle.py @@ -2,7 +2,7 @@ # --- App definition --- mod = Module() -mod.apps.kindle = """ +mod.apps.kindle = r""" os: windows and app.name: Kindle os: windows diff --git a/apps/nitro_reader/nitro_reader_5.py b/apps/nitro_reader/nitro_reader_5.py index 2828c56da7..25bff2de31 100644 --- a/apps/nitro_reader/nitro_reader_5.py +++ b/apps/nitro_reader/nitro_reader_5.py @@ -2,7 +2,7 @@ # --- App definition --- mod = Module() -mod.apps.nitro_reader_five = """ +mod.apps.nitro_reader_five = r""" os: windows and app.name: Nitro Reader 5 os: windows diff --git a/apps/sumatrapdf/sumatrapdf.py b/apps/sumatrapdf/sumatrapdf.py index 8dfd023c91..699f207c7f 100644 --- a/apps/sumatrapdf/sumatrapdf.py +++ b/apps/sumatrapdf/sumatrapdf.py @@ -2,7 +2,7 @@ # --- App definition --- mod = Module() -mod.apps.sumatrapdf = """ +mod.apps.sumatrapdf = r""" os: windows and app.name: SumatraPDF os: windows From 28af29e075ebff0f0b61ea1242b009c94f5a77d3 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 11 Jan 2025 18:15:19 +0100 Subject: [PATCH 75/99] Improve exception message when communication directory is not found (#1671) --- apps/vscode/command_client/rpc_client/rpc_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/command_client/rpc_client/rpc_client.py b/apps/vscode/command_client/rpc_client/rpc_client.py index 689e8d40f1..7e0ec29ffe 100644 --- a/apps/vscode/command_client/rpc_client/rpc_client.py +++ b/apps/vscode/command_client/rpc_client/rpc_client.py @@ -44,7 +44,7 @@ def rpc_client_run_command( if not communication_dir_path.exists(): if args or return_command_output: raise Exception( - "Must use command-server extension for advanced commands" + "Communication directory not found. Must use command-server extension for advanced commands" ) raise NoFileServerException("Communication directory not found") From 5a556d69367f479e6a3a1d21c79111f64c8e8172 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Sat, 11 Jan 2025 17:18:24 +0000 Subject: [PATCH 76/99] Move open volume command to file_manager_windows.talon (#1670) Moved the open volume command so that on windows it is enabled in all applications that enable the file_manager tag. I realise now that this could be implemented through the user.address capture, but implemented like this for now Also fixed powershell_win not being enabled in windows terminal. This means the file_manager tag is properly enabled in powershell, and so with this change powershell can now do `go drum`. --------- Co-authored-by: Nicholas Riley Co-authored-by: Andreas Arvidsson --- apps/powershell/powershell_win.talon | 3 +++ apps/windows_explorer/windows_explorer.talon | 1 - tags/file_manager/file_manager_win.talon | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 tags/file_manager/file_manager_win.talon diff --git a/apps/powershell/powershell_win.talon b/apps/powershell/powershell_win.talon index 5bd1742cc6..b0eabe08c4 100644 --- a/apps/powershell/powershell_win.talon +++ b/apps/powershell/powershell_win.talon @@ -1,6 +1,9 @@ os: windows and app.name: Windows PowerShell os: windows +app: windows_terminal +and win.title: /PowerShell/ +os: windows and app.exe: powershell.exe - # makes the commands in terminal.talon available diff --git a/apps/windows_explorer/windows_explorer.talon b/apps/windows_explorer/windows_explorer.talon index 503a158e4c..28d83c0e7b 100644 --- a/apps/windows_explorer/windows_explorer.talon +++ b/apps/windows_explorer/windows_explorer.talon @@ -5,6 +5,5 @@ tag(): user.address tag(): user.file_manager tag(): user.navigation -^go $: user.file_manager_open_volume("{letter}:") go app data: user.file_manager_open_directory("%AppData%") go program files: user.file_manager_open_directory("%programfiles%") diff --git a/tags/file_manager/file_manager_win.talon b/tags/file_manager/file_manager_win.talon new file mode 100644 index 0000000000..be00c8c0cc --- /dev/null +++ b/tags/file_manager/file_manager_win.talon @@ -0,0 +1,4 @@ +tag: user.file_manager +os: windows +- +^go {user.letter}$: user.file_manager_open_volume("{letter}:\\") From 23290e9cc4182d55a8d1d0cf1151b0eb4a9f02d6 Mon Sep 17 00:00:00 2001 From: Aodhagan <59316063+Aodhagan@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:19:39 +0000 Subject: [PATCH 77/99] Add positron vscode support (#1668) Enables Cursorless to work immediately in a VSCode fork called Positron (Data Science focused IDE). --------- Co-authored-by: Phil Cohen --- apps/vscode/vscode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index 902b24c870..5a012899b1 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -49,6 +49,8 @@ and app.name: Azure Data Studio os: windows and app.exe: /^azuredatastudio\.exe$/i +os: windows +and app.exe: positron.exe """ ctx.matches = r""" From 5c98848193b805e03837e7bee36d49b0da4f62c5 Mon Sep 17 00:00:00 2001 From: Schwa Aresty Date: Sat, 11 Jan 2025 09:27:12 -0800 Subject: [PATCH 78/99] Allow Passing Window to Snap User Actions and Introduce Configurable Snap Name List (#1629) ## Background: We want to be able to call these snap positions from another module (#1578), but the existing action takes a spoken form. We'd rather not build a dependency on those, because they can be brittle if the user changes them; it's become a best practice recently (formatters, etc) to use static identifiers instead, and use a Talon list to provide the spoken form. ## Change: - This change changes the keys of the snap positions dictionary to have static identifiers, instead of the previous spoken forms. - A talon list is introduced to map the spoken forms to the new identifiers. - To reduce user friction, as people are probably calling the existing action with a spoken form, we'll attempt to convert it to the new identifier if we can, to allow the action to succeed, although we will still show a deprecation warning so they can update their code. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- BREAKING_CHANGES.txt | 1 + core/deprecations.py | 1 + core/windows_and_tabs/window_snap.py | 141 +++++++++++------- .../window_snap_positions.talon-list | 44 ++++++ 4 files changed, 129 insertions(+), 58 deletions(-) create mode 100644 core/windows_and_tabs/window_snap_positions.talon-list diff --git a/BREAKING_CHANGES.txt b/BREAKING_CHANGES.txt index c3968715eb..462bd25265 100644 --- a/BREAKING_CHANGES.txt +++ b/BREAKING_CHANGES.txt @@ -7,6 +7,7 @@ and when the change was applied given the delay between changes being submitted and the time they were reviewed and merged. --- +* 2024-12-3 Introduced an intermediate layer for naming snap window positions instead of using the raw spoken forms. Instead of calling snap_window_to_position("top right") you should now call snap_window_to_position("TOP_RIGHT") * 2024-12-26 Deprecated action `user.zoom_close` in favor of `tracking.zoom_cancel`. * 2024-11-24 Deprecated a bunch of symbol commands to insert delimited pairs ("", '', []) in favor of the new `delimiter_pair` Talon list file. diff --git a/core/deprecations.py b/core/deprecations.py index 9659a1d361..4de08835a4 100644 --- a/core/deprecations.py +++ b/core/deprecations.py @@ -52,6 +52,7 @@ def legacy_capture(m) -> str: import datetime import os.path import warnings +from typing import Optional from talon import Module, actions, settings, speech_system diff --git a/core/windows_and_tabs/window_snap.py b/core/windows_and_tabs/window_snap.py index f23b26f240..44e729edf2 100644 --- a/core/windows_and_tabs/window_snap.py +++ b/core/windows_and_tabs/window_snap.py @@ -11,6 +11,7 @@ from typing import Dict, Optional from talon import Context, Module, actions, settings, ui +from talon.ui import Window mod = Module() mod.list( @@ -209,91 +210,93 @@ def __init__(self, left, top, right, bottom): self.bottom = bottom self.right = right + def __str__(self): + return f"RelativeScreenPos(left={self.left}, top={self.top}, right={self.right}, bottom={self.bottom})" + _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), + "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), + "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), # Alternate (simpler) spoken forms for thirds - "center small": RelativeScreenPos(1 / 3, 0, 2 / 3, 1), - "left small": RelativeScreenPos(0, 0, 1 / 3, 1), - "right small": RelativeScreenPos(2 / 3, 0, 1, 1), - "left large": RelativeScreenPos(0, 0, 2 / 3, 1), - "right large": RelativeScreenPos(1 / 3, 0, 1, 1), + "CENTER_SMALL": RelativeScreenPos(1 / 3, 0, 2 / 3, 1), + "LEFT_SMALL": RelativeScreenPos(0, 0, 1 / 3, 1), + "RIGHT_SMALL": RelativeScreenPos(2 / 3, 0, 1, 1), + "LEFT_LARGE": RelativeScreenPos(0, 0, 2 / 3, 1), + "RIGHT_LARGE": 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), + "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 left third": RelativeScreenPos(0, 0, 1 / 3, 0.5), - "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), + "TOP_LEFT_THIRD": RelativeScreenPos(0, 0, 1 / 3, 0.5), + "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), # Alternate (simpler) spoken forms for sixths - "top left small": RelativeScreenPos(0, 0, 1 / 3, 0.5), - "top right small": RelativeScreenPos(2 / 3, 0, 1, 0.5), - "top left large": RelativeScreenPos(0, 0, 2 / 3, 0.5), - "top right large": RelativeScreenPos(1 / 3, 0, 1, 0.5), - "top center small": RelativeScreenPos(1 / 3, 0, 2 / 3, 0.5), - "bottom left small": RelativeScreenPos(0, 0.5, 1 / 3, 1), - "bottom right small": RelativeScreenPos(2 / 3, 0.5, 1, 1), - "bottom left large": RelativeScreenPos(0, 0.5, 2 / 3, 1), - "bottom right large": RelativeScreenPos(1 / 3, 0.5, 1, 1), - "bottom center small": RelativeScreenPos(1 / 3, 0.5, 2 / 3, 1), + "TOP_LEFT_SMALL": RelativeScreenPos(0, 0, 1 / 3, 0.5), + "TOP_RIGHT_SMALL": RelativeScreenPos(2 / 3, 0, 1, 0.5), + "TOP_LEFT_LARGE": RelativeScreenPos(0, 0, 2 / 3, 0.5), + "TOP_RIGHT_LARGE": RelativeScreenPos(1 / 3, 0, 1, 0.5), + "TOP_CENTER_SMALL": RelativeScreenPos(1 / 3, 0, 2 / 3, 0.5), + "BOTTOM_LEFT_SMALL": RelativeScreenPos(0, 0.5, 1 / 3, 1), + "BOTTOM_RIGHT_SMALL": RelativeScreenPos(2 / 3, 0.5, 1, 1), + "BOTTOM_LEFT_LARGE": RelativeScreenPos(0, 0.5, 2 / 3, 1), + "BOTTOM_RIGHT_LARGE": RelativeScreenPos(1 / 3, 0.5, 1, 1), + "BOTTOM_CENTER_SMALL": 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), + "CENTER": RelativeScreenPos(1 / 8, 1 / 6, 7 / 8, 5 / 6), + "FULL": RelativeScreenPos(0, 0, 1, 1), + "FULLSCREEN": RelativeScreenPos(0, 0, 1, 1), } - _split_positions = { "split": { - 2: [_snap_positions["left"], _snap_positions["right"]], + 2: [_snap_positions["LEFT"], _snap_positions["RIGHT"]], 3: [ - _snap_positions["left third"], - _snap_positions["center third"], - _snap_positions["right third"], + _snap_positions["LEFT_THIRD"], + _snap_positions["CENTER_THIRD"], + _snap_positions["RIGHT_THIRD"], ], }, "clock": { 3: [ - _snap_positions["left"], - _snap_positions["top right"], - _snap_positions["bottom right"], + _snap_positions["LEFT"], + _snap_positions["TOP_RIGHT"], + _snap_positions["BOTTOM_RIGHT"], ], }, "counterclock": { 3: [ - _snap_positions["right"], - _snap_positions["top left"], - _snap_positions["bottom left"], + _snap_positions["RIGHT"], + _snap_positions["TOP_LEFT"], + _snap_positions["BOTTOM_LEFT"], ], }, } @@ -316,13 +319,35 @@ def window_split_position(m) -> Dict[int, list[RelativeScreenPos]]: @mod.action_class class Actions: - def snap_window(position: RelativeScreenPos) -> None: - """Move the active window to a specific position on its current screen, given a `RelativeScreenPos` object.""" - _snap_window_helper(ui.active_window(), position) + def snap_window( + position: RelativeScreenPos, window: Optional[Window] = None + ) -> None: + """Move a window (defaults to the active window) to a specific position on its current screen, given a `RelativeScreenPos` object.""" + if window is None: + window = ui.active_window() + _snap_window_helper(window, position) - def snap_window_to_position(position_name: str) -> None: - """Move the active window to a specifically named position on its current screen, using a key from `_snap_positions`.""" - actions.user.snap_window(_snap_positions[position_name]) + def snap_window_to_position( + position_name: str, window: Optional[Window] = None + ) -> None: + """Move a window (defaults to the active window) to a specifically named position on its current screen, using a key from `_snap_positions`.""" + position: Optional[RelativeScreenPos] = None + if position_name in _snap_positions: + position = _snap_positions[position_name] + else: + # Previously this function took a spoken form, but we now have constant identifiers in `_snap_positions`. + # If the user passed a previous spoken form instead, see if we can convert it to the new identifier. + new_key = actions.user.formatted_text(position_name, "ALL_CAPS,SNAKE_CASE") + if new_key in _snap_positions: + actions.user.deprecate_action( + "2024-12-02", + f"snap_window_to_position('{position_name}')", + f"snap_window_to_position('{new_key}')", + ) + position = _snap_positions[new_key] + actions.user.snap_window(position, window) + else: + raise KeyError(position_name) def move_window_next_screen() -> None: """Move the active window to a specific screen.""" diff --git a/core/windows_and_tabs/window_snap_positions.talon-list b/core/windows_and_tabs/window_snap_positions.talon-list new file mode 100644 index 0000000000..959f0552d5 --- /dev/null +++ b/core/windows_and_tabs/window_snap_positions.talon-list @@ -0,0 +1,44 @@ +list: user.window_snap_positions +- + +left: LEFT +right: RIGHT +top: TOP +bottom: BOTTOM +center third: CENTER_THIRD +left third: LEFT_THIRD +right third: RIGHT_THIRD +left two thirds: LEFT_TWO_THIRDS +right two thirds: RIGHT_TWO_THIRDS +center small: CENTER_SMALL +left small: LEFT_SMALL +right small: RIGHT_SMALL +left large: LEFT_LARGE +right large: RIGHT_LARGE +top left: TOP_LEFT +top right: TOP_RIGHT +bottom left: BOTTOM_LEFT +bottom right: BOTTOM_RIGHT +top left third: TOP_LEFT_THIRD +top right third: TOP_RIGHT_THIRD +top left two thirds: TOP_LEFT_TWO_THIRDS +top right two thirds: TOP_RIGHT_TWO_THIRDS +top center third: TOP_CENTER_THIRD +bottom left third: BOTTOM_LEFT_THIRD +bottom right third: BOTTOM_RIGHT_THIRD +bottom left two thirds: BOTTOM_LEFT_TWO_THIRDS +bottom right two thirds: BOTTOM_RIGHT_TWO_THIRDS +bottom center third: BOTTOM_CENTER_THIRD +top left small: TOP_LEFT_SMALL +top right small: TOP_RIGHT_SMALL +top left large: TOP_LEFT_LARGE +top right large: TOP_RIGHT_LARGE +top center small: TOP_CENTER_SMALL +bottom left small: BOTTOM_LEFT_SMALL +bottom right small: BOTTOM_RIGHT_SMALL +bottom left large: BOTTOM_LEFT_LARGE +bottom right large: BOTTOM_RIGHT_LARGE +bottom center small: BOTTOM_CENTER_SMALL +center: CENTER +full: FULL +fullscreen: FULLSCREEN From eddcea78797a95fdc680c1235156f64f116f2e5b Mon Sep 17 00:00:00 2001 From: Silico_Biomancer Date: Sun, 12 Jan 2025 20:53:16 +1300 Subject: [PATCH 79/99] Add comma-separated list formatter (#1645) Co-authored-by: bluedrink9 Co-authored-by: bluedrink9 --- core/formatters/code_formatter.talon-list | 1 + core/formatters/reformatter.talon-list | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/core/formatters/code_formatter.talon-list b/core/formatters/code_formatter.talon-list index 224099d26b..aa02462eb8 100644 --- a/core/formatters/code_formatter.talon-list +++ b/core/formatters/code_formatter.talon-list @@ -4,6 +4,7 @@ all cap: ALL_CAPS all down: ALL_LOWERCASE camel: PRIVATE_CAMEL_CASE dotted: DOT_SEPARATED +list: COMMA_SEPARATED dub string: DOUBLE_QUOTED_STRING dunder: DOUBLE_UNDERSCORE hammer: PUBLIC_CAMEL_CASE diff --git a/core/formatters/reformatter.talon-list b/core/formatters/reformatter.talon-list index 0e63755f5c..712ede5b0b 100644 --- a/core/formatters/reformatter.talon-list +++ b/core/formatters/reformatter.talon-list @@ -1,5 +1,4 @@ list: user.reformatter - cap: CAPITALIZE -list: COMMA_SEPARATED unformat: REMOVE_FORMATTING From 5c98085631729d7a59095be29d1e6cfd25a41dba Mon Sep 17 00:00:00 2001 From: Nicholas Riley Date: Sun, 12 Jan 2025 13:29:24 -0500 Subject: [PATCH 80/99] Avoid race reading user.draft_editor setting at startup. (#1681) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current Talon beta raises an exception rather than returning `None` if you try to read a setting that isn't declared yet. This is intentional behavior — it exposes bugs like this one. Also fixes `user.draft_editor*` tags not being set at startup. --- plugin/draft_editor/draft_editor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugin/draft_editor/draft_editor.py b/plugin/draft_editor/draft_editor.py index 87f1b1845e..33994f93c4 100644 --- a/plugin/draft_editor/draft_editor.py +++ b/plugin/draft_editor/draft_editor.py @@ -58,10 +58,16 @@ def handle_app_activate(app): remove_tag("user.draft_editor_app_focused") -ui.register("app_launch", handle_app_running) -ui.register("app_close", handle_app_running) -ui.register("app_activate", handle_app_activate) +def on_ready(): + ui.register("app_launch", handle_app_running) + ui.register("app_close", handle_app_running) + ui.register("app_activate", handle_app_activate) + handle_app_running(None) + handle_app_activate(ui.active_app()) + + +app.register("ready", on_ready) original_window = None From d79b05168145f75b02a2d9c62dfc867672a32f46 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 12 Jan 2025 21:28:00 +0100 Subject: [PATCH 81/99] Cleaned up visual studio files (#1679) The Vishal studio files was a bit weird with dupduplicatest context definitions and unnecessary operating system requirements. Once I was there I also removed some unnecessary comments and dead code. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- apps/visualstudio/visual_studio.py | 26 ++------------------------ apps/visualstudio/visual_studio.talon | 2 -- apps/visualstudio/visual_studio_win.py | 3 --- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/apps/visualstudio/visual_studio.py b/apps/visualstudio/visual_studio.py index 7362e8b335..e8a80e2b5a 100644 --- a/apps/visualstudio/visual_studio.py +++ b/apps/visualstudio/visual_studio.py @@ -10,13 +10,10 @@ from talon import Context, Module, actions -# is_mac = app.platform == "mac" - -ctx = Context() mod = Module() +ctx = Context() -apps = mod.apps -apps.visual_studio = """ +mod.apps.visual_studio = r""" os: windows and app.name: Microsoft Visual Studio 2022 os: windows @@ -25,23 +22,13 @@ and app.name: devenv.exe """ - -ctx.matches = r""" -app: visual_studio -""" - -from talon import Context, actions - -ctx = Context() ctx.matches = r""" -os: windows app: visual_studio """ @ctx.action_class("app") class AppActions: - # talon app actions def tab_close(): actions.key("ctrl-f4") @@ -57,14 +44,12 @@ def tab_reopen(): @ctx.action_class("code") class CodeActions: - # talon code actions def toggle_comment(): actions.key("ctrl-k ctrl-/") @ctx.action_class("edit") class EditActions: - # talon edit actions def indent_more(): actions.key("tab") @@ -100,16 +85,9 @@ def jump_line(n: int): class WinActions: def filename(): title = actions.win.title() - # this doesn't seem to be necessary on VSCode for Mac - # if title == "": - # title = ui.active_window().doc - result = title.split("-")[0].rstrip() - if "." in result: - # print(result) return result - return "" diff --git a/apps/visualstudio/visual_studio.talon b/apps/visualstudio/visual_studio.talon index 35545df816..cb450eec73 100644 --- a/apps/visualstudio/visual_studio.talon +++ b/apps/visualstudio/visual_studio.talon @@ -1,11 +1,9 @@ -os: windows app: visual_studio - tag(): user.tabs tag(): user.line_commands tag(): user.find_and_replace tag(): user.multiple_cursors -#multiple_cursor.py support end # Panels panel solution: key(ctrl-alt-l) diff --git a/apps/visualstudio/visual_studio_win.py b/apps/visualstudio/visual_studio_win.py index f2974284b9..c89a17c181 100644 --- a/apps/visualstudio/visual_studio_win.py +++ b/apps/visualstudio/visual_studio_win.py @@ -10,7 +10,6 @@ @ctx.action_class("app") class AppActions: - # talon app actions def tab_close(): actions.key("ctrl-f4") @@ -26,14 +25,12 @@ def tab_reopen(): @ctx.action_class("code") class CodeActions: - # talon code actions def toggle_comment(): actions.key("ctrl-k ctrl-/") @ctx.action_class("edit") class EditActions: - # talon edit actions def indent_more(): actions.key("tab") From 0f08fb83228bea6483e33f8e036d6801c67b91bd Mon Sep 17 00:00:00 2001 From: Silico_Biomancer Date: Mon, 13 Jan 2025 09:34:56 +1300 Subject: [PATCH 82/99] Add file save action via command pallete to vscode (#1676) Currently it uses the default ctrl+s, which is fine for a default setup but messes up if, for example, you use the vim extension. Not including the save command directly seems like an oversight, especially since the save_all command is right there --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jeff Knaus --- apps/vscode/vscode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index 5a012899b1..4345cbf89f 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -106,6 +106,9 @@ def indent_less(): def save_all(): actions.user.vscode("workbench.action.files.saveAll") + def save(): + actions.user.vscode("workbench.action.files.save") + def find_next(): actions.user.vscode("editor.action.nextMatchFindAction") From 2100ebc4e2e572e42a2de9d2d217c2004b874098 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Mon, 13 Jan 2025 10:10:23 +0100 Subject: [PATCH 83/99] Move command client into core folder (#1678) The reason the command client resides in the vscode folder is because it was designed for vscode originally, but now it's actually used for more applications so I'm moving it to the core folder instead. I will take care of the subtree. I have successfully run the split command on my computer. --- apps/visualstudio/visual_studio.py | 3 +++ apps/visualstudio/visual_studio.talon | 1 + apps/vscode/command_client/visual_studio.py | 15 --------------- .../vscode.py => vscode_command_client.py} | 4 ++-- {apps/vscode => core}/command_client/README.md | 6 +++--- .../command_client/command_client.py | 0 .../command_client/command_client_tag.py | 0 .../rpc_client/get_communication_dir_path.py | 0 .../rpc_client/read_json_with_timeout.py | 0 .../command_client/rpc_client/robust_unlink.py | 0 .../command_client/rpc_client/rpc_client.py | 0 .../command_client/rpc_client/types.py | 0 .../command_client/rpc_client/write_request.py | 0 13 files changed, 9 insertions(+), 20 deletions(-) delete mode 100644 apps/vscode/command_client/visual_studio.py rename apps/vscode/{command_client/vscode.py => vscode_command_client.py} (95%) rename {apps/vscode => core}/command_client/README.md (67%) rename {apps/vscode => core}/command_client/command_client.py (100%) rename {apps/vscode => core}/command_client/command_client_tag.py (100%) rename {apps/vscode => core}/command_client/rpc_client/get_communication_dir_path.py (100%) rename {apps/vscode => core}/command_client/rpc_client/read_json_with_timeout.py (100%) rename {apps/vscode => core}/command_client/rpc_client/robust_unlink.py (100%) rename {apps/vscode => core}/command_client/rpc_client/rpc_client.py (100%) rename {apps/vscode => core}/command_client/rpc_client/types.py (100%) rename {apps/vscode => core}/command_client/rpc_client/write_request.py (100%) diff --git a/apps/visualstudio/visual_studio.py b/apps/visualstudio/visual_studio.py index e8a80e2b5a..d587870b52 100644 --- a/apps/visualstudio/visual_studio.py +++ b/apps/visualstudio/visual_studio.py @@ -93,6 +93,9 @@ def filename(): @ctx.action_class("user") class UserActions: + def command_server_directory() -> str: + return "visual-studio-command-server" + # def select_word(verb: str): # actions.key("ctrl-w") # actions.user.perform_selection_action(verb) diff --git a/apps/visualstudio/visual_studio.talon b/apps/visualstudio/visual_studio.talon index cb450eec73..b37007fba7 100644 --- a/apps/visualstudio/visual_studio.talon +++ b/apps/visualstudio/visual_studio.talon @@ -4,6 +4,7 @@ tag(): user.tabs tag(): user.line_commands tag(): user.find_and_replace tag(): user.multiple_cursors +tag(): user.command_client # Panels panel solution: key(ctrl-alt-l) diff --git a/apps/vscode/command_client/visual_studio.py b/apps/vscode/command_client/visual_studio.py deleted file mode 100644 index f614d3c34e..0000000000 --- a/apps/vscode/command_client/visual_studio.py +++ /dev/null @@ -1,15 +0,0 @@ -from talon import Context - -ctx = Context() - -ctx.matches = r""" -app: visual_studio -""" - -ctx.tags = ["user.command_client"] - - -@ctx.action_class("user") -class VisualStudioActions: - def command_server_directory() -> str: - return "visual-studio-command-server" diff --git a/apps/vscode/command_client/vscode.py b/apps/vscode/vscode_command_client.py similarity index 95% rename from apps/vscode/command_client/vscode.py rename to apps/vscode/vscode_command_client.py index fce2814e73..1765e12a98 100644 --- a/apps/vscode/command_client/vscode.py +++ b/apps/vscode/vscode_command_client.py @@ -2,8 +2,8 @@ from talon import Context, Module, actions -from .command_client import NotSet, run_command -from .rpc_client.types import NoFileServerException +from ...core.command_client.command_client import NotSet, run_command +from ...core.command_client.rpc_client.types import NoFileServerException mod = Module() diff --git a/apps/vscode/command_client/README.md b/core/command_client/README.md similarity index 67% rename from apps/vscode/command_client/README.md rename to core/command_client/README.md index 33ecce11f5..9d3108ff0f 100644 --- a/apps/vscode/command_client/README.md +++ b/core/command_client/README.md @@ -1,16 +1,16 @@ -# Talon VSCode command client +# Talon command client This directory contains the client code for communicating with the [VSCode command server](https://marketplace.visualstudio.com/items?itemName=pokey.command-server). ## Contributing -The source of truth is in https://github.com/talonhub/community/tree/main/apps/vscode/command_client, but the code is also maintained as a subtree at https://github.com/cursorless-dev/talon-command-client. +The source of truth is in https://github.com/talonhub/community/tree/main/core/command_client, but the code is also maintained as a subtree at https://github.com/cursorless-dev/talon-command-client. To contribute, first open a PR on `community`. Once the PR is merged, you can push the changes to the subtree by running the following commands on an up-to-date `community` main: (need write access) ```sh -git subtree split --prefix=apps/vscode/command_client --annotate="[split] " -b split +git subtree split --prefix=core/command_client --annotate="[split] " -b split git push git@github.com:cursorless-dev/talon-command-client.git split:main ``` diff --git a/apps/vscode/command_client/command_client.py b/core/command_client/command_client.py similarity index 100% rename from apps/vscode/command_client/command_client.py rename to core/command_client/command_client.py diff --git a/apps/vscode/command_client/command_client_tag.py b/core/command_client/command_client_tag.py similarity index 100% rename from apps/vscode/command_client/command_client_tag.py rename to core/command_client/command_client_tag.py diff --git a/apps/vscode/command_client/rpc_client/get_communication_dir_path.py b/core/command_client/rpc_client/get_communication_dir_path.py similarity index 100% rename from apps/vscode/command_client/rpc_client/get_communication_dir_path.py rename to core/command_client/rpc_client/get_communication_dir_path.py diff --git a/apps/vscode/command_client/rpc_client/read_json_with_timeout.py b/core/command_client/rpc_client/read_json_with_timeout.py similarity index 100% rename from apps/vscode/command_client/rpc_client/read_json_with_timeout.py rename to core/command_client/rpc_client/read_json_with_timeout.py diff --git a/apps/vscode/command_client/rpc_client/robust_unlink.py b/core/command_client/rpc_client/robust_unlink.py similarity index 100% rename from apps/vscode/command_client/rpc_client/robust_unlink.py rename to core/command_client/rpc_client/robust_unlink.py diff --git a/apps/vscode/command_client/rpc_client/rpc_client.py b/core/command_client/rpc_client/rpc_client.py similarity index 100% rename from apps/vscode/command_client/rpc_client/rpc_client.py rename to core/command_client/rpc_client/rpc_client.py diff --git a/apps/vscode/command_client/rpc_client/types.py b/core/command_client/rpc_client/types.py similarity index 100% rename from apps/vscode/command_client/rpc_client/types.py rename to core/command_client/rpc_client/types.py diff --git a/apps/vscode/command_client/rpc_client/write_request.py b/core/command_client/rpc_client/write_request.py similarity index 100% rename from apps/vscode/command_client/rpc_client/write_request.py rename to core/command_client/rpc_client/write_request.py From cf5807dae6b963fc23cdb682a73a5b89a4250c53 Mon Sep 17 00:00:00 2001 From: Schwa Aresty Date: Wed, 15 Jan 2025 15:57:15 -0800 Subject: [PATCH 84/99] Bugfix for snap window (#1685) The snap command is not respecting the new talon list. It was skipping the actual snapping of the windows. This restores that behavior. fixes #1684 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- core/windows_and_tabs/window_snap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/windows_and_tabs/window_snap.py b/core/windows_and_tabs/window_snap.py index 44e729edf2..7b4fd19ed9 100644 --- a/core/windows_and_tabs/window_snap.py +++ b/core/windows_and_tabs/window_snap.py @@ -334,6 +334,7 @@ def snap_window_to_position( position: Optional[RelativeScreenPos] = None if position_name in _snap_positions: position = _snap_positions[position_name] + actions.user.snap_window(position, window) else: # Previously this function took a spoken form, but we now have constant identifiers in `_snap_positions`. # If the user passed a previous spoken form instead, see if we can convert it to the new identifier. From f34f73dafd94d04c1cbb5bc78df5c535f3dcc7f6 Mon Sep 17 00:00:00 2001 From: Silico_Biomancer Date: Sun, 19 Jan 2025 06:29:27 +1300 Subject: [PATCH 85/99] vscode: copy, paste, redo and undo through command palate (#1677) Makes these actions more reliable than the default of sending keypresses, especially if the user has rebound those keys or is in a different editor mode (eg with vim extension) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jeff Knaus Co-authored-by: Nicholas Riley --- apps/vscode/vscode.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index 4345cbf89f..588ef18023 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -96,6 +96,18 @@ def toggle_comment(): @ctx.action_class("edit") class EditActions: + def undo(): + actions.user.vscode("undo") + + def redo(): + actions.user.vscode("redo") + + def copy(): + actions.user.vscode("editor.action.clipboardCopyAction") + + def paste(): + actions.user.vscode("editor.action.clipboardPasteAction") + # talon edit actions def indent_more(): actions.user.vscode("editor.action.indentLines") From afe16aa789c75991466b8ca5254a0455ddaa0259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8e?= Date: Sat, 18 Jan 2025 18:30:54 +0100 Subject: [PATCH 86/99] add macos matchers for more jetbrains products. (#1688) add matchers for goland, intellij CE and local plugin development --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley --- apps/jetbrains/jetbrains.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/jetbrains/jetbrains.py b/apps/jetbrains/jetbrains.py index b424b4269d..0fde6204a1 100644 --- a/apps/jetbrains/jetbrains.py +++ b/apps/jetbrains/jetbrains.py @@ -55,6 +55,8 @@ "pycharm64.exe": 8658, "WebStorm": 8663, "webstorm64.exe": 8663, + # Local plugin development: + "com.jetbrains.jbr.java": 8666, } @@ -121,9 +123,19 @@ def get_idea_location() -> list[str]: mod.apps.jetbrains = """ os: mac and app.bundle: com.jetbrains.pycharm +""" +mod.apps.jetbrains = """ os: mac and app.bundle: com.jetbrains.rider """ +mod.apps.jetbrains = """ +os: mac +and app.bundle: com.jetbrains.goland +""" +mod.apps.jetbrains = """ +os: mac +and app.bundle: com.jetbrains.intellij.ce +""" mod.apps.jetbrains = r""" os: windows and app.name: JetBrains Rider @@ -131,6 +143,12 @@ def get_idea_location() -> list[str]: and app.exe: /^rider64\.exe$/i """ +# Local plugin development: +mod.apps.jetbrains = """ +os: mac +and app.bundle: com.jetbrains.jbr.java +""" + @mod.action_class class Actions: From d69d83a16aacc990d5b24781f8fa9b0c14f10911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8e?= Date: Sat, 18 Jan 2025 18:31:29 +0100 Subject: [PATCH 87/99] Improve kotlin - add types (#1689) --- lang/kotlin/kotlin.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lang/kotlin/kotlin.py b/lang/kotlin/kotlin.py index 5a3d84fa1b..aca6358c1e 100644 --- a/lang/kotlin/kotlin.py +++ b/lang/kotlin/kotlin.py @@ -23,6 +23,23 @@ "return": "return ", } +ctx.lists["user.code_type"] = { + "boolean": "Boolean", + "byte": "Byte", + "short": "Short", + "int": "Int", + "long": "Long", + "float": "Float", + "double": "Double", + "char": "Char", + "string": "String", + "array": "Array", + "map": "Map", + "any": "Any", + "nothing": "Nothing", + "unit": "Unit", +} + @ctx.action_class("user") class UserActions: From 72853e3e0d2722f81fd3312e9648c8fb0fbd70c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8e?= Date: Sat, 18 Jan 2025 18:51:14 +0100 Subject: [PATCH 88/99] Add cursorless support for jetbrains (#1628) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- apps/jetbrains/jetbrains.py | 5 +++++ apps/jetbrains/jetbrains.talon | 2 ++ 2 files changed, 7 insertions(+) diff --git a/apps/jetbrains/jetbrains.py b/apps/jetbrains/jetbrains.py index 0fde6204a1..d914d7e613 100644 --- a/apps/jetbrains/jetbrains.py +++ b/apps/jetbrains/jetbrains.py @@ -152,6 +152,7 @@ def get_idea_location() -> list[str]: @mod.action_class class Actions: + def idea(commands: str): """Send a command to Jetbrains product""" command_list = commands.split(",") @@ -298,6 +299,10 @@ def filename() -> str: @ctx.action_class("user") class UserActions: + + def command_server_directory() -> str: + return "jetbrains-command-server" + def tab_jump(number: int): # depends on plugin GoToTabs if number < 10: diff --git a/apps/jetbrains/jetbrains.talon b/apps/jetbrains/jetbrains.talon index 72db4a4542..c86fe35e4d 100644 --- a/apps/jetbrains/jetbrains.talon +++ b/apps/jetbrains/jetbrains.talon @@ -6,6 +6,8 @@ tag(): user.multiple_cursors tag(): user.splits tag(): user.tabs tag(): user.command_search +tag(): user.command_client + # multiple_cursors.py support end # Auto complete From d1c88553065782e0383253d2c8389ba3f258e17d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20S=C3=B8e?= Date: Sun, 19 Jan 2025 17:59:26 +0100 Subject: [PATCH 89/99] Resurrect go language support (#1690) Trying to keep it simple for a start --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley --- lang/go/go.py | 243 +++++++++++++++++++++++++++++++++++++++++++++++ lang/go/go.talon | 37 ++++++++ 2 files changed, 280 insertions(+) create mode 100644 lang/go/go.py create mode 100644 lang/go/go.talon diff --git a/lang/go/go.py b/lang/go/go.py new file mode 100644 index 0000000000..d3319d1cc2 --- /dev/null +++ b/lang/go/go.py @@ -0,0 +1,243 @@ +from talon import Context, Module, actions, settings + +ctx = Context() +mod = Module() +ctx.matches = r""" +code.language: go +""" + +# Primitive Types +ctx.lists["self.code_type"] = { + "boolean": "bool", + "int": "int", + "float": "float", + "byte": "byte", + "double": "double", + "short": "short", + "long": "long", + "char": "char", + "string": "string", + "rune": "rune", + "void": "void", + "channel": "channel", +} + +ctx.lists["user.code_keyword"] = { + "break": "break", + "continue": "continue", + "struct": "struct", + "type": "type", + "return": "return", + "package": "package", + "import": "import", + "null": "nil", + "nil": "nil", + "true": "true", + "false": "false", + "defer": "defer", + "go": "go", + "if": "if", + "else": "else", + "switch": "switch", + "select": "select", + "const": "const", +} + +ctx.lists["user.code_common_function"] = { + # golang buildin functions + "append": "append", + "length": "len", + "make": "make", + # formatting + "format print": "fmt.Printf", + "format sprint": "fmt.Sprintf", + "format print line": "fmt.Println", + # time + "time hour": "time.Hour", + "time minute": "time.Minute", + "time second": "time.Second", + "time millisecond": "time.Millisecond", + "time microsecond": "time.Microsecond", + "time nanosecond": "time.Nanosecond", + # IO + "buf I O": "bufio.", + # strings + "string convert": "strconv.", + "string convert to int": "strconv.AtoI", +} + + +@ctx.action_class("user") +class UserActions: + def code_operator_lambda(): + actions.insert(" -> ") + + def code_operator_subscript(): + actions.user.insert_between("[", "]") + + def code_operator_assignment(): + actions.insert(" = ") + + def code_operator_subtraction(): + actions.insert(" - ") + + def code_operator_subtraction_assignment(): + actions.insert(" -= ") + + def code_operator_addition(): + actions.insert(" + ") + + def code_operator_addition_assignment(): + actions.insert(" += ") + + def code_operator_multiplication(): + actions.insert(" * ") + + def code_operator_multiplication_assignment(): + actions.insert(" *= ") + + def code_operator_exponent(): + actions.insert(" ^ ") + + def code_operator_division(): + actions.insert(" / ") + + def code_operator_division_assignment(): + actions.insert(" /= ") + + def code_operator_modulo(): + actions.insert(" % ") + + def code_operator_modulo_assignment(): + actions.insert(" %= ") + + def code_operator_equal(): + actions.insert(" == ") + + def code_operator_not_equal(): + actions.insert(" != ") + + def code_operator_greater_than(): + actions.insert(" > ") + + def code_operator_greater_than_or_equal_to(): + actions.insert(" >= ") + + def code_operator_less_than(): + actions.insert(" < ") + + def code_operator_less_than_or_equal_to(): + actions.insert(" <= ") + + def code_operator_and(): + actions.insert(" && ") + + def code_operator_or(): + actions.insert(" || ") + + def code_operator_bitwise_and(): + actions.insert(" & ") + + def code_operator_bitwise_and_assignment(): + actions.insert(" &= ") + + def code_operator_increment(): + actions.insert("++") + + def code_operator_bitwise_or(): + actions.insert(" | ") + + def code_operator_bitwise_exclusive_or(): + actions.insert(" ^ ") + + def code_operator_bitwise_left_shift(): + actions.insert(" << ") + + def code_operator_bitwise_left_shift_assignment(): + actions.insert(" <<= ") + + def code_operator_bitwise_right_shift(): + actions.insert(" >> ") + + def code_operator_bitwise_right_shift_assignment(): + actions.insert(" >>= ") + + def code_operator_indirection(): + actions.insert("*") + + def code_operator_address_of(): + actions.insert("&") + + def code_self(): + actions.insert("this") + + def code_operator_object_accessor(): + actions.insert(".") + + def code_insert_null(): + actions.insert("nil") + + def code_insert_is_null(): + actions.insert(" == nil") + + def code_insert_is_not_null(): + actions.insert(" != nil") + + def code_state_if(): + actions.user.insert_between("if ", " ") + + def code_state_else_if(): + actions.user.insert_between("else if ", " ") + + def code_state_else(): + actions.insert("else ") + actions.key("enter") + + def code_state_switch(): + actions.user.insert_between("switch ", " ") + + def code_state_case(): + actions.user.insert_between("case ", ":") + + def code_state_for(): + actions.user.insert_between("for ", " ") + + # There is no while keyword in go. Closest approximation is a for loop. + def code_state_while(): + actions.user.insert_between("for ", " ") + + def code_break(): + actions.insert("break") + + def code_next(): + actions.insert("continue") + + def code_insert_true(): + actions.insert("true") + + def code_insert_false(): + actions.insert("false") + + def code_import(): + actions.insert("import ") + + def code_state_return(): + actions.insert("return ") + + def code_comment_line_prefix(): + actions.insert("// ") + + def code_insert_function(text: str, selection: str): + text += f"({selection or ''})" + actions.user.paste(text) + actions.edit.left() + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "func {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) diff --git a/lang/go/go.talon b/lang/go/go.talon new file mode 100644 index 0000000000..1305def37b --- /dev/null +++ b/lang/go/go.talon @@ -0,0 +1,37 @@ +code.language: go +- + +tag(): user.code_imperative + +tag(): user.code_comment_line +tag(): user.code_comment_block_c_like +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_functions +tag(): user.code_libraries +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_bitwise +tag(): user.code_operators_lambda +tag(): user.code_operators_math +tag(): user.code_operators_pointer + +settings(): + user.code_private_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_function_formatter = "PUBLIC_CAMEL_CASE" + user.code_private_variable_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_variable_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_variable_formatter = "PRIVATE_CAMEL_CASE" + +(variadic | spread): "..." +declare: " := " +channel (receive | send): " <- " + +[state] if (err | error): + insert("if err != nil {") + key("enter") + +[state] if not (err | error): + insert("if err == nil {") + key("enter") From 13ba0ede79867ced6d309eb32fff5a00db205af3 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 19 Jan 2025 18:08:17 +0100 Subject: [PATCH 90/99] Revert vscode clipboard actions from #1677 (#1693) Reverts #1677 These actions doesn't work in all scenarios. eg a save dialog. --- apps/vscode/vscode.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index 588ef18023..4345cbf89f 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -96,18 +96,6 @@ def toggle_comment(): @ctx.action_class("edit") class EditActions: - def undo(): - actions.user.vscode("undo") - - def redo(): - actions.user.vscode("redo") - - def copy(): - actions.user.vscode("editor.action.clipboardCopyAction") - - def paste(): - actions.user.vscode("editor.action.clipboardPasteAction") - # talon edit actions def indent_more(): actions.user.vscode("editor.action.indentLines") From cbde24e6752ed6f2ed742ef5d447ad61fbf24fce Mon Sep 17 00:00:00 2001 From: Gabriele Date: Thu, 23 Jan 2025 03:03:59 +0100 Subject: [PATCH 91/99] Add new bundle string for PhpStorm 2024.3.2 on Windows (#1698) --- apps/jetbrains/jetbrains.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/jetbrains/jetbrains.py b/apps/jetbrains/jetbrains.py index d914d7e613..1d0889b334 100644 --- a/apps/jetbrains/jetbrains.py +++ b/apps/jetbrains/jetbrains.py @@ -55,6 +55,7 @@ "pycharm64.exe": 8658, "WebStorm": 8663, "webstorm64.exe": 8663, + "PhpStorm": 8662, # Local plugin development: "com.jetbrains.jbr.java": 8666, } From c9a4c0987eddfca98eeba73834452d25dbbf0c23 Mon Sep 17 00:00:00 2001 From: Wen Kokke Date: Thu, 23 Jan 2025 02:04:20 +0000 Subject: [PATCH 92/99] feat: support VSCodium on macOS (#1699) --- apps/vscode/vscode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index 4345cbf89f..99b0f4be39 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -12,6 +12,8 @@ os: mac and app.bundle: com.microsoft.VSCodeInsiders os: mac +and app.bundle: com.vscodium +os: mac and app.bundle: com.visualstudio.code.oss os: mac and app.bundle: com.todesktop.230313mzl4w4u92 From 3161639d1e630a768db88b60b9ce6f9e9f8af046 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 24 Jan 2025 14:37:25 +0100 Subject: [PATCH 93/99] Fix not working commands in jetbrains.talon file (#1704) --- apps/jetbrains/jetbrains.talon | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/jetbrains/jetbrains.talon b/apps/jetbrains/jetbrains.talon index c86fe35e4d..86a9470966 100644 --- a/apps/jetbrains/jetbrains.talon +++ b/apps/jetbrains/jetbrains.talon @@ -60,7 +60,7 @@ find (everywhere | all) [over]: recent: user.idea("action RecentFiles") surround [this] with [over]: - idea("action SurroundWith") + user.idea("action SurroundWith") sleep(500ms) insert(text) # Making these longer to reduce collisions with real code dictation. @@ -69,7 +69,7 @@ insert generated [over]: sleep(500ms) insert(text) insert template [over]: - idea("action InsertLiveTemplate") + user.idea("action InsertLiveTemplate") sleep(500ms) insert(text) create (template | snippet): user.idea("action SaveAsTemplate") @@ -78,7 +78,7 @@ toggle recording: user.idea("action StartStopMacroRecording") change (recording | recordings): user.idea("action EditMacros") play recording: user.idea("action PlaybackLastMacro") play recording [over]: - idea("action PlaySavedMacrosAction") + user.idea("action PlaySavedMacrosAction") insert(text) sleep(500ms) Key("enter") From 1ec2afc60468f7f8212c129b04e7b974c54cb41b Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 25 Jan 2025 16:15:27 +0100 Subject: [PATCH 94/99] Fix bug in snippet parser line calculation (#1701) The trailing new line after the `---` separator was incorrectly counted resulting in an incorrect line number for error messages. Also remove unnecessary logic preventing error message about duplicate keys if those keys are invalid. Complicating the code just so we don't show a duplicate key and invalid key error isn't necessary. --------- Co-authored-by: Nicholas Riley --- core/snippets/snippets_parser.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/core/snippets/snippets_parser.py b/core/snippets/snippets_parser.py index ee22a41c14..6b5ce9b320 100644 --- a/core/snippets/snippets_parser.py +++ b/core/snippets/snippets_parser.py @@ -174,7 +174,7 @@ def parse_file(file_path: str) -> list[SnippetDocument]: def parse_file_content(file: str, text: str) -> list[SnippetDocument]: - doc_texts = re.split(r"^---$", text, flags=re.MULTILINE) + doc_texts = re.split(r"^---\n?$", text, flags=re.MULTILINE) documents: list[SnippetDocument] = [] line = 0 @@ -189,7 +189,10 @@ def parse_file_content(file: str, text: str) -> list[SnippetDocument]: def parse_document( - file: str, line: int, optional_body: bool, text: str + file: str, + line: int, + optional_body: bool, + text: str, ) -> Union[SnippetDocument, None]: parts = re.split(r"^-$", text, maxsplit=1, flags=re.MULTILINE) line_body = line + parts[0].count("\n") + 1 @@ -211,7 +214,10 @@ def parse_document( def parse_context( - file: str, line: int, document: SnippetDocument, text: str + file: str, + line: int, + document: SnippetDocument, + text: str, ) -> Union[SnippetDocument, None]: lines = [l.strip() for l in text.splitlines()] keys: set[str] = set() @@ -265,7 +271,7 @@ def parse_context_line( if key in keys: error(file, line, f"Duplicate key '{key}'") - valid_key = True + keys.add(key) match key: case "name": @@ -278,14 +284,9 @@ def parse_context_line( document.languages = parse_vector_value(value) case _: if key.startswith("$"): - if not parse_variable(file, line, get_variable, key, value): - valid_key = False + parse_variable(file, line, get_variable, key, value) else: error(file, line, f"Invalid key '{key}'") - valid_key = False - - if valid_key: - keys.add(key) def parse_variable( @@ -294,12 +295,12 @@ def parse_variable( get_variable: Callable[[str], SnippetVariable], key: str, value: str, -) -> bool: +): parts = key.split(".") if len(parts) != 2: error(file, line_numb, f"Invalid variable key '{key}'") - return False + return name = parts[0][1:] field = parts[1] @@ -313,9 +314,6 @@ def parse_variable( get_variable(name).wrapper_scope = value case _: error(file, line_numb, f"Invalid variable key '{key}'") - return False - - return True def parse_body(text: str) -> Union[str, None]: From 98a52992ab955fed03321696f5c378cf4ebaee65 Mon Sep 17 00:00:00 2001 From: James Stout Date: Sat, 25 Jan 2025 08:19:47 -0800 Subject: [PATCH 95/99] Added "check vocab" command and some other vocabulary improvements. (#1696) - Fixed duplicate handling. - Only escape written form when needed. Co-authored-by: Nicholas Riley --- core/vocabulary/edit_vocabulary.talon | 1 + core/vocabulary/vocabulary.py | 50 ++++++++++++++++----------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/core/vocabulary/edit_vocabulary.talon b/core/vocabulary/edit_vocabulary.talon index af01012a50..f85bae02e1 100644 --- a/core/vocabulary/edit_vocabulary.talon +++ b/core/vocabulary/edit_vocabulary.talon @@ -8,6 +8,7 @@ copy name to vocab [as ]$: # Automatically adds plural form by simply appending "s". copy noun to vocab [as ]$: user.add_selection_to_vocabulary(phrase or "", "noun") +check vocab: user.check_vocabulary_for_selection() copy to replacements as $: user.add_selection_to_words_to_replace(phrase) # Automatically adds possessive form by appending "'s". copy name to replacements as $: diff --git a/core/vocabulary/vocabulary.py b/core/vocabulary/vocabulary.py index 3792138ed1..b0c2152c96 100644 --- a/core/vocabulary/vocabulary.py +++ b/core/vocabulary/vocabulary.py @@ -196,24 +196,15 @@ def _add_selection_to_file( entries = _create_vocabulary_entries(spoken_form, written_form, type) added_some_phrases = False - # until we add support for parsing or otherwise getting the active - # vocabulary.talon-list, skip the logic for checking for duplicates etc - if file_contents: - # clear the new entries dictionary - new_entries = {} - for spoken_form, written_form in entries.items(): - if skip_identical_replacement and spoken_form == written_form: - actions.app.notify(f'Skipping identical replacement: "{spoken_form}"') - elif spoken_form in file_contents: - actions.app.notify( - f'Spoken form "{spoken_form}" is already in {file_name}' - ) - else: - new_entries[spoken_form] = written_form - added_some_phrases = True - else: - new_entries = entries - added_some_phrases = True + new_entries = {} + for spoken_form, written_form in entries.items(): + if skip_identical_replacement and spoken_form == written_form: + actions.app.notify(f'Skipping identical replacement: "{spoken_form}"') + elif spoken_form in file_contents: + actions.app.notify(f'Spoken form "{spoken_form}" is already in {file_name}') + else: + new_entries[spoken_form] = written_form + added_some_phrases = True if file_name.endswith(".csv"): append_to_csv(file_name, new_entries) @@ -239,7 +230,8 @@ def append_to_vocabulary(rows: dict[str, str]): if key == value: file.write(f"{key}\n") else: - value = repr(value) + if not str.isprintable(value) or "'" in value or '"' in value: + value = repr(value) file.write(f"{key}: {value}\n") @@ -262,7 +254,7 @@ def add_selection_to_vocabulary(phrase: Union[Phrase, str] = "", type: str = "") phrase, type, "vocabulary.talon-list", - None, + actions.user.talon_get_active_registry_list("user.vocabulary"), False, ) @@ -277,3 +269,21 @@ def add_selection_to_words_to_replace(phrase: Phrase, type: str = ""): phrases_to_replace, True, ) + + def check_vocabulary_for_selection(): + """Checks if the currently selected text is in the vocabulary.""" + text = actions.edit.selected_text().strip() + spoken_forms = [ + spoken + for spoken, written in actions.user.talon_get_active_registry_list( + "user.vocabulary" + ).items() + if text == written + ] + if spoken_forms: + if len(spoken_forms) == 1: + actions.app.notify(f'"{text}" is spoken as "{spoken_forms[0]}"') + else: + actions.app.notify(f'"{text}" is spoken as any of {spoken_forms}') + else: + actions.app.notify(f'"{text}" is not in the vocabulary') From 3b0c23843747a28670da95922e01327528ebc584 Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 25 Jan 2025 19:05:23 +0100 Subject: [PATCH 96/99] Refactor code operators (#1650) Fixes #1597 I think this is where we landed in our latest discussion. Please have a look and give feedback. I would also want a community user to test drive this thoroughly. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley Co-authored-by: Jeff Knaus Co-authored-by: David Vo --- BREAKING_CHANGES.txt | 1 + lang/java/java.py | 136 +++++---------- lang/tags/operators.py | 204 ++++++++++++++++++++++ lang/tags/operators.talon | 6 + lang/tags/operators_array.py | 5 +- lang/tags/operators_array.talon | 5 - lang/tags/operators_array.talon-list | 5 + lang/tags/operators_assignment.py | 5 +- lang/tags/operators_assignment.talon | 47 +++-- lang/tags/operators_assignment.talon-list | 22 +++ lang/tags/operators_bitwise.py | 5 +- lang/tags/operators_bitwise.talon | 26 ++- lang/tags/operators_bitwise.talon-list | 11 ++ lang/tags/operators_lambda.py | 9 +- lang/tags/operators_lambda.talon | 13 -- lang/tags/operators_lambda.talon-list | 5 + lang/tags/operators_math.py | 8 +- lang/tags/operators_math.talon | 78 +++++++-- lang/tags/operators_math.talon-list | 28 +++ lang/tags/operators_pointer.py | 5 +- lang/tags/operators_pointer.talon | 6 - lang/tags/operators_pointer.talon-list | 7 + 22 files changed, 451 insertions(+), 186 deletions(-) create mode 100644 lang/tags/operators.py create mode 100644 lang/tags/operators.talon delete mode 100644 lang/tags/operators_array.talon create mode 100644 lang/tags/operators_array.talon-list create mode 100644 lang/tags/operators_assignment.talon-list create mode 100644 lang/tags/operators_bitwise.talon-list delete mode 100644 lang/tags/operators_lambda.talon create mode 100644 lang/tags/operators_lambda.talon-list create mode 100644 lang/tags/operators_math.talon-list delete mode 100644 lang/tags/operators_pointer.talon create mode 100644 lang/tags/operators_pointer.talon-list diff --git a/BREAKING_CHANGES.txt b/BREAKING_CHANGES.txt index 462bd25265..1ae899c31f 100644 --- a/BREAKING_CHANGES.txt +++ b/BREAKING_CHANGES.txt @@ -7,6 +7,7 @@ and when the change was applied given the delay between changes being submitted and the time they were reviewed and merged. --- +* 2025-01-19 Deprecated a bunch of programming language operator commands in favor of using Talon lists * 2024-12-3 Introduced an intermediate layer for naming snap window positions instead of using the raw spoken forms. Instead of calling snap_window_to_position("top right") you should now call snap_window_to_position("TOP_RIGHT") * 2024-12-26 Deprecated action `user.zoom_close` in favor of `tracking.zoom_cancel`. * 2024-11-24 Deprecated a bunch of symbol commands to insert delimited pairs diff --git a/lang/java/java.py b/lang/java/java.py index e5a569f0d5..eeb0a6ff9a 100644 --- a/lang/java/java.py +++ b/lang/java/java.py @@ -1,5 +1,7 @@ from talon import Context, Module, actions, settings +from ..tags.operators import Operators + ctx = Context() mod = Module() ctx.matches = r""" @@ -87,101 +89,51 @@ mod.list("java_modifier", desc="Java Modifiers") ctx.lists["self.java_modifier"] = java_modifiers +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + # code_operators_bitwise + BITWISE_AND=" & ", + BITWISE_OR=" | ", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_RIGHT_SHIFT=" >> ", + # code_operators_lambda + LAMBDA=" -> ", + # code_operators_math + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EXPONENT=" ^ ", + MATH_EQUAL=" == ", + MATH_NOT_EQUAL=" != ", + MATH_GREATER_THAN=" > ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN=" < ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_AND=" && ", + MATH_OR=" || ", + MATH_NOT="!", +) + @ctx.action_class("user") class UserActions: - def code_operator_lambda(): - actions.insert(" -> ") - - def code_operator_subscript(): - actions.user.insert_between("[", "]") - - def code_operator_assignment(): - actions.insert(" = ") - - def code_operator_subtraction(): - actions.insert(" - ") - - def code_operator_subtraction_assignment(): - actions.insert(" -= ") - - def code_operator_addition(): - actions.insert(" + ") - - def code_operator_addition_assignment(): - actions.insert(" += ") - - def code_operator_multiplication(): - actions.insert(" * ") - - def code_operator_multiplication_assignment(): - actions.insert(" *= ") - - def code_operator_exponent(): - actions.insert(" ^ ") - - def code_operator_division(): - actions.insert(" / ") - - def code_operator_division_assignment(): - actions.insert(" /= ") - - def code_operator_modulo(): - actions.insert(" % ") - - def code_operator_modulo_assignment(): - actions.insert(" %= ") - - def code_operator_equal(): - actions.insert(" == ") - - def code_operator_not_equal(): - actions.insert(" != ") - - def code_operator_greater_than(): - actions.insert(" > ") - - def code_operator_greater_than_or_equal_to(): - actions.insert(" >= ") - - def code_operator_less_than(): - actions.insert(" < ") - - def code_operator_less_than_or_equal_to(): - actions.insert(" <= ") - - def code_operator_and(): - actions.insert(" && ") - - def code_operator_or(): - actions.insert(" || ") - - def code_operator_bitwise_and(): - actions.insert(" & ") - - def code_operator_bitwise_and_assignment(): - actions.insert(" &= ") - - def code_operator_increment(): - actions.insert("++") - - def code_operator_bitwise_or(): - actions.insert(" | ") - - def code_operator_bitwise_exclusive_or(): - actions.insert(" ^ ") - - def code_operator_bitwise_left_shift(): - actions.insert(" << ") - - def code_operator_bitwise_left_shift_assignment(): - actions.insert(" <<= ") - - def code_operator_bitwise_right_shift(): - actions.insert(" >> ") - - def code_operator_bitwise_right_shift_assignment(): - actions.insert(" >>= ") + def code_get_operators() -> Operators: + return operators def code_self(): actions.insert("this") diff --git a/lang/tags/operators.py b/lang/tags/operators.py new file mode 100644 index 0000000000..e2ae2fc881 --- /dev/null +++ b/lang/tags/operators.py @@ -0,0 +1,204 @@ +from typing import Callable, TypedDict + +from talon import Module, actions + +mod = Module() + +mod.tag("code_operators_array", desc="Tag for enabling array operator commands") +mod.tag("code_operators_assignment", desc="Tag for enabling assignment commands") +mod.tag("code_operators_bitwise", desc="Tag for enabling bitwise operator commands") +mod.tag( + "code_operators_lambda", desc="Tag for enabling commands for anonymous functions" +) +mod.tag("code_operators_math", desc="Tag for enabling mathematical operator commands") +mod.tag("code_operators_pointer", desc="Tag for enabling pointer operator commands") + +mod.list("code_operators_array", desc="List of code operators for arrays") +mod.list("code_operators_assignment", desc="List of code operators for assignments") +mod.list("code_operators_bitwise", desc="List of code operators for bitwise operations") +mod.list("code_operators_lambda", desc="List of code operators for anonymous functions") +mod.list( + "code_operators_math", desc="List of code operators for mathematical operations" +) +mod.list("code_operators_pointer", desc="List of code operators for pointers") + + +Operator = str | Callable[[], None] + + +class Operators(TypedDict, total=False): + # code_operators_array + SUBSCRIPT: Operator + + # code_operators_assignment + ASSIGNMENT: Operator + ASSIGNMENT_OR: Operator + ASSIGNMENT_SUBTRACTION: Operator + ASSIGNMENT_ADDITION: Operator + ASSIGNMENT_MULTIPLICATION: Operator + ASSIGNMENT_DIVISION: Operator + ASSIGNMENT_MODULO: Operator + ASSIGNMENT_INCREMENT: Operator + ASSIGNMENT_BITWISE_AND: Operator + ASSIGNMENT_BITWISE_OR: Operator + ASSIGNMENT_BITWISE_EXCLUSIVE_OR: Operator + ASSIGNMENT_BITWISE_LEFT_SHIFT: Operator + ASSIGNMENT_BITWISE_RIGHT_SHIFT: Operator + + # code_operators_bitwise + BITWISE_AND: Operator + BITWISE_OR: Operator + BITWISE_NOT: Operator + BITWISE_EXCLUSIVE_OR: Operator + BITWISE_LEFT_SHIFT: Operator + BITWISE_RIGHT_SHIFT: Operator + + # code_operators_lambda + LAMBDA: Operator + + # code_operators_math + MATH_SUBTRACT: Operator + MATH_ADD: Operator + MATH_MULTIPLY: Operator + MATH_DIVIDE: Operator + MATH_MODULO: Operator + MATH_EXPONENT: Operator + MATH_EQUAL: Operator + MATH_NOT_EQUAL: Operator + MATH_GREATER_THAN: Operator + MATH_GREATER_THAN_OR_EQUAL: Operator + MATH_LESS_THAN: Operator + MATH_LESS_THAN_OR_EQUAL: Operator + MATH_AND: Operator + MATH_OR: Operator + MATH_NOT: Operator + MATH_IN: Operator + MATH_NOT_IN: Operator + + # code_operators_pointer + POINTER_INDIRECTION: Operator + POINTER_ADDRESS_OF: Operator + POINTER_STRUCTURE_DEREFERENCE: Operator + + +@mod.action_class +class Actions: + def code_operator(identifier: str): + """Insert a code operator""" + try: + operators: Operators = actions.user.code_get_operators() + operator = operators.get(identifier) + + if operator is None: + raise ValueError(f"Operator {identifier} not found") + + if callable(operator): + operator() + else: + actions.insert(operator) + except NotImplementedError: + # This language has not implement the operators dict and we therefore use the fallback + operators_fallback(identifier) + return + + def code_get_operators() -> Operators: + """Get code operators dictionary""" + + +# Fallback is to rely on the legacy actions +def operators_fallback(identifier: str) -> None: + match identifier: + # code_operators_array + case "SUBSCRIPT": + actions.user.code_operator_subscript() + + # code_operators_assignment + case "ASSIGNMENT": + actions.user.code_operator_assignment() + case "ASSIGNMENT_OR": + actions.user.code_or_operator_assignment() + case "ASSIGNMENT_SUBTRACTION": + actions.user.code_operator_subtraction_assignment() + case "ASSIGNMENT_ADDITION": + actions.user.code_operator_addition_assignment() + case "ASSIGNMENT_MULTIPLICATION": + actions.user.code_operator_multiplication_assignment() + case "ASSIGNMENT_MODULO": + actions.user.code_operator_modulo_assignment() + case "ASSIGNMENT_INCREMENT": + actions.user.code_operator_increment() + case "ASSIGNMENT_BITWISE_AND": + actions.user.code_operator_bitwise_and_assignment() + case "ASSIGNMENT_BITWISE_OR": + actions.user.code_operator_bitwise_or_assignment() + case "ASSIGNMENT_BITWISE_EXCLUSIVE_OR": + actions.user.code_operator_bitwise_exclusive_or_assignment() + case "ASSIGNMENT_BITWISE_LEFT_SHIFT": + actions.user.code_operator_bitwise_left_shift_assignment() + case "ASSIGNMENT_BITWISE_RIGHT_SHIFT": + actions.user.code_operator_bitwise_right_shift_assignment() + + # code_operators_bitwise + case "BITWISE_AND": + actions.user.code_operator_bitwise_and() + case "BITWISE_OR": + actions.user.code_operator_bitwise_or() + case "BITWISE_NOT": + actions.user.code_operator_bitwise_not() + case "BITWISE_EXCLUSIVE_OR": + actions.user.code_operator_bitwise_exclusive_or() + case "BITWISE_LEFT_SHIFT": + actions.user.code_operator_bitwise_left_shift() + case "BITWISE_RIGHT_SHIFT": + actions.user.code_operator_bitwise_right_shift() + + # code_operators_lambda + case "LAMBDA": + actions.user.code_operator_lambda() + + # code_operators_math + case "MATH_SUBTRACT": + actions.user.code_operator_subtraction() + case "MATH_ADD": + actions.user.code_operator_addition() + case "MATH_MULTIPLY": + actions.user.code_operator_multiplication() + case "MATH_DIVIDE": + actions.user.code_operator_division() + case "MATH_MODULO": + actions.user.code_operator_modulo() + case "MATH_EXPONENT": + actions.user.code_operator_exponent() + case "MATH_EQUAL": + actions.user.code_operator_equal() + case "MATH_NOT_EQUAL": + actions.user.code_operator_not_equal() + case "MATH_GREATER_THAN": + actions.user.code_operator_greater_than() + case "MATH_GREATER_THAN_OR_EQUAL": + actions.user.code_operator_greater_than_or_equal_to() + case "MATH_LESS_THAN": + actions.user.code_operator_less_than() + case "MATH_LESS_THAN_OR_EQUAL": + actions.user.code_operator_less_than_or_equal_to() + case "MATH_AND": + actions.user.code_operator_and() + case "MATH_OR": + actions.user.code_operator_or() + case "MATH_NOT": + actions.user.code_operator_not() + case "MATH_IN": + actions.user.code_operator_in() + case "MATH_NOT_IN": + actions.user.code_operator_not_in() + + # code_operators_pointer + case "POINTER_INDIRECTION": + actions.user.code_operator_indirection() + case "POINTER_ADDRESS_OF": + actions.user.code_operator_address_of() + case "POINTER_STRUCTURE_DEREFERENCE": + actions.user.code_operator_structure_dereference() + + case _: + raise ValueError(f"Operator {identifier} not found") diff --git a/lang/tags/operators.talon b/lang/tags/operators.talon new file mode 100644 index 0000000000..0dd0d797ef --- /dev/null +++ b/lang/tags/operators.talon @@ -0,0 +1,6 @@ +op {user.code_operators_array}: user.code_operator(code_operators_array) +op {user.code_operators_assignment}: user.code_operator(code_operators_assignment) +op {user.code_operators_bitwise}: user.code_operator(code_operators_bitwise) +op {user.code_operators_lambda}: user.code_operator(code_operators_lambda) +op {user.code_operators_math}: user.code_operator(code_operators_math) +op {user.code_operators_pointer}: user.code_operator(code_operators_pointer) diff --git a/lang/tags/operators_array.py b/lang/tags/operators_array.py index a61dcfaa45..f33f3bce32 100644 --- a/lang/tags/operators_array.py +++ b/lang/tags/operators_array.py @@ -1,10 +1,7 @@ -from talon import Context, Module +from talon import Module -ctx = Context() mod = Module() -mod.tag("code_operators_array", desc="Tag for enabling array operator commands") - @mod.action_class class Actions: diff --git a/lang/tags/operators_array.talon b/lang/tags/operators_array.talon deleted file mode 100644 index c90d35e02d..0000000000 --- a/lang/tags/operators_array.talon +++ /dev/null @@ -1,5 +0,0 @@ -tag: user.code_operators_array -- - -# array subscription -op subscript: user.code_operator_subscript() diff --git a/lang/tags/operators_array.talon-list b/lang/tags/operators_array.talon-list new file mode 100644 index 0000000000..8404f45b1f --- /dev/null +++ b/lang/tags/operators_array.talon-list @@ -0,0 +1,5 @@ +list: user.code_operators_array +tag: user.code_operators_array +- + +subscript: SUBSCRIPT diff --git a/lang/tags/operators_assignment.py b/lang/tags/operators_assignment.py index b218502224..1dda36c9f2 100644 --- a/lang/tags/operators_assignment.py +++ b/lang/tags/operators_assignment.py @@ -1,10 +1,7 @@ -from talon import Context, Module +from talon import Module -ctx = Context() mod = Module() -mod.tag("code_operators_assignment", desc="Tag for enabling assignment commands") - @mod.action_class class Actions: diff --git a/lang/tags/operators_assignment.talon b/lang/tags/operators_assignment.talon index 1f8f3373ea..1f0c15cc2d 100644 --- a/lang/tags/operators_assignment.talon +++ b/lang/tags/operators_assignment.talon @@ -4,23 +4,44 @@ tag(): user.code_operators_math tag(): user.code_operators_bitwise # assignment -op (equals | assign): user.code_operator_assignment() -op or equals: user.code_or_operator_assignment() +op assign: + user.deprecate_command("2025-01-19", "op assign", "op equals") + user.code_operator("ASSIGNMENT") # combined computation and assignment -op (minus | subtract) equals: user.code_operator_subtraction_assignment() -op (plus | add) equals: user.code_operator_addition_assignment() -op (times | multiply) equals: user.code_operator_multiplication_assignment() -op divide equals: user.code_operator_division_assignment() -op mod equals: user.code_operator_modulo_assignment() -[op] increment: user.code_operator_increment() +op subtract equals: + user.deprecate_command("2025-01-19", "op subtract equals", "op minus equals") + user.code_operator("ASSIGNMENT_SUBTRACTION") + +op add equals: + user.deprecate_command("2025-01-19", "op add equals", "op plus equals") + user.code_operator("ASSIGNMENT_ADDITION") + +op multiply equals: + user.deprecate_command("2025-01-19", "op multiply equals", "op times equals") + user.code_operator("ASSIGNMENT_MULTIPLICATION") + +increment: + user.deprecate_command("2025-01-19", "increment", "op increment") + user.code_operator("ASSIGNMENT_INCREMENT") #bitwise operators -[op] bit [wise] and equals: user.code_operator_bitwise_and_assignment() -[op] bit [wise] or equals: user.code_operator_bitwise_or_assignment() +[op] bit [wise] and equals: + user.deprecate_command("2025-01-19", "[op] bit [wise] and equals", "op bitwise and equals") + user.code_operator("ASSIGNMENT_BITWISE_AND") + +[op] bit [wise] or equals: + user.deprecate_command("2025-01-19", "[op] bit [wise] or equals", "op bitwise or equals") + user.code_operator("ASSIGNMENT_BITWISE_OR") + (op | logical | bitwise) (ex | exclusive) or equals: - user.code_operator_bitwise_exclusive_or_assignment() + user.deprecate_command("2025-01-19", "(op | logical | bitwise) (ex | exclusive) or equals", "op bitwise exclusive or equals") + user.code_operator("ASSIGNMENT_BITWISE_EXCLUSIVE_OR") + [(op | logical | bitwise)] (left shift | shift left) equals: - user.code_operator_bitwise_left_shift_assignment() + user.deprecate_command("2025-01-19", "[(op | logical | bitwise)] (left shift | shift left) equals", "op left shift equals") + user.code_operator("ASSIGNMENT_BITWISE_LEFT_SHIFT") + [(op | logical | bitwise)] (right shift | shift right) equals: - user.code_operator_bitwise_right_shift_assignment() + user.deprecate_command("2025-01-19", "[(op | logical | bitwise)] (right shift | shift right) equals", "op right shift equals") + user.code_operator("ASSIGNMENT_BITWISE_RIGHT_SHIFT") diff --git a/lang/tags/operators_assignment.talon-list b/lang/tags/operators_assignment.talon-list new file mode 100644 index 0000000000..d4ce8e45ca --- /dev/null +++ b/lang/tags/operators_assignment.talon-list @@ -0,0 +1,22 @@ +list: user.code_operators_assignment +tag: user.code_operators_assignment +- + +# Assignment +equals: ASSIGNMENT +or equals: ASSIGNMENT_OR + +# Combined computation and assignment +minus equals: ASSIGNMENT_SUBTRACTION +plus equals: ASSIGNMENT_ADDITION +times equals: ASSIGNMENT_MULTIPLICATION +divide equals: ASSIGNMENT_DIVISION +mod equals: ASSIGNMENT_MODULO +increment: ASSIGNMENT_INCREMENT + +# Bitwise operators +bitwise and equals: ASSIGNMENT_BITWISE_AND +bitwise or equals: ASSIGNMENT_BITWISE_OR +bitwise exclusive or equals: ASSIGNMENT_BITWISE_EXCLUSIVE_OR +left shift equals: ASSIGNMENT_BITWISE_LEFT_SHIFT +right shift equals: ASSIGNMENT_BITWISE_RIGHT_SHIFT diff --git a/lang/tags/operators_bitwise.py b/lang/tags/operators_bitwise.py index d5f7797c33..82fdf89590 100644 --- a/lang/tags/operators_bitwise.py +++ b/lang/tags/operators_bitwise.py @@ -1,10 +1,7 @@ -from talon import Context, Module +from talon import Module -ctx = Context() mod = Module() -mod.tag("code_operators_bitwise", desc="Tag for enabling bitwise operator commands") - @mod.action_class class Actions: diff --git a/lang/tags/operators_bitwise.talon b/lang/tags/operators_bitwise.talon index e403e66c89..4498626249 100644 --- a/lang/tags/operators_bitwise.talon +++ b/lang/tags/operators_bitwise.talon @@ -2,14 +2,26 @@ tag: user.code_operators_bitwise - #bitwise operators -[op] bitwise and: user.code_operator_bitwise_and() -[op] bitwise or: user.code_operator_bitwise_or() -[op] bitwise not: user.code_operator_bitwise_not() +bitwise and: + user.deprecate_command("2025-01-19", "bitwise and", "op bitwise and") + user.code_operator("BITWISE_AND") -# TODO: split these out into separate logical and bitwise operator commands +bitwise or: + user.deprecate_command("2025-01-19", "bitwise or", "op bitwise or") + user.code_operator("BITWISE_OR") + +bitwise not: + user.deprecate_command("2025-01-19", "bitwise not", "op bitwise not") + user.code_operator("BITWISE_NOT") + +(op | logical | bitwise) (ex | exclusive) or: + user.deprecate_command("2025-01-19", "(op | logical | bitwise) (ex | exclusive) or", "op bitwise ex or") + user.code_operator("BITWISE_EXCLUSIVE_OR") -(op | logical | bitwise) (ex | exclusive) or: user.code_operator_bitwise_exclusive_or() (op | logical | bitwise) (left shift | shift left): - user.code_operator_bitwise_left_shift() + user.deprecate_command("2025-01-19", "(op | logical | bitwise) (left shift | shift left)", "op bitwise left shift") + user.code_operator("BITWISE_LEFT_SHIFT") + (op | logical | bitwise) (right shift | shift right): - user.code_operator_bitwise_right_shift() + user.deprecate_command("2025-01-19", "(op | logical | bitwise) (right shift | shift right)", "op bitwise right shift") + user.code_operator("BITWISE_RIGHT_SHIFT") diff --git a/lang/tags/operators_bitwise.talon-list b/lang/tags/operators_bitwise.talon-list new file mode 100644 index 0000000000..f621be806e --- /dev/null +++ b/lang/tags/operators_bitwise.talon-list @@ -0,0 +1,11 @@ +list: user.code_operators_bitwise +tag: user.code_operators_bitwise +- + +bitwise and: BITWISE_AND +bitwise or: BITWISE_OR +bitwise not: BITWISE_NOT + +bitwise ex or: BITWISE_EXCLUSIVE_OR +bitwise left shift: BITWISE_LEFT_SHIFT +bitwise right shift: BITWISE_RIGHT_SHIFT diff --git a/lang/tags/operators_lambda.py b/lang/tags/operators_lambda.py index 9cc8ede915..285537950f 100644 --- a/lang/tags/operators_lambda.py +++ b/lang/tags/operators_lambda.py @@ -1,14 +1,7 @@ -from talon import Context, Module +from talon import Module -ctx = Context() mod = Module() -# TODO: this probably shouldn't be in operators - -mod.tag( - "code_operators_lambda", desc="Tag for enabling commands for anonymous functions" -) - @mod.action_class class Actions: diff --git a/lang/tags/operators_lambda.talon b/lang/tags/operators_lambda.talon deleted file mode 100644 index 263450e3c2..0000000000 --- a/lang/tags/operators_lambda.talon +++ /dev/null @@ -1,13 +0,0 @@ -tag: user.code_operators_lambda -- - -# In many languages, anonymous functions aren't merely infix syntax: -# -# Haskell '\x -> bla' -# OCaml 'fun x -> bla' -# Rust '|x| { bla }' -# -# Therefore a revision of this command may be in order. - -# syntax for anonymous functions -op lambda: user.code_operator_lambda() diff --git a/lang/tags/operators_lambda.talon-list b/lang/tags/operators_lambda.talon-list new file mode 100644 index 0000000000..6f302e4b66 --- /dev/null +++ b/lang/tags/operators_lambda.talon-list @@ -0,0 +1,5 @@ +list: user.code_operators_lambda +tag: user.code_operators_lambda +- + +lambda: LAMBDA diff --git a/lang/tags/operators_math.py b/lang/tags/operators_math.py index 34507bf690..53cb900820 100644 --- a/lang/tags/operators_math.py +++ b/lang/tags/operators_math.py @@ -1,14 +1,8 @@ -from talon import Context, Module +from talon import Module -ctx = Context() mod = Module() -# TODO: Could split into numeric, comparison, and logic? - -mod.tag("code_operators_math", desc="Tag for enabling mathematical operator commands") - - @mod.action_class class Actions: def code_operator_subtraction(): diff --git a/lang/tags/operators_math.talon b/lang/tags/operators_math.talon index 57d590e0d8..234cf0bf56 100644 --- a/lang/tags/operators_math.talon +++ b/lang/tags/operators_math.talon @@ -2,29 +2,69 @@ tag: user.code_operators_math - # math operators -op (minus | subtract): user.code_operator_subtraction() -op (plus | add): user.code_operator_addition() -op (times | multiply): user.code_operator_multiplication() -op divide: user.code_operator_division() -op mod: user.code_operator_modulo() -(op (power | exponent) | to the power [of]): user.code_operator_exponent() +op subtract: + user.deprecate_command("2025-01-19", "op subtract", "op minus") + user.code_operator("MATH_SUBTRACT") + +op add: + user.deprecate_command("2025-01-19", "op add", "op plus") + user.code_operator("MATH_ADD") + +op multiply: + user.deprecate_command("2025-01-19", "op multiply", "op times") + user.code_operator("MATH_MULTIPLY") + +op (exponent | to the power [of]): + user.deprecate_command("2025-01-19", "op (exponent | to the power [of])", "op power") + user.code_operator("MATH_EXPONENT") # comparison operators -(op | is) equal: user.code_operator_equal() -(op | is) not equal: user.code_operator_not_equal() -(op | is) (greater | more): user.code_operator_greater_than() -(op | is) (less | below) [than]: user.code_operator_less_than() -(op | is) greater [than] or equal: user.code_operator_greater_than_or_equal_to() -(op | is) less [than] or equal: user.code_operator_less_than_or_equal_to() +is equal: + user.deprecate_command("2025-01-19", "is equal", "op equal") + user.code_operator("MATH_EQUAL") + +is not equal: + user.deprecate_command("2025-01-19", "is not equal", "op not equal") + user.code_operator("MATH_NOT_EQUAL") + +is (greater | more): + user.deprecate_command("2025-01-19", "is (greater | more)", "op greater") + user.code_operator("MATH_GREATER_THAN") + +is (less | below) [than]: + user.deprecate_command("2025-01-19", "is (less | below) [than]", "op less") + user.code_operator("MATH_LESS_THAN") + +is greater [than] or equal: + user.deprecate_command("2025-01-19", "is greater [than] or equal", "op greater or equal") + user.code_operator("MATH_GREATER_THAN_OR_EQUAL") + +is less [than] or equal: + user.deprecate_command("2025-01-19", "is less [than] or equal", "op less or equal") + user.code_operator("MATH_LESS_THAN_OR_EQUAL") # logical operators -(op | logical) and: user.code_operator_and() -(op | logical) or: user.code_operator_or() -(op | logical) not: user.code_operator_not() +logical and: + user.deprecate_command("2025-01-19", "logical and", "op and") + user.code_operator("MATH_AND") + +logical or: + user.deprecate_command("2025-01-19", "logical or", "op or") + user.code_operator("MATH_OR") + +logical not: + user.deprecate_command("2025-01-19", "logical not", "op not") + user.code_operator("MATH_NOT") # set operators -(op | is) in: user.code_operator_in() -(op | is) not in: user.code_operator_not_in() +is in: + user.deprecate_command("2025-01-19", "is in", "op in") + user.code_operator("MATH_IN") + +is not in: + user.deprecate_command("2025-01-19", "is not in", "op not in") + user.code_operator("MATH_NOT_IN") -# TODO: This operator should either be abstracted into a function or removed. -(op | pad) colon: " : " +op colon: + user.deprecate_command("2025-01-19", "op colon", "pad colon") + insert(" : ") diff --git a/lang/tags/operators_math.talon-list b/lang/tags/operators_math.talon-list new file mode 100644 index 0000000000..51c6128d82 --- /dev/null +++ b/lang/tags/operators_math.talon-list @@ -0,0 +1,28 @@ +list: user.code_operators_math +tag: user.code_operators_math +- + +# Math operators +minus: MATH_SUBTRACT +plus: MATH_ADD +times: MATH_MULTIPLY +divide: MATH_DIVIDE +mod: MATH_MODULO +power: MATH_EXPONENT + +# Comparison operators +equal: MATH_EQUAL +not equal: MATH_NOT_EQUAL +greater: MATH_GREATER_THAN +less: MATH_LESS_THAN +greater or equal: MATH_GREATER_THAN_OR_EQUAL +less or equal: MATH_LESS_THAN_OR_EQUAL + +# logical operators +and: MATH_AND +or: MATH_OR +not: MATH_NOT + +# set operators +in: MATH_IN +not in: MATH_NOT_IN diff --git a/lang/tags/operators_pointer.py b/lang/tags/operators_pointer.py index a663e91a61..1f4b82651c 100644 --- a/lang/tags/operators_pointer.py +++ b/lang/tags/operators_pointer.py @@ -1,10 +1,7 @@ -from talon import Context, Module +from talon import Module -ctx = Context() mod = Module() -mod.tag("code_operators_pointer", desc="Tag for enabling pointer operator commands") - @mod.action_class class Actions: diff --git a/lang/tags/operators_pointer.talon b/lang/tags/operators_pointer.talon deleted file mode 100644 index 29d3b85a26..0000000000 --- a/lang/tags/operators_pointer.talon +++ /dev/null @@ -1,6 +0,0 @@ -tag: user.code_operators_pointer -- -# pointer operators -op dereference: user.code_operator_indirection() -op address of: user.code_operator_address_of() -op arrow: user.code_operator_structure_dereference() diff --git a/lang/tags/operators_pointer.talon-list b/lang/tags/operators_pointer.talon-list new file mode 100644 index 0000000000..7f5d724c07 --- /dev/null +++ b/lang/tags/operators_pointer.talon-list @@ -0,0 +1,7 @@ +list: user.code_operators_pointer +tag: user.code_operators_pointer +- + +dereference: POINTER_INDIRECTION +address of: POINTER_ADDRESS_OF +arrow: POINTER_STRUCTURE_DEREFERENCE From 1775118661026a30329fe419235368db28be661e Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sun, 26 Jan 2025 02:00:07 +0100 Subject: [PATCH 97/99] Support description in snippets (#1705) Support a description field in snippet files --- core/snippets/README.md | 1 + core/snippets/snippet_types.py | 19 ++++++++++--------- core/snippets/snippets_parser.py | 20 ++++++++++++++++---- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/core/snippets/README.md b/core/snippets/README.md index 3d13fed283..033a3826f2 100644 --- a/core/snippets/README.md +++ b/core/snippets/README.md @@ -32,6 +32,7 @@ Custom format to represent snippets. | Key | Required | Multiple values | Example | | -------------- | -------- | --------------- | ------------------------------ | | name | Yes | No | `name: ifStatement` | +| description | No | No | `description: My snippet` | | language | No | Yes | `language: javascript \| java` | | phrase | No | Yes | `phrase: if \| if state` | | insertionScope | No | Yes | `insertionScope: statement` | diff --git a/core/snippets/snippet_types.py b/core/snippets/snippet_types.py index 1c6c83db5e..944470e2db 100644 --- a/core/snippets/snippet_types.py +++ b/core/snippets/snippet_types.py @@ -4,19 +4,20 @@ @dataclass class SnippetVariable: name: str - insertion_formatters: list[str] = None - wrapper_phrases: list[str] = None - wrapper_scope: str = None + insertion_formatters: list[str] | None = None + wrapper_phrases: list[str] | None = None + wrapper_scope: str | None = None @dataclass class Snippet: name: str body: str - phrases: list[str] = None - insertion_scopes: list[str] = None - languages: list[str] = None - variables: list[SnippetVariable] = None + description: str | None + phrases: list[str] | None = None + insertion_scopes: list[str] | None = None + languages: list[str] | None = None + variables: list[SnippetVariable] | None = None def get_variable(self, name: str): if self.variables: @@ -35,11 +36,11 @@ def get_variable_strict(self, name: str): @dataclass class InsertionSnippet: body: str - scopes: list[str] = None + scopes: list[str] | None = None @dataclass class WrapperSnippet: body: str variable_name: str - scope: str = None + scope: str | None = None diff --git a/core/snippets/snippets_parser.py b/core/snippets/snippets_parser.py index 6b5ce9b320..484c408bf0 100644 --- a/core/snippets/snippets_parser.py +++ b/core/snippets/snippets_parser.py @@ -11,6 +11,7 @@ class SnippetDocument: line_body: int variables: list[SnippetVariable] = [] name: str | None = None + description: str | None = None phrases: list[str] | None = None insertionScopes: list[str] | None = None languages: list[str] | None = None @@ -48,10 +49,12 @@ def create_snippets(documents: list[SnippetDocument]) -> list[Snippet]: def create_snippet( - document: SnippetDocument, default_context: SnippetDocument + document: SnippetDocument, + default_context: SnippetDocument, ) -> Snippet | None: snippet = Snippet( - name=document.name or default_context.name, + name=document.name or default_context.name or "", + description=document.description or default_context.description, languages=document.languages or default_context.languages, phrases=document.phrases or default_context.phrases, insertion_scopes=document.insertionScopes or default_context.insertionScopes, @@ -72,6 +75,10 @@ def validate_snippet(document: SnippetDocument, snippet: Snippet) -> bool: error(document.file, document.line_doc, "Missing snippet name") is_valid = False + if snippet.variables is None: + error(document.file, document.line_doc, "Missing snippet variables") + return False + for variable in snippet.variables: var_name = f"${variable.name}" if var_name not in snippet.body: @@ -125,9 +132,12 @@ def combine_variables( return list(variables.values()) -def normalize_snippet_body_tabs(body: str) -> str: +def normalize_snippet_body_tabs(body: str | None) -> str: + if not body: + return "" + # If snippet body already contains tabs. No change. - if not body or "\t" in body: + if "\t" in body: return body lines = [] @@ -276,6 +286,8 @@ def parse_context_line( match key: case "name": document.name = value + case "description": + document.description = value case "phrase": document.phrases = parse_vector_value(value) case "insertionScope": From 16adf1c2e8103d8eae74629b4ee5b57e85af49bb Mon Sep 17 00:00:00 2001 From: FireChickenProductivity <107892169+FireChickenProductivity@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:09:15 -0700 Subject: [PATCH 98/99] Add continuous scrolling speed adjustment (#1649) Add the ability to adjust continuous scrolling speed while scrolling by dictating a number_small. The speed becomes the speed setting multiplied by the dictated number divided by ten. The acceleration gets reset every time the speed gets changed. The scrolling speed reverts to the default after the current scroll is finished. closes #1648 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nicholas Riley Co-authored-by: Jeff Knaus --- core/numbers/numbers_unprefixed.talon | 1 + plugin/mouse/README.md | 7 +++ plugin/mouse/continuous_scrolling.talon | 3 + plugin/mouse/mouse.talon | 8 +++ plugin/mouse/mouse_scroll.py | 79 ++++++++++++++++++++----- 5 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 plugin/mouse/README.md create mode 100644 plugin/mouse/continuous_scrolling.talon diff --git a/core/numbers/numbers_unprefixed.talon b/core/numbers/numbers_unprefixed.talon index f28957b42f..3539294645 100644 --- a/core/numbers/numbers_unprefixed.talon +++ b/core/numbers/numbers_unprefixed.talon @@ -1,4 +1,5 @@ tag: user.unprefixed_numbers +and not tag: user.continuous_scrolling - : "{number_prose_unprefixed}" diff --git a/plugin/mouse/README.md b/plugin/mouse/README.md new file mode 100644 index 0000000000..f7b016961c --- /dev/null +++ b/plugin/mouse/README.md @@ -0,0 +1,7 @@ +# Mouse + +## Continuous Scrolling + +You can start continuous scrolling by saying "wheel upper" or "wheel downer" and stop by saying "wheel stop". Saying "here" after one of the scrolling commands first moves the cursor to the middle of the window. A number between 1 and 99 can be dictated at the end of a scrolling command to set the scrolling speed. Dictating a continuous scrolling command in the same direction twice stops the scrolling. + +During continuous scrolling, you can dictate a number between 0 and 99 to change the scrolling speed. The resulting speed is the user.mouse_continuous_scroll_amount setting multiplied by the number you dictated divided by the user.mouse_continuous_scroll_speed_quotient setting (which defaults to 10). With default settings, dictating 5 gives you half speed and dictating 20 gives you double speed. Note: Because the scrolling speed has to be an integer number, changing the speed by a small amount like 1 might not change how fast scrolling actually happens depending on your settings. The final scrolling speed is chosen by rounding and enforcing a minimum speed of 1. diff --git a/plugin/mouse/continuous_scrolling.talon b/plugin/mouse/continuous_scrolling.talon new file mode 100644 index 0000000000..f1d00e0b66 --- /dev/null +++ b/plugin/mouse/continuous_scrolling.talon @@ -0,0 +1,3 @@ +tag: user.continuous_scrolling +- +: user.mouse_scroll_set_speed(number_small) diff --git a/plugin/mouse/mouse.talon b/plugin/mouse/mouse.talon index 974f8025ba..ab295479fb 100644 --- a/plugin/mouse/mouse.talon +++ b/plugin/mouse/mouse.talon @@ -86,7 +86,11 @@ wheel tiny [down]: user.mouse_scroll_down(0.2) wheel tiny [down] here: user.mouse_move_center_active_window() user.mouse_scroll_down(0.2) +wheel downer : user.mouse_scroll_down_continuous(number_small) wheel downer: user.mouse_scroll_down_continuous() +wheel downer here : + user.mouse_move_center_active_window() + user.mouse_scroll_down_continuous(number_small) wheel downer here: user.mouse_move_center_active_window() user.mouse_scroll_down_continuous() @@ -98,7 +102,11 @@ wheel tiny up: user.mouse_scroll_up(0.2) wheel tiny up here: user.mouse_move_center_active_window() user.mouse_scroll_up(0.2) +wheel upper : user.mouse_scroll_up_continuous(number_small) wheel upper: user.mouse_scroll_up_continuous() +wheel upper here : + user.mouse_move_center_active_window() + user.mouse_scroll_up_continuous(number_small) wheel upper here: user.mouse_move_center_active_window() user.mouse_scroll_up_continuous() diff --git a/plugin/mouse/mouse_scroll.py b/plugin/mouse/mouse_scroll.py index d2bce05818..b677e48cc2 100644 --- a/plugin/mouse/mouse_scroll.py +++ b/plugin/mouse/mouse_scroll.py @@ -1,5 +1,5 @@ import time -from typing import Literal +from typing import Literal, Optional from talon import Context, Module, actions, app, cron, ctrl, imgui, settings, ui @@ -10,11 +10,11 @@ scroll_start_ts: float = 0 hiss_scroll_up = False control_mouse_forced = False +continuous_scrolling_speed_factor: float = 1.0 mod = Module() ctx = Context() - mod.setting( "mouse_wheel_down_amount", type=int, @@ -52,10 +52,23 @@ desc="When enabled, the 'Scroll Mouse' GUI will not be shown.", ) +mod.setting( + "mouse_continuous_scroll_speed_quotient", + type=float, + default=10.0, + desc="When adjusting the continuous scrolling speed through voice commands, the result is that the speed is multiplied by the dictated number divided by this number.", +) + +mod.tag( + "continuous_scrolling", + desc="Allows commands for adjusting continuous scrolling behavior", +) + @imgui.open(x=700, y=0) def gui_wheel(gui: imgui.GUI): gui.text(f"Scroll mode: {continuous_scroll_mode}") + gui.text(f"say a number between 0 and 99 to set scrolling speed") gui.line() if gui.button("Wheel Stop [stop scrolling]"): actions.user.mouse_scroll_stop() @@ -83,13 +96,13 @@ def mouse_scroll_right(amount: float = 1): x = amount * settings.get("user.mouse_wheel_horizontal_amount") actions.mouse_scroll(0, x) - def mouse_scroll_up_continuous(): + def mouse_scroll_up_continuous(speed_factor: Optional[int] = None): """Scrolls up continuously""" - mouse_scroll_continuous(-1) + mouse_scroll_continuous(-1, speed_factor) - def mouse_scroll_down_continuous(): + def mouse_scroll_down_continuous(speed_factor: Optional[int] = None): """Scrolls down continuously""" - mouse_scroll_continuous(1) + mouse_scroll_continuous(1, speed_factor) def mouse_gaze_scroll(): """Starts gaze scroll""" @@ -115,10 +128,12 @@ def mouse_gaze_scroll_toggle(): def mouse_scroll_stop() -> bool: """Stops scrolling""" - global scroll_job, gaze_job, continuous_scroll_mode, control_mouse_forced + global scroll_job, gaze_job, continuous_scroll_mode, control_mouse_forced, continuous_scrolling_speed_factor continuous_scroll_mode = "" + continuous_scrolling_speed_factor = 1.0 return_value = False + ctx.tags = [] if scroll_job: cron.cancel(scroll_job) @@ -138,6 +153,22 @@ def mouse_scroll_stop() -> bool: return return_value + def mouse_scroll_set_speed(speed: Optional[int]): + """Sets the continuous scrolling speed for the current scrolling""" + global continuous_scrolling_speed_factor, scroll_start_ts + if scroll_start_ts: + scroll_start_ts = time.perf_counter() + if speed is None: + continuous_scrolling_speed_factor = 1.0 + else: + continuous_scrolling_speed_factor = speed / settings.get( + "user.mouse_continuous_scroll_speed_quotient" + ) + + def mouse_is_continuous_scrolling(): + """Returns whether continuous scroll is in progress""" + return len(continuous_scroll_mode) > 0 + def hiss_scroll_up(): """Change mouse hiss scroll direction to up""" global hiss_scroll_up @@ -162,15 +193,19 @@ def noise_trigger_hiss(active: bool): actions.user.mouse_scroll_stop() -def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): - global scroll_job, scroll_dir, scroll_start_ts, continuous_scroll_mode +def mouse_scroll_continuous( + new_scroll_dir: Literal[-1, 1], + speed_factor: Optional[int] = None, +): + global scroll_job, scroll_dir, scroll_start_ts + actions.user.mouse_scroll_set_speed(speed_factor) + + update_continuous_scrolling_mode(new_scroll_dir) if scroll_job: # Issuing a scroll in the same direction aborts scrolling if scroll_dir == new_scroll_dir: - cron.cancel(scroll_job) - scroll_job = None - continuous_scroll_mode = "" + actions.user.mouse_scroll_stop() # Issuing a scroll in the reverse direction resets acceleration else: scroll_dir = new_scroll_dir @@ -178,23 +213,37 @@ def mouse_scroll_continuous(new_scroll_dir: Literal[-1, 1]): else: scroll_dir = new_scroll_dir scroll_start_ts = time.perf_counter() - continuous_scroll_mode = "scroll down continuous" scroll_continuous_helper() scroll_job = cron.interval("16ms", scroll_continuous_helper) + ctx.tags = ["user.continuous_scrolling"] if not settings.get("user.mouse_hide_mouse_gui"): gui_wheel.show() +def update_continuous_scrolling_mode(new_scroll_dir: Literal[-1, 1]): + global continuous_scroll_mode + if new_scroll_dir == -1: + continuous_scroll_mode = "scroll up continuous" + else: + continuous_scroll_mode = "scroll down continuous" + + def scroll_continuous_helper(): - scroll_amount = settings.get("user.mouse_continuous_scroll_amount") + scroll_amount = ( + settings.get("user.mouse_continuous_scroll_amount") + * continuous_scrolling_speed_factor + ) acceleration_setting = settings.get("user.mouse_continuous_scroll_acceleration") acceleration_speed = ( 1 + min((time.perf_counter() - scroll_start_ts) / 0.5, acceleration_setting - 1) if acceleration_setting > 1 else 1 ) - y = scroll_amount * acceleration_speed * scroll_dir + + y = round(scroll_amount * acceleration_speed * scroll_dir) + if y == 0: + y = scroll_dir actions.mouse_scroll(y) From b57dac88d69a06c22aea76dc6f47bab895a32f56 Mon Sep 17 00:00:00 2001 From: Stefan du Fresne Date: Mon, 27 Jan 2025 16:34:10 +0000 Subject: [PATCH 99/99] Fix default block implementation (#1708) `self` was a typo I think, see comment-block for an implementation that works. This fixes a bug with saying "block" in c-style languages. Before this PR, it crashes. After this PR, it uses the default successfully. --- lang/tags/imperative.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/tags/imperative.py b/lang/tags/imperative.py index 66f451816c..76f4daf83b 100644 --- a/lang/tags/imperative.py +++ b/lang/tags/imperative.py @@ -10,7 +10,7 @@ mod.tag("code_block_c_like", desc="Language uses C style code blocks, i.e. braces") c_like_ctx.matches = """ -tag: self.code_block_c_like +tag: user.code_block_c_like """ @@ -65,7 +65,7 @@ def code_try_catch(): """Inserts try/catch. If selection is true, does so around the selection""" -@c_like_ctx.action_class("self") +@c_like_ctx.action_class("user") class CActions: def code_block(): actions.user.insert_between("{", "}")