commit 2957b5515a3623d39ab2aa11f8e1995a8a7e9c91 Author: unknown Date: Tue Aug 19 08:06:37 2025 -0400 init commit diff --git a/community/.editorconfig b/community/.editorconfig new file mode 100644 index 0000000..0d6fd5f --- /dev/null +++ b/community/.editorconfig @@ -0,0 +1,17 @@ +# See https://EditorConfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_size = 4 +indent_style = space +max_line_length = 88 +trim_trailing_whitespace = true + +[*.{md,yaml,yml}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/community/.git-blame-ignore-revs b/community/.git-blame-ignore-revs new file mode 100644 index 0000000..92da84f --- /dev/null +++ b/community/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +2877a6849d75e5fa78c9453991a9235b4f6d9dcf +3bf4882fa0a05b22171e59118bd7c9640aae753a +446ec764c9caa98973eacd7f792b6a087a1b635f diff --git a/community/.github/dependabot.yml b/community/.github/dependabot.yml new file mode 100644 index 0000000..2c7d170 --- /dev/null +++ b/community/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/community/.github/workflows/ci.yml b/community/.github/workflows/ci.yml new file mode 100644 index 0000000..6d93665 --- /dev/null +++ b/community/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: | + requirements-dev.txt + - run: pip install -r requirements-dev.txt + - run: pytest diff --git a/community/.gitignore b/community/.gitignore new file mode 100644 index 0000000..e6bc808 --- /dev/null +++ b/community/.gitignore @@ -0,0 +1,17 @@ +# Dev stuff +__pycache__ +*.sw? +.idea/ +# Locally generated +# we highly recommended against removing the private folder from the gitignore file +# the directory is used to store sensitive information such as contacts +/private +.vscode/settings.json +.DS_Store +*.bak +# Emacs (from https://github.com/github/gitignore/blob/main/Global/Emacs.gitignore) +*~ +\#*\# +.\#* +# Vim (from https://github.com/github/gitignore/blob/main/Global/Vim.gitignore) +[._]*.swp diff --git a/community/.pre-commit-config.yaml b/community/.pre-commit-config.yaml new file mode 100644 index 0000000..d4d65a1 --- /dev/null +++ b/community/.pre-commit-config.yaml @@ -0,0 +1,52 @@ +minimum_pre_commit_version: "3.2.0" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: destroyed-symlinks + - id: detect-private-key + - id: fix-byte-order-marker + # NB. To avoid sometimes needing multiple runs, we need: + # - trailing-whitespace BEFORE end-of-file-fixer, + # otherwise trailing newline followed by whitespace, "\n ", + # will need multiple runs. + # - end-of-file-fixer BEFORE mixed-line-ending, + # otherwise a file with CRLF line endings but missing a trailing + # newline will need multiple runs. + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v4.0.0-alpha.8" + hooks: + - id: prettier + - repo: https://github.com/ikamensh/flynt/ + rev: "1.0.6" + hooks: + - id: flynt + - repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.1.0 + hooks: + - id: black + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: remove-tabs + types: [file] + files: \.talon$ + args: ["--whitespaces-count=4"] + - repo: https://github.com/wenkokke/talonfmt + rev: 1.10.2 + hooks: + - id: talonfmt + args: ["--in-place"] diff --git a/community/BREAKING_CHANGES.txt b/community/BREAKING_CHANGES.txt new file mode 100644 index 0000000..99117b7 --- /dev/null +++ b/community/BREAKING_CHANGES.txt @@ -0,0 +1,57 @@ +This file lists known changes to `community` that are likely to have +broken existing functionality. The file is sorted by date with the +newest entries up the top. + +Be aware there may be some difference between the date in this file +and when the change was applied given the delay between changes being +submitted and the time they were reviewed and merged. + +--- +* 2025-08-09 Rename `user.code_keyword_bare` to `user.code_keyword_unprefixed` to improve clarity. +* 2025-07-10 Make `state do` for ruby insert a do while loop instead of the `do` keyword to make the behavior consistent with other languages. +* 2025-06-28 Remove user.code_block_c_like tag in favor of using `codeBlock.snippet` to handle code blocks. +* 2025-06-24 Deprecate imperative.py code state actions in favor of their corresponding snippets. +* 2025-05-26 Change the `genericForLoopStatement` snippet's name to `forLoopStatement` and change the existing `forLoopStatement` snippet's name to `forRangeStatement`. `forRangeStatement` is now used with `snip for range`. +* 2025-04-20 Deprecated php commands `(op | is) loosely equal` and `(op | is) loosely not equal` in favor of `is weak equal` and `is weak not equal`. The names of the comparison operators can be updated in lang/tags/operators_math_comparison.talon-list if desired. +* 2025-03-04 Deprecated javascript commands `(op | is) strict equal` and `(op | is) strict not equal` in favor of `is equal` and `is not equal`. Weak comparison can be done with `is weak equal` and `is weak not equal`. +* 2025-02-01 Removed snippet language inheritance. From now on typescript will not automatically use javascript snippets. Each snippet need to define every language it should be active in. +* 2025-01-19 Deprecated a bunch of programming language operator commands in favor of using Talon lists +* 2024-12-03 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. +* 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. +* 2024-09-07 If you've updated `community` since 2024-08-31, you may + need to replace `host:` with `hostname:` in the header of + `core/system_paths-.talon-list` due to an issue with + automatic conversion from CSV (#1268). +* 2024-07-31 Remove commands `"command mode"`, `"dictation mode"` from + custom user modes. Note that if you have any custom modes where you + want these commands you could add that mode to the context of + `command_and_dictation_mode.talon` or copying the command to one of + your custom files. +* 2024-07-30 Deprecate `lend` and `bend` commands in favor of `go line + end | tail` and `go line start | head`. +* 2024-07-28 Removed the following user namespace actions in favor of + the new action/modifier grammar. + https://github.com/talonhub/community/blob/37a8ebde90c8120a0b52555030988d4f54e65159/core/edit/edit.talon#L3 + cut_word, copy_word, paste_word + cut_all, copy_all, paste_all, delete_all + copy_line, paste_line + cut_line_start, copy_line_start, paste_line_start, delete_line_start + cut_line_end, copy_line_end, paste_line_end, delete_line_end +* 2024-05-30 Deprecate 'drop down ' in favor of + overridable 'choose' helper +* 2024-01-27 Deprecate '' command without a spoken + prefix like `numb`. See `numbers.talon` and + `numbers_unprefixed.talon.` If in the future you want to still use + unprefixed numbers, you will need to comment out the + `tag(): user.prefixed_numbers` line in your `settings.talon` file. +* 2023-06-06 Deprecate `go` command for VSCode. Use 'bar marks' instead. +* 2023-02-04 Deprecate `murder` command for i3wm. Use 'win kill' instead. +* 2022-12-11 Deprecate user.insert_with_history. Just use + `user.add_phrase_to_history(text); insert(text)` instead. See #939. +* 2022-10-01 Large refactoring of code base that moves many files into + new locations. No other backwards-incompatible changes included. diff --git a/community/CONTRIBUTING.md b/community/CONTRIBUTING.md new file mode 100644 index 0000000..c5ddf51 --- /dev/null +++ b/community/CONTRIBUTING.md @@ -0,0 +1,13 @@ +This document attempts to list a set of principles for contributors to the `community` repository to consider. The idea is to document some agreed upon approaches toward reviewing and including code so we can all more easily make consistent decisions. + +Each of the principles is numbered for easy referencing. The body is formatted as a short single-line summary of the principle followed by elaboration and discussion links. + +# Voice command principles + +- P01 - Prefer [object][verb] rather than [verb][object] for new commands. For example 'file save' is better than 'save file'. It may not sound as natural, but it helps for grouping related commands in lists and avoiding conflicting names. +- P02 - Use `browser.host` matcher for web apps. Though this matcher requires a [browser extension](https://github.com/talonhub/community/blob/main/apps/README.md) on some operating systems it is the only unambiguous way of referring to a web app. + +# Coding principles + +- P03 - Use the `app.bundle` matcher for apps on OSX. This is the least ambiguous way of referring to a particular program. +- P04 - Use both `app.name` and `app.exe` matchers for apps on Windows. That is the context should OR together one matcher of each type. Apparently the [MUICache](https://www.magnetforensics.com/blog/forensic-analysis-of-muicache-files-in-windows/) can break, perhaps making one of these matchers stop working. diff --git a/community/LICENSE b/community/LICENSE new file mode 100644 index 0000000..bbc3522 --- /dev/null +++ b/community/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jeff Knaus, Ryan Hileman, Zach Dwiel, Michael Arntzenius, and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/community/README.md b/community/README.md new file mode 100644 index 0000000..13ee512 --- /dev/null +++ b/community/README.md @@ -0,0 +1,381 @@ +# community + +Voice command set for [Talon](https://talonvoice.com/), community-supported. + +_(Originally called `knausj_talon`, after [its original creator :superhero:](https://github.com/knausj85))_ + +Can be used on its own, but shines when combined with: + +- [Cursorless](https://www.cursorless.org/) for programming and text editing +- [Rango](https://github.com/david-tejada/rango) for browser navigation +- [gaze-ocr](https://github.com/wolfmanstout/talon-gaze-ocr) for advanced cursor control using eye tracking and text recognition (OCR) +- [AXKit](https://github.com/phillco/talon-axkit) (macOS only) to enhance Talon with native OS accessibility integrations +- [Other user file sets](https://talon.wiki/Integrations/talon_user_file_sets) + +## Installation + +### Prerequisites + +- [Talon](https://talonvoice.com/) +- Mac, Windows, or Linux +- Talon's built-in Conformer (wav2letter) speech recognition engine (recommended), or Dragon NaturallySpeaking (Windows) / Dragon for Mac (although beware that Dragon for Mac is discontinued and its use deprecated). + +Includes commands for working with an eye tracker; an [eye tracker](https://talon.wiki/Quickstart/Hardware/#eye-trackers) is not required. + +### Linux & Mac + +It is recommended to install `community` using [`git`](https://git-scm.com/). + +1. Install [`git`](https://git-scm.com/) +2. Open a terminal ([Mac](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac) / [Ubuntu](https://ubuntu.com/tutorials/command-line-for-beginners#3-opening-a-terminal)) +3. Paste the following into the terminal window then press Enter/Return: + + ```bash + cd ~/.talon/user + git clone https://github.com/talonhub/community community + ``` + +Note that it is also possible to install `community` by [downloading and extracting a zip file](#alternate-installation-method-zip-file), but this approach is discouraged because it makes it more difficult to keep track of any changes you may make to your copy of the files. + +### Windows + +It is recommended to install `community` using [`git`](https://git-scm.com/). + +1. Install [`git`](https://git-scm.com/) +2. Open a [command prompt](https://www.wikihow.com/Open-the-Command-Prompt-in-Windows) +3. Paste the following into the command prompt window then press Enter: + + ``` + cd %AppData%\Talon\user + git clone https://github.com/talonhub/community community + ``` + +Note that it is also possible to install `community` by [downloading and extracting a zip file](#alternate-installation-method-zip-file), but this approach is discouraged because it makes it more difficult to keep track of any changes you may make to your copy of the files. + +## Getting started with Talon + +1. `help active` displays commands available in the active (frontmost) application. + - Available commands can change by application, or even the window title. + - Navigate help by voice using the displayed numbers (e.g., `help one one` or `help eleven` to open the item numbered 11), or by speaking button titles that don't start with numbers (e.g., `help next` to see the next page of contexts). + - Help-related commands are defined in [help.talon](core/help/help.talon) and [help_open.talon](core/help/help_open.talon). +2. Search for commands by saying `help search `. For example, `help search tab` displays all tab-related commands, and `help search help` displays all help-related commands. +3. Jump immediately to help for a particular help context with the name displayed the in help window (based on the name of the .talon file), e.g. `help context symbols` or `help context visual studio` +4. `help alphabet` displays words for letters of the alphabet; `help symbols` displays words for symbols. +5. `command history` toggles display of recent voice commands. +6. `help format` displays available [formatters](#formatters) with examples. +7. Many useful, basic commands are defined in [edit.talon](core/edit/edit.talon). + - `undo that` and `redo that` are the default undo/redo commands. + - `paste that`, `copy that`, and `cut that` for pasting/copy/cutting, respectively. +8. For community-generated documentation on Talon itself, please visit https://talon.wiki/. + +It's recommended to learn the alphabet first, then get familiar with the keys, symbols, formatters, mouse, and generic_editor commands. + +Once you have the basics of text input down, try copying some code from one window to another. + +After that, explore using ordinal repetition for easily repeating a command without pausing (e.g., saying `go up fifth` will go up five lines), window switching (`focus chrome`), and moving around in your text editor of choice. + +If you use vim, just start with the numbers and alphabet, otherwise look at generic_editor.talon as well at jetbrains, vscode, and any other integrations. + +### Alphabet + +The alphabet is defined in +[this Talon list file](core/keys/letter.talon-list). + +Say `help alphabet` to open a window displaying the alphabet. `help close` closes the window. + +Try saying e.g. `air bat cap` to insert abc. + +### Keys + +All key commands are defined in [keys.talon](core/keys/keys.talon). Say letters of the [Talon alphabet](#alphabet) for A–Z. + +For modifier keys, say `help modifiers`. For example, say `shift air` to press Shift + A, which types a capital `A`. + +For symbols, say `help symbols`. These are defined in keys.py; +search for `modifier_keys` and then keep scrolling — roughly starting [here](core/keys/keys.py#L124). + +On Windows, try commands such as: + +- `control air` to press Control + A and select all. + +- `super-shift-sun` to press Win + Shift + S, triggering the screenshot application (Windows 10/11). Then try `escape` to exit. + +On Mac, try commands such as: + +- `command air` to press A and select all. + +- `control shift command 4` to press 4, copying a screenshot of the selected area to the clipboard. Then try `escape` to exit. Please note the order of the modifiers doesn't matter. + +Say any combination of modifiers, symbols, alphabet, numbers and function keys to execute keyboard shortcuts. Modifier keys can be tapped using `press`, for example `press control` taps the Control () key by itself. + +### Symbols + +Some symbols are defined in [keys.py](core/keys/keys.py#L144), so you can say, e.g. `control colon` to press those keys. + +Multi-character punctuation (e.g., ellipses) is defined in [symbols.talon](plugin/symbols/symbols.talon). + +### Formatters + +Formatters allow you to insert words with consistent capitalization and punctuation. `help format` displays available formatters with examples of their output when followed by `one two three`. + +Try using a formatter by saying `snake hello world`. This inserts "hello_world". + +Multiple formatters can be chained together — for example, `dubstring snake hello world` inserts "hello_world". + +Prose formatters (marked with \* in the help window) preserve hyphens and apostrophes. Non-prose (code) formatters strip punctuation instead, for example to generate a valid variable name. `title how's it going` inserts "How's It Going"; `hammer how's it going` inserts "HowsItGoing". + +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/formatters/formatters.py#L245). Formatter-related commands are defined in [text.talon](core/text/text.talon#L8). + +### Mouse commands + +See [mouse.talon](plugin/mouse/mouse.talon) for commands to click, drag, scroll, and use an eye tracker. To use a grid to click at a certain location on the screen, see [mouse_grid](core/mouse_grid). + +### Generic editing commands + +Editing commands in [edit.talon](core/edit/edit.talon) are global. Commands such as `go word left` will work in any text box that uses standard platform text navigation conventions. + +### Repeating commands + +Voice commands for repeating commands are defined in [repeater.talon](plugin/repeater/repeater.talon). + +Say `go up fifth` or `go up five times` to go up five lines. `select up third` will press Shift+ three times to select several lines of text. + +### Window management + +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. +- `launch music` will launch the music application. Note this is currently only implemented on macOS. + +### Screenshot commands + +See [screenshot.talon](plugin/screenshot/screenshot.talon). + +### Programming languages + +Specific programming languages may be activated by voice commands, or via title tracking. + +Activating languages via commands will enable the commands globally, e.g. they'll work in any application. This will also disable the title tracking method (code.language in .talon files) until the "clear language modes" voice command is used. + +Commands for enabling languages are defined in [language_modes.talon](core/modes/language_modes.talon). + +By default, title tracking activates languages in supported applications such as VSCode, Visual Studio (requires plugin), and Notepad++. + +To enable title tracking for your application: + +1. Ensure the active filename (including extension) is included in the window title. +2. Implement the required Talon-defined `filename` action to correctly extract the filename from the window title. See the [Visual Studio Code implementation](apps/vscode/vscode.py#L137-L153) for an example. + +Python, C#, Talon and JavaScript language support is broken up into multiple tags in an attempt to standardize common voice commands for features available across languages. Each tag is defined in a .talon file named after a `user.code_` tag (e.g., `user.code_functions` → `functions.talon`) containing voice commands and a Python file declaring the actions that should be implemented by each concrete language implementation to support the voice commands. These files include: + +- `lang/tags/comment_block.{talon,py}` - block comments (e.g., C++'s `/* */`) +- `lang/tags/comment_documentation.{talon,py}` - documentation comments (e.g., Java's `/** */`) +- `lang/tags/comment_line.{talon,py}` - line comments (e.g., Python's `#`) +- `lang/tags/data_null.{talon,py}` - null & null checks (e.g., Python's `None`) +- `lang/tags/data_bool.{talon,py}` - booleans (e.g., Haskell's `True`) +- `lang/tags/functions.{talon,py}` - functions and definitions +- `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/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`) +- `lang/tags/operators_bitwise.{talon,py}` - bitwise operators (e.g., C's `x >> 1`) +- `lang/tags/operators_lambda.{talon,py}` - anonymous functions (e.g., JavaScript's `x => x + 1`) +- `lang/tags/operators_math.{talon,py}` - numeric, comparison, and logical operators +- `lang/tags/operators_pointer.{talon,py}` - pointer operators (e.g., C's `&x`) + +Language-specific implementations of the above features are in files named `lang/{your-language}/{your-language}.py`. + +To add support for a new language, ensure appropriate extension is added/uncommented in the [`language_extensions` dictionary in language_modes.py](core/modes/language_modes.py#L9). Then create the following files: + +- `lang/{your-language}/{your-language}.py` +- `lang/{your-language}/{your-language}.talon` + +Activate the appropriate tags in `{your-language}.talon` and implement the corresponding actions in `{your-language}.py`, following existing language implementations. Put additional voice commands for your language (not shared with other languages) in `{your-language}.talon`. + +## File manager commands + +For the following file manager commands to work, your file manager must display the full folder path in the title bar. tags/file_manager/file_manager.talon + +For the Mac Finder, run this command in Terminal to display the full path in the window title: + +``` +defaults write com.apple.finder _FXShowPosixPathInTitle -bool YES +``` + +For Windows Explorer, [follow these directions](https://www.howtogeek.com/121218/beginner-how-to-make-explorer-always-show-the-full-path-in-windows-8/). + +For the Windows command line, the `refresh title` command will force the title to the current directory, and all directory commands (`follow 1`) will automatically update the title. + +Notes: + +- Both Windows Explorer and Finder hide certain files and folders by default, so it's often best to use the imgui to list the options before issuing commands. + +- If there no hidden files or folders, and the items are displayed in alphabetical order, you can typically issue the `follow `, `file ` and `open ` commands based on the displayed order. + +To implement support for a new program, implement the relevant file manager actions for your application and assert the `user.file_manager` tag. There are a number of example implementations in the repository. [Finder](apps/finder/finder.py) is a good example to copy and modify. + +## Terminal commands + +Many terminal applications are supported out of the box, but you may not want all the commands enabled. + +To use command sets in your terminal applications, enable/disable the corresponding tags in the terminal application-specific .talon file. + +``` +tag(): user.file_manager +tag(): user.git +tag(): user.kubectl +tag(): user.tabs +``` + +For instance, kubectl commands (kubernetes) aren't relevant to everyone. + +Note also that while some of the command sets associated with these tags are defined in talon files within [tags](tags), others, like git, are defined within [apps](apps). Commands for tabs are defined in [tabs.talon](core/windows_and_tabs/tabs.talon). + +### Unix utilities + +If you have a Unix (e.g. macOS) or Linux computer, you can enable support for a number of +common terminal utilities like `cat`, `tail`, or `grep` by uncommenting the following +line in [unix_shell.py](tags/terminal/unix_shell.py): + +``` +# ctx.tags = ["user.unix_utilities"] +``` + +Once you have uncommented the line, you can customize your utility commands by editing +`tags/terminal/unix_utility.talon-list`. + +## Jetbrains commands + +For Jetbrains commands to work you must install https://plugins.jetbrains.com/plugin/10504-voice-code-idea +into each editor. + +## Additional commands + +There are other commands not described fully within this file. As an overview: + +- The apps folder has command sets for use within different applications +- The core folder has various commands described [here](core/README.md) +- The lang folder has commands for writing [programming languages](#programming-languages) +- The plugin folder has various commands described [here](plugin/README.md) +- The tags folder has various other commands, such as using a browser, navigating a filesystem in terminal, and managing multiple cursors + +## Settings + +Several options are configurable via a [single settings file](settings.talon) out of the box. Any setting can be made context specific as needed (e.g., per-OS, per-app, etc). + +The most commonly adjusted settings are probably + +- `imgui.scale` to improve the visibility of all imgui-based windows (help, history, etc). This is simply a scale factor, 1.3 = 130%. + +- `user.help_max_command_lines_per_page` and `user.help_max_contexts_per_page` to ensure all help information is visible. + +- `user.mouse_wheel_down_amount` and `user.mouse_continuous_scroll_amount` for adjusting the scroll amounts for the various scroll commands. + +## Customizing words and lists + +Most lists of words are provided as Talon list files, with an extension of `.talon-list`. Read about the syntax of these files [on the Talon wiki](https://talon.wiki/Customization/talon_lists). + +Some lists with multiple spoken forms/alternatives are instead provided as CSV files. Some are in the `settings` folder and are not created until you launch Talon with `community` installed. + +You can customize common Talon list and CSV files with voice commands: say the word `customize` followed by `abbreviations`, `additional words`, `alphabet`, `homophones`, `search engines`, `Unix utilities`, `websites`, `words to replace`, `contacts json` or `contacts csv`. These open the file in a text editor and move the insertion point to the bottom of the file so you can add to it. + +You can also add words to the vocabulary or replacements (words_to_replace) by using the commands in [edit_vocabulary.talon](core/vocabulary/edit_vocabulary.talon). + +## 💡 Tip: Overriding cleanly + +You can override Talon lists by creating a new `.talon-list` file of your own, rather than changing the existing list in the repository. +This reduces how much manual `git merge`-ing you'll have to do in the future, when you go to merge new versions of this repository (colloquially called "upstream") with your local changes. This is because _new_ files you create will almost never conflict with upstream changes, whereas changing an existing file (especially hot spots, like commonly-customized lists) frequently do. +Your override files can even live outside of the `community` repository (anywhere in the Talon user directory), if you prefer, further simplifying merging. +To do so, simply create a `.talon-list` file with a more specific [context header](https://talon.wiki/Customization/talon-files#context-header) than the default. (For example, `lang: en` or `os: mac` main). Talon ensures that the most specific header (your override file) wins. + +For example, to override `user.modifier_key`, you could create `modifier_keys_MYNAME.talon`: + +```talon +list: user.modifier_key +language: en +- + +# My preferred modifier keys +rose: cmd +troll: control +shift: shift +alt: alt +``` + +## Other Talon user file sets + +In addition to this repo, there are [other Talon user file sets](https://talon.wiki/talon_user_file_sets/) containing additional commands that you may want to experiment with if you're feeling adventurous 😊. Many of them are meant to be used alongside `community`, but a few of them are designed as replacements. If it's not clear which, please file an issue against the given GitHub repository for that user file set! + +# Collaborators + +This repository is now officially a team effort. The following contributors have direct access: + +- @dwiel +- @fidgetingbits +- @knausj85 +- @rntz +- @splondike +- @pokey + +Collaborators will reply to issues and pull requests as time and health permits. Please be patient. + +## Guidelines for collaborators + +1. Collaborators prioritize their health and their personal/professional needs first. Their time commitment to this effort is limited. +2. For "minor" fixes and improvements/bugs/new apps, collaborators are free to contribute without any review +3. For "significant" new development and refactors, collaborators should seek appropriate input and reviews from each-other. Collaborators are encouraged to open a discussion before committing their time to any major effort. + +# Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for our guidelines for contributors + +## Automatic formatting/linters + +This repository uses [`pre-commit`](https://pre-commit.com/) to run and manage its formatters/linters. Running these yourself is optional. If you wish to do so, first [install](https://pre-commit.com/#install) `pre-commit`: + +```bash +$ pip install pre-commit +``` + +You then have a few options as to when to run it: + +- Run yourself at any time on your locally changed files: `pre-commit run` +- Run yourself on all files in the repository: `pre-commit run --all-files` +- Run automatically on your PRs (fixes will be pushed automatically to your branch): + - Visit https://pre-commit.ci/ and authorize the app to connect to your `community` fork. +- Set up an editor hook to run on save: + - You could follow the instructions for [Black](https://black.readthedocs.io/en/stable/integrations/editors.html), which are well written; simply replace `black ` with `pre-commit run --files `. + - It's more performant to only reformat the specific file you're editing, rather than all changed files. +- Install a git pre-commit hook with `pre-commit install` (optional) + - This essentially runs `pre-commit run` automatically before creating local commits, applying formatters/linters on all changed files. If it "fails", the commit will be blocked. + - Note that because many of the rules automatically apply fixes, typically you just need to stage the changes that they made, then reattempt your commit. + - Whether to use the hook comes down to personal taste. If you like to make many small incremental "work" commits developing a feature, it may be too much overhead. + +If you run into setup difficulty with `pre-commit`, you might want to ensure that you have a modern Python 3 local environment first. [pyenv](https://github.com/pyenv/pyenv) is good way to install such Python versions without affecting your system Python (recommend installing 3.9 to match Talon's current version). On macOS you can also `brew install pre-commit`. + +## Automated tests + +There are a number of automated unit tests in the repository. These are all run _outside_ of the Talon environment (e.g. we don't have access to Talon's window management APIs). These make use of a set of stubbed out Talon APIs in `test/stubs/` and a bit of class loader trickery in `conftest.py`. + +To run the test suite you just need to install the `pytest` python package in to a non-Talon Python runtime you want to use for tests (i.e. don't install in the `~/.talon/.venv directory`). You can then just run the `pytest` command from the repository root to execute all the tests. + +## Talon documentation + +For official documentation on Talon's API and features, please visit https://talonvoice.com/docs/. + +For community-generated documentation on Talon, please visit https://talon.wiki/. + +## Alternate installation method: Zip file + +It is possible to install `community` by downloading and extracting a zip file instead of using `git`. Note that this approach is discouraged, because it makes it more difficult to keep track of any changes you may make to your copy of the files. + +If you wish to install `community` by downloading and extracting a zip file, proceed as follows: + +1. Download the [zip archive of community](https://github.com/talonhub/community/archive/refs/heads/main.zip). +1. Extract the files. If you don’t know how to extract zip files, a quick google search for "extract zip files" may be helpful. +1. Place these extracted files inside the `user` folder of the Talon Home directory. You can find this folder by right-clicking the Talon icon in the taskbar (Windows) or clicking the Talon icon in the menu bar (Mac), clicking Scripting > Open ~/talon, and navigating to `user`. diff --git a/community/apps/1password/1password.talon b/community/apps/1password/1password.talon new file mode 100644 index 0000000..6536105 --- /dev/null +++ b/community/apps/1password/1password.talon @@ -0,0 +1,6 @@ +app: one_password +- +password new: user.password_new() +password dup: user.password_duplicate() +password edit: user.password_edit() +password delete: user.password_delete() diff --git a/community/apps/1password/1password_global.talon b/community/apps/1password/1password_global.talon new file mode 100644 index 0000000..f9117be --- /dev/null +++ b/community/apps/1password/1password_global.talon @@ -0,0 +1,4 @@ +#todo: tags +- +password fill: user.password_fill() +password show: user.password_show() diff --git a/community/apps/1password/1password_mac.py b/community/apps/1password/1password_mac.py new file mode 100644 index 0000000..4465a3e --- /dev/null +++ b/community/apps/1password/1password_mac.py @@ -0,0 +1,31 @@ +from talon import Context, actions + +ctx = Context() + +# i don't see a need to restrict the app here, this just defines the actions +# each app can support appropriate voice commands as needed +# the below are for 1password, redefine as needed +ctx.matches = r""" +os: mac +""" + + +@ctx.action_class("user") +class UserActions: + def password_fill(): + actions.key("cmd-\\") + + def password_show(): + actions.key("cmd-alt-\\") + + def password_new(): + actions.key("cmd-i") + + def password_duplicate(): + actions.key("cmd-d") + + def password_edit(): + actions.key("cmd-e") + + def password_delete(): + actions.key("cmd-backspace") diff --git a/community/apps/1password/1password_win.py b/community/apps/1password/1password_win.py new file mode 100644 index 0000000..89a3ff8 --- /dev/null +++ b/community/apps/1password/1password_win.py @@ -0,0 +1,31 @@ +from talon import Context, actions + +ctx = Context() + +# i don't see a need to restrict the app here, this just defines the actions +# each app can support appropriate voice commands as needed +# the below are for 1password, redefine as needed +ctx.matches = r""" +os: windows +""" + + +@ctx.action_class("user") +class UserActions: + def password_fill(): + actions.key("ctrl-\\\\") + + def password_show(): + actions.key("alt-ctrl-\\\\") + + def password_new(): + actions.key("ctrl-n") + + def password_duplicate(): + actions.key("ctrl-d") + + def password_edit(): + actions.key("ctrl-e") + + def password_delete(): + actions.key("ctrl-delete") diff --git a/community/apps/1password/password_manager.py b/community/apps/1password/password_manager.py new file mode 100644 index 0000000..195e07a --- /dev/null +++ b/community/apps/1password/password_manager.py @@ -0,0 +1,29 @@ +from talon import Module + +mod = Module() + +# 1password +mod.apps.one_password = "app.bundle: com.agilebits.onepassword7" +mod.apps.one_password = "app.name: 1Password for Windows desktop" +mod.apps.one_password = "app.name: 1Password.exe" + + +@mod.action_class +class Actions: + def password_fill(): + """fill the password""" + + def password_show(): + """show the password""" + + def password_new(): + """New password""" + + def password_duplicate(): + """Duplicate password""" + + def password_edit(): + """Edit password""" + + def password_delete(): + """Delete password""" diff --git a/community/apps/README.md b/community/apps/README.md new file mode 100644 index 0000000..b0bf4e7 --- /dev/null +++ b/community/apps/README.md @@ -0,0 +1,15 @@ +# Web apps and browser extensions + +Some of the Talon files for web apps (e.g. `apps/github/github_web.talon`) use a `browser.host` matcher. These talon files should work out of the box for Safari, Chrome, Brave, on Mac, but require additional configuration on other browsers/operating systems. + +`community` is set up so that if a URL is found in the titlebar of an application matching the 'browser' tag it will be used to populate the browser.host matcher (see `code/browser.py`). This probably means that you will need an extension to make the browser.host based scripts work. + +Browser extensions that can add the protocol and hostname or even the entire URL to the window title: + +Firefox: + +- https://addons.mozilla.org/en-US/firefox/addon/keepass-helper-url-in-title/ + +Chrome: + +- https://chrome.google.com/webstore/detail/url-in-title/ignpacbgnbnkaiooknalneoeladjnfgb diff --git a/community/apps/adobe/adobe_acrobat_reader_dc.py b/community/apps/adobe/adobe_acrobat_reader_dc.py new file mode 100644 index 0000000..858d21c --- /dev/null +++ b/community/apps/adobe/adobe_acrobat_reader_dc.py @@ -0,0 +1,15 @@ +from talon import Module + +# --- App definition --- +mod = Module() +mod.apps.adobe_acrobat_reader_dc = r""" +os: windows +and app.name: Adobe Acrobat DC +os: windows +and app.exe: /^acrobat\.exe$/i +os: windows +and app.name: Adobe Acrobat Reader DC +os: windows +and app.exe: /^acrord32\.exe$/i +""" +# TODO: mac context and implementation diff --git a/community/apps/adobe/adobe_acrobat_reader_dc.talon b/community/apps/adobe/adobe_acrobat_reader_dc.talon new file mode 100644 index 0000000..0375101 --- /dev/null +++ b/community/apps/adobe/adobe_acrobat_reader_dc.talon @@ -0,0 +1,5 @@ +app: adobe_acrobat_reader_dc +- +# Set tags +tag(): user.tabs +tag(): user.pages diff --git a/community/apps/adobe/adobe_acrobat_reader_dc_win.py b/community/apps/adobe/adobe_acrobat_reader_dc_win.py new file mode 100644 index 0000000..846ec5c --- /dev/null +++ b/community/apps/adobe/adobe_acrobat_reader_dc_win.py @@ -0,0 +1,61 @@ +from talon import Context, actions + +# Context matching +ctx = Context() +ctx.matches = """ +os: windows +app: adobe_acrobat_reader_dc +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_next(): + actions.key("ctrl-tab") + + def tab_previous(): + actions.key("ctrl-shift-tab") + + +@ctx.action_class("edit") +class EditActions: + def zoom_in(): + actions.key("ctrl-0") # in german version + + def zoom_out(): + actions.key("ctrl-1") # in german version TODO: differentiate languages + + def zoom_reset(): + actions.key("ctrl-2") + + +@ctx.action_class("user") +class UserActions: + # user.pages + def page_current(): + actions.key("ctrl-shift-n") + page = actions.edit.selected_text() + actions.key("tab:2 enter") + return int(page) + + def page_next(): + actions.key("ctrl-pagedown") + + def page_previous(): + actions.key("ctrl-pageup") + + def page_jump(number: int): + actions.key("ctrl-shift-n") + actions.insert(str(number)) + actions.key("enter") + + 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/community/apps/amethyst/amethyst.talon b/community/apps/amethyst/amethyst.talon new file mode 100644 index 0000000..d83d1b2 --- /dev/null +++ b/community/apps/amethyst/amethyst.talon @@ -0,0 +1,13 @@ +user.running: amethyst +- +window next: key("alt-shift-j") +window previous: key("alt-shift-k") +# window move desk: key("ctrl-alt-shift-h") +window full: key("alt-shift-d") +window float: key(alt-shift-t) +window tall: key("alt-shift-a") +window middle: key("alt-shift-`") +window move main: key("alt-shift-enter") +window grow: key("alt-shift-l") +window shrink: key("alt-shift-h") +window reevaluate: key("alt-shift-z") diff --git a/community/apps/anaconda/anaconda.py b/community/apps/anaconda/anaconda.py new file mode 100644 index 0000000..791f91e --- /dev/null +++ b/community/apps/anaconda/anaconda.py @@ -0,0 +1,9 @@ +from talon import Context, Module + +mod = Module() +mod.tag("anaconda", desc="tag for enabling anaconda commands in your terminal") + +ctx = Context() +ctx.matches = r""" +tag: user.anaconda +""" diff --git a/community/apps/anaconda/anaconda.talon b/community/apps/anaconda/anaconda.talon new file mode 100644 index 0000000..c2354bb --- /dev/null +++ b/community/apps/anaconda/anaconda.talon @@ -0,0 +1,40 @@ +tag: terminal +and tag: user.anaconda +- +anaconda: "conda " +anaconda help: "conda --help\n" +anaconda version: "conda --version\n" + +anaconda environment list: "conda env list\n" +anaconda environment create: "conda env create -f " +anaconda environment remove: "conda env remove -n " + +anaconda activate: "conda activate " +anaconda clean: "conda clean " +anaconda compare: "conda compare " +anaconda config: "conda config " +anaconda create: "conda create " +anaconda info: "conda info " +anaconda init: "conda init " +anaconda install: "conda install " +anaconda list: "conda list " +anaconda package: "conda package " +anaconda remove: "conda remove " +anaconda uninstall: "conda uninstall " +anaconda run: "conda run " +anaconda search: "conda search " +anaconda update: "conda update " +anaconda upgrade: "conda upgrade " + +anaconda build: "conda build " +anaconda convert: "conda convert " +anaconda debug: "conda debug " +anaconda develop: "conda develop " +anaconda environment: "conda env " +anaconda index: "conda index " +anaconda inspect: "conda inspect " +anaconda metapackage: "conda metapackage " +anaconda render: "conda render " +anaconda server: "conda server " +anaconda skeleton: "conda skeleton " +anaconda verify: "conda verify " diff --git a/community/apps/apple_notes/apple_notes.py b/community/apps/apple_notes/apple_notes.py new file mode 100644 index 0000000..5f2920a --- /dev/null +++ b/community/apps/apple_notes/apple_notes.py @@ -0,0 +1,21 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +app: notes +""" + + +@ctx.action_class("edit") +class EditActions: + def zoom_in(): + actions.key("shift-cmd->") + + def zoom_out(): + actions.key("shift-cmd-<") + + def zoom_reset(): + actions.key("shift-cmd-0") + + def indent_less(): + actions.key("cmd-[") diff --git a/community/apps/apple_notes/apple_notes.talon b/community/apps/apple_notes/apple_notes.talon new file mode 100644 index 0000000..ba4e37f --- /dev/null +++ b/community/apps/apple_notes/apple_notes.talon @@ -0,0 +1,32 @@ +os: mac +and app: notes +- + +new note: key(cmd-n) +duplicate note: key(cmd-d) +new folder: key(shift-cmd-n) +toggle folders: key(alt-cmd-s) +show main: key(cmd-0) +list view: key(cmd-1) +gallery view: key(cmd-2) +toggle attachments: key(cmd-3) +find all: key(alt-cmd-f) +print note: key(cmd-p) +attach file: key(shift-cmd-a) +create link: key(cmd-k) +insert table: key(alt-cmd-t) +apply title: key(shift-cmd-t) +apply heading: key(shift-cmd-h) +apply subheading: key(shift-cmd-j) +apply body: key(shift-cmd-b) +apply mono: key(shift-cmd-m) +apply bullet: key(shift-cmd-7) +apply dash: key(shift-cmd-8) +apply number: key(shift-cmd-9) +apply checklist: key(shift-cmd-l) +increase font: key(cmd-+) +decrease font: key(cmd--) +line break: key(ctrl-enter) +mark: key(shift-cmd-u) +drag [line] down: key('ctrl-cmd-down') +drag [line] up: key('ctrl-cmd-up') diff --git a/community/apps/apple_terminal/apple_terminal.py b/community/apps/apple_terminal/apple_terminal.py new file mode 100644 index 0000000..4d10e6d --- /dev/null +++ b/community/apps/apple_terminal/apple_terminal.py @@ -0,0 +1,90 @@ +import os + +from talon import Context, actions, ui + +# TODO: fit this to terminal.py + +ctx = Context() +ctx.matches = r""" +app: apple_terminal +""" +directories_to_remap = {} +directories_to_exclude = {} + + +@ctx.action_class("edit") +class EditActions: + def delete_line(): + actions.key("ctrl-u") + + +@ctx.action_class("user") +class UserActions: + def file_manager_current_path(): + title = ui.active_window().title + + # take the first split for the zsh-based terminal + if " — " in title: + title = title.split(" — ")[0] + + if "~" in title: + title = os.path.expanduser(title) + + if title in directories_to_remap: + title = directories_to_remap[title] + + if title in directories_to_exclude: + title = None + + return title + + def file_manager_show_properties(): + """Shows the properties for the file""" + + def file_manager_open_directory(path: str): + """opens the directory that's already visible in the view""" + actions.insert("cd ") + path = f'"{path}"' + actions.insert(path) + actions.key("enter") + + # jtk - refresh title isn't necessary since the apple terminal does it for us + # actions.user.file_manager_refresh_title() + + def file_manager_open_parent(): + actions.insert("cd ..") + actions.key("enter") + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(path) + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + name = f'"{name}"' + + actions.insert("mkdir " + name) + + def file_manager_open_file(path: str): + """opens the file""" + actions.insert(path) + actions.key("enter") + + def file_manager_select_file(path: str): + """selects the file""" + actions.insert(path) + + def file_manager_refresh_title(): + return + + +@ctx.action_class("app") +class app_actions: + # other tab functions should already be implemented in + # code/platforms/mac/app.py + + def tab_previous(): + actions.key("ctrl-shift-tab") + + def tab_next(): + actions.key("ctrl-tab") diff --git a/community/apps/apple_terminal/apple_terminal.talon b/community/apps/apple_terminal/apple_terminal.talon new file mode 100644 index 0000000..2d0f6d1 --- /dev/null +++ b/community/apps/apple_terminal/apple_terminal.talon @@ -0,0 +1,25 @@ +app: apple_terminal +- +# makes the commands in terminal.talon available +tag(): terminal + +# use readline keybindings for various editing commands +tag(): user.readline + +# activates the implementation of the commands/functions in terminal.talon +tag(): user.generic_unix_shell + +# makes commands for certain applications available +# you can deactivate them if you do not use the application +tag(): user.git +tag(): user.anaconda +tag(): user.kubectl + +# TODO: explain +tag(): user.tabs +tag(): user.file_manager + +suspend: key(ctrl-z) +resume: + insert("fg") + key(enter) diff --git a/community/apps/arc/arc.py b/community/apps/arc/arc.py new file mode 100644 index 0000000..c89731e --- /dev/null +++ b/community/apps/arc/arc.py @@ -0,0 +1,34 @@ +from talon import Context, Module, actions, app + +ctx = Context() +mod = Module() + +mod.apps.arc = "app.name: Arc" +mod.apps.arc = """ +os: mac +app.bundle: company.thebrowser.Browser + +""" +ctx.matches = r""" +app: arc +""" + + +@ctx.action_class("user") +class UserActions: + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + def command_search(command: str = ""): + actions.key("cmd-l") + if command != "": + actions.sleep("200ms") + actions.insert(command) + + +@ctx.action_class("browser") +class BrowserActions: + def show_extensions(): + actions.app.tab_open() + actions.browser.go("arc://extensions") diff --git a/community/apps/arc/arc_mac.talon b/community/apps/arc/arc_mac.talon new file mode 100644 index 0000000..0fa8d5d --- /dev/null +++ b/community/apps/arc/arc_mac.talon @@ -0,0 +1,6 @@ +app: arc +os: mac +- +tag(): browser +tag(): user.tabs +tag(): user.command_search diff --git a/community/apps/arc/little-arc_mac.talon b/community/apps/arc/little-arc_mac.talon new file mode 100644 index 0000000..bec24f9 --- /dev/null +++ b/community/apps/arc/little-arc_mac.talon @@ -0,0 +1,8 @@ +user.running: arc +os: mac +- +# This assumes that you have not disabled Little Arc +little arc []: + key("cmd-alt-n") + sleep(200ms) + insert(user.text or "") diff --git a/community/apps/atril/atril.py b/community/apps/atril/atril.py new file mode 100644 index 0000000..daaa70b --- /dev/null +++ b/community/apps/atril/atril.py @@ -0,0 +1,39 @@ +from talon import Context, Module, actions + +# --- App definition --- +mod = Module() +mod.apps.atril = """ +os: linux +and app.name: Atril +""" + +# Context matching +ctx = Context() +ctx.matches = r""" +app: atril +""" + + +# --- Implement actions --- +@ctx.action_class("user") +class UserActions: + # user.pages + def page_current(): + actions.key("ctrl-l") + page = actions.edit.selected_text() + actions.key("right escape") + return int(page) + + def page_next(): + actions.key("ctrl-pagedown") + + def page_previous(): + actions.key("ctrl-pageup") + + def page_jump(number: int): + actions.key("ctrl-l") + actions.insert(str(number)) + actions.key("enter") + + def page_final(): + actions.key("ctrl-end") diff --git a/community/apps/atril/atril.talon b/community/apps/atril/atril.talon new file mode 100644 index 0000000..5b87d7e --- /dev/null +++ b/community/apps/atril/atril.talon @@ -0,0 +1,4 @@ +app: atril +- +# Set tags +tag(): user.pages diff --git a/community/apps/brave/brave.py b/community/apps/brave/brave.py new file mode 100644 index 0000000..b89a57d --- /dev/null +++ b/community/apps/brave/brave.py @@ -0,0 +1,32 @@ +from talon import Context, Module, actions, app + +ctx = Context() +mod = Module() + +mod.apps.brave = "app.name: Brave Browser" +mod.apps.brave = "app.name: Brave-browser" +mod.apps.brave = r""" +os: windows +and app.exe: /^brave\.exe$/i +os: linux +and app.exe: brave +os: mac +and app.bundle: com.brave.Browser +""" +ctx.matches = r""" +app: brave +""" + + +@ctx.action_class("user") +class UserActions: + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + +@ctx.action_class("browser") +class BrowserActions: + def show_extensions(): + actions.app.tab_open() + actions.browser.go("brave://extensions") diff --git a/community/apps/brave/brave.talon b/community/apps/brave/brave.talon new file mode 100644 index 0000000..11cc1af --- /dev/null +++ b/community/apps/brave/brave.talon @@ -0,0 +1,4 @@ +app: brave +- +tag(): browser +tag(): user.tabs diff --git a/community/apps/calibre/calibre.py b/community/apps/calibre/calibre.py new file mode 100644 index 0000000..effa604 --- /dev/null +++ b/community/apps/calibre/calibre.py @@ -0,0 +1,19 @@ +from talon import Module + +# --- App definition --- +mod = Module() +mod.apps.calibre = r""" +os: windows +and app.name: calibre.exe +os: windows +and app.exe: /^calibre\.exe$/i +os: windows +and app.name: calibre-parallel.exe +os: windows +and app.exe: /^calibre-parallel\.exe$/i +""" +mod.apps.calibre = """ +os: linux +app.name: calibre +""" +# TODO: mac context diff --git a/community/apps/calibre/calibre_viewer.py b/community/apps/calibre/calibre_viewer.py new file mode 100644 index 0000000..52bda95 --- /dev/null +++ b/community/apps/calibre/calibre_viewer.py @@ -0,0 +1,39 @@ +from talon import Context, Module, actions + +# --- App definition --- +mod = Module() +mod.apps.calibre_viewer = """ +app: calibre +title: /E-book viewer$/ +title: /eBook-Betrachter$/ +""" + +# Context matching +ctx = Context() +ctx.matches = """ +os: windows +os: linux +app: calibre_viewer +""" +# TODO: mac implementation + + +# --- Implement actions --- +@ctx.action_class("user") +class UserActions: + # user.pages + def page_next(): + actions.key("pagedown") + + def page_previous(): + actions.key("pageup") + + def page_final(): + actions.key("ctrl-end") + + # user.chapters + def chapter_next(): + actions.key("ctrl-pagedown") + + def chapter_previous(): + actions.key("ctrl-pageup") diff --git a/community/apps/calibre/calibre_viewer.talon b/community/apps/calibre/calibre_viewer.talon new file mode 100644 index 0000000..bffed5d --- /dev/null +++ b/community/apps/calibre/calibre_viewer.talon @@ -0,0 +1,5 @@ +app: calibre_viewer +- +# Set tags +tag(): user.pages +tag(): user.chapters diff --git a/community/apps/chrome/chrome.py b/community/apps/chrome/chrome.py new file mode 100644 index 0000000..5a429a4 --- /dev/null +++ b/community/apps/chrome/chrome.py @@ -0,0 +1,54 @@ +from talon import Context, Module, actions, app + +ctx = Context() +mod = Module() + +mod.apps.chrome = "app.name: Google Chrome" +mod.apps.chrome = r""" +os: windows +and app.exe: /^chrome\.exe$/i +""" +mod.apps.chrome = """ +os: mac +app.bundle: com.google.Chrome +app.bundle: com.google.Chrome.canary +app.bundle: org.chromium.Chromium +""" +mod.apps.chrome = """ +os: linux +app.exe: chrome +app.exe: chromium-browser +app.exe: chromium +""" +mod.apps.chrome = """ +os: linux +and app.name: Google-chrome +""" + +ctx.matches = r""" +app: chrome +""" + + +@mod.action_class +class Actions: + def chrome_mod(key: str): + """Press the specified key with the correct modifier key for the OS""" + if app.platform == "mac": + actions.key(f"cmd-{key}") + else: + actions.key(f"ctrl-{key}") + + +@ctx.action_class("user") +class UserActions: + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + +@ctx.action_class("browser") +class BrowserActions: + def show_extensions(): + actions.app.tab_open() + actions.browser.go("chrome://extensions") diff --git a/community/apps/chrome/chrome.talon b/community/apps/chrome/chrome.talon new file mode 100644 index 0000000..5626f96 --- /dev/null +++ b/community/apps/chrome/chrome.talon @@ -0,0 +1,14 @@ +app: chrome +- +tag(): browser +tag(): user.tabs + +profile switch: user.chrome_mod("shift-m") + +tab search: user.chrome_mod("shift-a") + +tab search $: + user.chrome_mod("shift-a") + sleep(200ms) + insert("{text}") + key(down) diff --git a/community/apps/conemu/conemu.talon b/community/apps/conemu/conemu.talon new file mode 100644 index 0000000..e6cfbc8 --- /dev/null +++ b/community/apps/conemu/conemu.talon @@ -0,0 +1,6 @@ +os: windows +app.exe: /^conemu64\.exe$/i +- + +tag(): terminal +tag(): user.git diff --git a/community/apps/discord/discord.py b/community/apps/discord/discord.py new file mode 100644 index 0000000..a5836dc --- /dev/null +++ b/community/apps/discord/discord.py @@ -0,0 +1,85 @@ +from talon import Context, Module, actions + +mod = Module() +apps = mod.apps +apps.discord = "app.bundle: com.hnc.Discord" +apps.discord = "app.name: Discord" +apps.discord = "app.name: Discord.exe" +apps.discord = """ +tag: browser +browser.host: discord.com +""" + +mod.list("discord_destination", desc="discord destination") + +ctx = Context() +ctx.matches = r""" +app: discord +""" + +ctx.lists["user.discord_destination"] = { + "user": "@", + "voice": "!", + "server": "*", +} + + +@mod.action_class +class discord_actions: + def discord_mentions_last(): + """Go up to channel with unread mentions""" + + def discord_mentions_next(): + """Go down to channel with unread mentions""" + + def discord_oldest_unread(): + """Go to oldest unread message""" + + def discord_toggle_pins(): + """Toggle pins popout""" + + def discord_toggle_inbox(): + """Toggle inbox popout""" + + def discord_toggle_members(): + """Toggle channel member list""" + + def discord_emoji_picker(): + """Toggle emoji picker""" + + def discord_gif_picker(): + """Toggle gif picker""" + + def discord_sticker_picker(): + """Toggle sticker picker""" + + def discord_mark_inbox_read(): + """Mark top inbox channel read""" + + def discord_mute(): + """Toggle mute""" + + def discord_deafen(): + """Toggle deafen""" + + def discord_answer_call(): + """Answer incoming call""" + + def discord_decline_call(): + """Decline incoming call""" + + def discord_quick_switcher(dest_type: str, dest_search: str): + """Open up the quick switcher, optionally specifying a type of destination""" + + def discord_go_current_call(): + """Go to current call""" + + def discord_toggle_dms(): + """Toggle between dms and your most recent server""" + + +@ctx.action_class("user") +class UserActions: + # Navigation: Channels + def messaging_open_channel_picker(): + actions.user.discord_quick_switcher("#", "") diff --git a/community/apps/discord/discord.talon b/community/apps/discord/discord.talon new file mode 100644 index 0000000..3dae6c1 --- /dev/null +++ b/community/apps/discord/discord.talon @@ -0,0 +1,34 @@ +app: discord +- +tag(): user.messaging +tag(): user.emoji + +# Navigation: QuickSwitcher +{user.discord_destination} []: + user.discord_quick_switcher(user.discord_destination, user.text or "") +switcher: user.discord_quick_switcher("", "") + +# Navigation: Channels +[channel] mentions last: user.discord_mentions_last() +[channel] mentions next: user.discord_mentions_next() +oldest unread: user.discord_oldest_unread() +current call: user.discord_go_current_call() + +# UI +toggle pins: user.discord_toggle_pins() +toggle inbox: user.discord_toggle_inbox() +toggle (members | member list): user.discord_toggle_members() +toggle (dee ems | dims): user.discord_toggle_dms() +pick emoji: user.discord_emoji_picker() +pick (jif | gif | gift): user.discord_gif_picker() +pick sticker: user.discord_sticker_picker() + +# Misc +mark inbox channel read: user.discord_mark_inbox_read() +[toggle] (mute | unmute): user.discord_mute() +(mute | unmute) and sleep: + user.discord_mute() + speech.disable() +[toggle] (deafen | undeafen): user.discord_deafen() +answer call: user.discord_answer_call() +decline call: user.discord_decline_call() diff --git a/community/apps/discord/discord_mac.py b/community/apps/discord/discord_mac.py new file mode 100644 index 0000000..82a58fd --- /dev/null +++ b/community/apps/discord/discord_mac.py @@ -0,0 +1,96 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: mac +app: discord +""" + + +@ctx.action_class("user") +class UserActions: + # Navigation: QuickSwitcher + def discord_quick_switcher(dest_type: str, dest_search: str): + actions.key("cmd-k") + actions.insert(dest_type) + if dest_search: + actions.insert(dest_search) + + # Navigation: Servers + def messaging_workspace_previous(): + actions.key("cmd-alt-up") + + def messaging_workspace_next(): + actions.key("cmd-alt-down") + + # Navigation: Channels + def messaging_channel_previous(): + actions.key("alt-up") + + def messaging_channel_next(): + actions.key("alt-down") + + def messaging_unread_previous(): + actions.key("alt-shift-up") + + def messaging_unread_next(): + actions.key("alt-shift-down") + + def discord_mentions_last(): + actions.key("cmd-alt-shift-up") + + def discord_mentions_next(): + actions.key("cmd-alt-shift-down") + + def discord_oldest_unread(): + actions.key("shift-pageup") + + # UI + def discord_toggle_pins(): + actions.key("cmd-p") + + def discord_toggle_inbox(): + actions.key("cmd-i") + + def discord_toggle_members(): + actions.key("cmd-u") + + def discord_emoji_picker(): + actions.key("cmd-e") + + def discord_gif_picker(): + actions.key("cmd-g") + + def discord_sticker_picker(): + actions.key("cmd-s") + + # Misc + def messaging_mark_workspace_read(): + actions.key("shift-esc") + + def messaging_mark_channel_read(): + actions.key("esc") + + def messaging_upload_file(): + actions.key("cmd-shift-u") + + def discord_mark_inbox_read(): + actions.key("cmd-shift-e") + + def discord_mute(): + actions.key("cmd-shift-m") + + def discord_deafen(): + actions.key("cmd-shift-d") + + def discord_answer_call(): + actions.key("cmd-enter") + + def discord_decline_call(): + actions.key("esc") + + def discord_go_current_call(): + actions.key("cmd-alt-a") + + def discord_toggle_dms(): + actions.key("cmd-alt-right") diff --git a/community/apps/discord/discord_win.py b/community/apps/discord/discord_win.py new file mode 100644 index 0000000..2f7246f --- /dev/null +++ b/community/apps/discord/discord_win.py @@ -0,0 +1,97 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: windows +os: linux +app: discord +""" + + +@ctx.action_class("user") +class UserActions: + # Navigation: QuickSwitcher + def discord_quick_switcher(dest_type: str, dest_search: str): + actions.key("ctrl-k") + actions.insert(dest_type) + if dest_search: + actions.insert(dest_search) + + # Navigation: Servers + def messaging_workspace_previous(): + actions.key("ctrl-alt-up") + + def messaging_workspace_next(): + actions.key("ctrl-alt-down") + + # Navigation: Channels + def messaging_channel_previous(): + actions.key("alt-up") + + def messaging_channel_next(): + actions.key("alt-down") + + def messaging_unread_previous(): + actions.key("alt-shift-up") + + def messaging_unread_next(): + actions.key("alt-shift-down") + + def discord_mentions_last(): + actions.key("ctrl-alt-shift-up") + + def discord_mentions_next(): + actions.key("ctrl-alt-shift-down") + + def discord_oldest_unread(): + actions.key("shift-pageup") + + # UI + def discord_toggle_pins(): + actions.key("ctrl-p") + + def discord_toggle_inbox(): + actions.key("ctrl-i") + + def discord_toggle_members(): + actions.key("ctrl-u") + + def discord_emoji_picker(): + actions.key("ctrl-e") + + def discord_gif_picker(): + actions.key("ctrl-g") + + def discord_sticker_picker(): + actions.key("ctrl-s") + + # Misc + def messaging_mark_workspace_read(): + actions.key("shift-esc") + + def messaging_mark_channel_read(): + actions.key("esc") + + def messaging_upload_file(): + actions.key("ctrl-shift-u") + + def discord_mark_inbox_read(): + actions.key("ctrl-shift-e") + + def discord_mute(): + actions.key("ctrl-shift-m") + + def discord_deafen(): + actions.key("ctrl-shift-d") + + def discord_answer_call(): + actions.key("ctrl-enter") + + def discord_decline_call(): + actions.key("esc") + + def discord_go_current_call(): + actions.key("ctrl-shift-alt-v") + + def discord_toggle_dms(): + actions.key("ctrl-alt-right") diff --git a/community/apps/dock/dock.py b/community/apps/dock/dock.py new file mode 100644 index 0000000..99f8fa9 --- /dev/null +++ b/community/apps/dock/dock.py @@ -0,0 +1,50 @@ +from pathlib import Path +from typing import Optional + +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""" + + 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 + + dock_notify(notification) diff --git a/community/apps/dock/dock.talon b/community/apps/dock/dock.talon new file mode 100644 index 0000000..896c002 --- /dev/null +++ b/community/apps/dock/dock.talon @@ -0,0 +1,5 @@ +os: mac +- +^desktop$: user.dock_send_notification("com.apple.showdesktop.awake") +^window$: user.dock_app_expose() +^launch pad$: user.dock_send_notification("com.apple.launchpad.toggle") diff --git a/community/apps/dunst/dunst.talon b/community/apps/dunst/dunst.talon new file mode 100644 index 0000000..27df9ea --- /dev/null +++ b/community/apps/dunst/dunst.talon @@ -0,0 +1,9 @@ +os: linux +- + +show notifications: key(ctrl-`) +dismiss [notifications]: user.system_command("dunstctl close") +dismiss all [notifications]: user.system_command("dunstctl close-all") +#dunce pause: user.system_command('notify-send "DUNST_COMMAND_PAUSE"') +#dunce resume: user.system_command('notify-send "DUNST_COMMAND_RESUME"') +#test notification: user.system_command('notify-send "Hello from Talon"') diff --git a/community/apps/eclipse/eclipse.talon b/community/apps/eclipse/eclipse.talon new file mode 100644 index 0000000..f399510 --- /dev/null +++ b/community/apps/eclipse/eclipse.talon @@ -0,0 +1,144 @@ +#custom eclipse commands go here +app: eclipse +- +tag(): user.find_and_replace +tag(): user.line_commands +# tag(): user.multiple_cursors +tag(): user.splits +tag(): user.tabs +tag(): user.command_search +# splits.py support end + +# Sidebar +bar explore: key(alt-shift-w p) +# bar extensions: +bar outline: key(alt-shift-q o) + +# bar run: + +# bar source: +# bar switch: + +# Panels +# panel control: +panel output: + key(alt-shift-q) + sleep(200ms) + key(c) +panel problems: + key(alt-shift-q) + sleep(200ms) + key(x) +panel errors: + key(alt-shift-q) + sleep(200ms) + key(l) +panel breakpoints: + key(alt-shift-q) + sleep(200ms) + key(b) +panel search: + key(alt-shift-q) + sleep(200ms) + key(s) +panel variables: + key(alt-shift-q) + sleep(200ms) + key(v) +# panel switch: +# panel terminal: + +# Settings +show settings: key(alt-w p) +show shortcuts: key(ctrl-shift-l) +#show snippets: + +# Display +# centered switch: +# fullscreen switch: +# theme switch: +# wrap switch: +# zen switch: + +# File Commands +file hunt []: + key(ctrl-shift-r) + sleep(50ms) + insert(text or "") +# file copy path: +# file create sibling: +file create: key(ctrl-n) +file open folder: key(alt-shift-w x) +file rename: key(alt-shift-w p enter f2) +file reveal: key(alt-shift-w p enter) + +# Language Features +# suggest show: +# hint show: +# definition show: +# definition peek: +# definition side: +# references show: +# references find: +# format that: +# format selection: +imports fix: key(ctrl-shift-o) +# problem last: +# problem fix: +# rename that: +# refactor that: +# whitespace trim: +# language switch: +refactor rename: key(alt-shift-r) +refactor this: key(alt-shift-i) + +#code navigation +(go declaration | follow): key(f3) +go back: key(alt-left) +go forward: key(alt-right) +# go implementation: +# go recent: +# go type: +# go usage: + +# Bookmarks. +#requires https://marketplace.eclipse.org/content/quick-bookmarks +go marks: key(alt-end) +toggle mark: key(ctrl-alt-b down enter) +go next mark: key(alt-pagedown) +go last mark: key(alt-pageup) + +# Folding +# fold that: +# unfold that: +# fold those: +# unfold those: +# fold all: +# unfold all: +# fold comments: + +#Debugging +break point: key(ctrl-shift-b) +step over: key(f6) +debug step into: key(f5) +debug step out [of]: key(f7) +#debug start: user.vscode("workbench.action.debug.start") +#debug pause: +#debug stopper: +debug continue: key(f8) +#debug restart: + +# Terminal +# terminal external: user.vscode("workbench.action.terminal.openNativeConsole") + +# terminal new: user.vscode("workbench.action.terminal.new") +# terminal next: user.vscode("workbench.action.terminal.focusNextPane") +# terminal last:user.vscode("workbench.action.terminal.focusPreviousPane") +# terminal split: user.vscode("workbench.action.terminal.split") +# terminal trash: user.vscode("Terminal:Kill") +# terminal scroll up: user.vscode("Terminal:ScrollUp") +# terminal scroll down: user.vscode("Terminal:ScrollDown") + +#TODO: should this be added to linecommands? +copy line down: key(ctrl-alt-down) +copy line up: key(ctrl-alt-up) diff --git a/community/apps/eclipse/eclipse_win.py b/community/apps/eclipse/eclipse_win.py new file mode 100644 index 0000000..3346fa0 --- /dev/null +++ b/community/apps/eclipse/eclipse_win.py @@ -0,0 +1,175 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.apps.eclipse = """ +os: windows +and app.name: eclipse.exe +""" + +ctx.matches = r""" +app: eclipse +""" + + +@ctx.action_class("app") +class AppActions: + # talon app actions + def tab_close(): + actions.key("ctrl-w") + + def tab_next(): + actions.key("ctrl-pagedown") + + def tab_previous(): + actions.key("ctrl-pageup") + + # action(app.tab_reopen): + def window_close(): + actions.key("alt-f4") + + def window_open(): + actions.key("alt-w n") + + +@ctx.action_class("code") +class CodeActions: + # talon code actions + def toggle_comment(): + actions.key("ctrl-7") + + +@ctx.action_class("edit") +class EditActions: + def find_next(): + actions.key("enter") + + def find_previous(): + actions.key("shift-enter") + + def line_swap_up(): + actions.key("alt-up") + + def line_swap_down(): + actions.key("alt-down") + + def line_clone(): + actions.key("ctrl-alt-down") + + def jump_line(n: int): + actions.key("ctrl-l") + actions.insert(str(n)) + actions.key("enter") + + def delete_line(): + actions.key("ctrl-d") + + def indent_more(): + actions.key("tab") + + def indent_less(): + actions.key("shift-tab") + + def save_all(): + actions.key("ctrl-shift-s") + + +@ctx.action_class("user") +class UserActions: + # splits.py support begin + # requires https://marketplace.eclipse.org/content/handysplit + def split_clear_all(): + actions.key("alt-shift-s f") + + def split_clear(): + actions.key("alt-shift-s f") + + # action(user.split_flip): + def split_last(): + actions.key("alt-shift-s t") + + def split_next(): + actions.key("alt-shift-s t") + + def split_window_down(): + actions.key("alt-shift-s m") + + def split_window_horizontally(): + actions.key("alt-ctrl-s s") + + def split_window_right(): + actions.key("alt-shift-s m") + + def split_window_up(): + actions.key("alt-shift-s m") + + def split_window_vertically(): + actions.key("alt-shift-s s") + + def split_window(): + actions.key("alt-ctrl-s s") + + def command_search(command: str = ""): + actions.key("ctrl-3") + if command != "": + actions.insert(command) + + # splits.py support end + + # find_and_replace.py support begin + + def find_everywhere(text: str): + """Triggers find across project""" + actions.key("ctrl-h") + + if text: + actions.insert(text) + + # todo: these commands should only be available + # when it's focused + def find_toggle_match_by_case(): + """Toggles find match by case sensitivity""" + actions.key("alt-c") + + def find_toggle_match_by_word(): + """Toggles find match by whole words""" + actions.key("alt-w") + + def find_toggle_match_by_regex(): + """Toggles find match by regex""" + actions.key("alt-e") + + def replace(text: str): + """Search and replaces in the active editor""" + actions.key("ctrl-f") + + if text: + actions.insert(text) + + def replace_everywhere(text: str): + """Search and replaces in the entire project""" + actions.key("alt-a f") + + if text: + actions.insert(text) + + def replace_confirm(): + """Confirm replace at current position""" + actions.key("alt-r") + + def replace_confirm_all(): + """Confirm replace all""" + actions.key("alt-a") + + def select_previous_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("alt-b alt-f enter esc") + + def select_next_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("alt-f alt-o esc") + + # find_and_replace.py support end diff --git a/community/apps/edge/edge.py b/community/apps/edge/edge.py new file mode 100644 index 0000000..a7f72f1 --- /dev/null +++ b/community/apps/edge/edge.py @@ -0,0 +1,28 @@ +from talon import Context, Module, actions + +mod = Module() +ctx = Context() + +mod.apps.microsoft_edge = r""" +os: windows +and app.name: msedge.exe +os: windows +and app.name: Microsoft Edge +os: windows +and app.exe: /^msedge\.exe$/i +os: mac +and app.bundle: com.microsoft.edgemac +os: linux +and app.exe: msedge +""" + +ctx.matches = r""" +app: microsoft_edge +""" + + +@ctx.action_class("browser") +class BrowserActions: + def show_extensions(): + actions.app.tab_open() + actions.browser.go("edge://extensions") diff --git a/community/apps/edge/edge.talon b/community/apps/edge/edge.talon new file mode 100644 index 0000000..7bfee4e --- /dev/null +++ b/community/apps/edge/edge.talon @@ -0,0 +1,4 @@ +app: microsoft_edge +- +tag(): browser +tag(): user.tabs diff --git a/community/apps/emacs/emacs.py b/community/apps/emacs/emacs.py new file mode 100644 index 0000000..9488db5 --- /dev/null +++ b/community/apps/emacs/emacs.py @@ -0,0 +1,363 @@ +import logging +from typing import Optional + +from talon import Context, Module, actions, settings + +mod = Module() +mod.setting( + "emacs_meta", + type=str, + default="esc", + desc="""What to use for the meta key in emacs. Defaults to 'esc', since that should work everywhere. Other options are 'alt' and 'cmd'.""", +) + +mod.apps.emacs = "app.name: Emacs" +mod.apps.emacs = "app.name: emacs" +mod.apps.emacs = "app.name: /^GNU Emacs/" +mod.apps.emacs = """ +os: mac +app.bundle: org.gnu.Emacs +""" +mod.apps.emacs = r""" +os: windows +app.exe: /^emacs\.exe$/i +""" + +ctx = Context() +ctx.matches = "app: emacs" + + +def meta(keys): + m = settings.get("user.emacs_meta") + if m == "alt": + return " ".join("alt-" + k for k in keys.split()) + elif m == "cmd": + return " ".join("cmd-" + k for k in keys.split()) + elif m != "esc": + logging.error( + f"Unrecognized 'emacs_meta' setting: {m!r}. Falling back to 'esc'." + ) + return "esc " + keys + + +def meta_fixup(k): + if k.startswith("meta-"): + k = meta(k[len("meta-") :]) + elif "meta-" in k: + raise NotImplementedError("user.emacs_key(): please put meta- first") + return k + + +@mod.action_class +class Actions: + def emacs_meta(key: str): + "Presses some keys modified by Emacs' meta key." + actions.key(meta(key)) + + def emacs_key(keys: str): + """ + Presses some keys, translating 'meta-' prefix to the appropriate keys. For + example, if the setting user.emacs_meta = 'esc', user.emacs_key("meta-ctrl-a") + becomes key("esc ctrl-a"). + """ + # TODO: handle corner-cases like key(" ") and key("ctrl- "), etc. + actions.key(" ".join(meta_fixup(k) for k in keys.split())) + + def emacs_prefix(n: Optional[int] = None): + "Inputs a prefix argument." + if n is None: + # `M-x universal-argument` doesn't have the same effect as pressing the key. + prefix_key = actions.user.emacs_command_keybinding("universal-argument") + actions.key(prefix_key or "ctrl-u") # default to ctrl-u + else: + # Applying meta to each key can use fewer keypresses and 'works' in ansi-term + # mode. + actions.user.emacs_meta(" ".join(str(n))) + + def emacs(command_name: str, prefix: Optional[int] = None): + """ + Runs the emacs command `command_name`. Defaults to using M-x, but may use + a key binding if known or rpc if available. Provides numeric prefix argument + `prefix` if specified. + """ + meta_x = actions.user.emacs_command_keybinding("execute-extended-command") + keys = actions.user.emacs_command_keybinding(command_name) + short_form = actions.user.emacs_command_short_form(command_name) + if prefix is not None: + actions.user.emacs_prefix(prefix) + if keys is not None: + actions.user.emacs_key(keys) + else: + actions.user.emacs_key(meta_x or "meta-x") + actions.insert(short_form or command_name) + actions.key("enter") + + def emacs_help(key: str = None): + "Runs the emacs help command prefix, optionally followed by some keys." + # NB. f1 works in ansi-term mode; C-h doesn't. + actions.key("f1") + if key is not None: + actions.key(key) + + +@ctx.action_class("user") +class UserActions: + def cut_line(): + actions.edit.line_start() + actions.user.emacs("kill-line", 1) + + def split_window(): + actions.user.emacs("split-window-below") + + def split_window_vertically(): + actions.user.emacs("split-window-below") + + def split_window_up(): + actions.user.emacs("split-window-below") + + def split_window_down(): + actions.user.emacs("split-window-below") + actions.user.emacs("other-window") + + def split_window_horizontally(): + actions.user.emacs("split-window-right") + + def split_window_left(): + actions.user.emacs("split-window-right") + + def split_window_right(): + actions.user.emacs("split-window-right") + actions.user.emacs("other-window") + + def split_clear(): + actions.user.emacs("delete-window") + + def split_clear_all(): + actions.user.emacs("delete-other-windows") + + def split_reset(): + actions.user.emacs("balance-windows") + + def split_next(): + actions.user.emacs("other-window") + + def split_last(): + actions.user.emacs("other-window", -1) + + def split_flip(): + # only works reliably if there are only two panes/windows. + actions.key("ctrl-x b enter ctrl-x o ctrl-x b enter") + actions.user.split_last() + actions.key("ctrl-x b enter ctrl-x o") + + def select_range(line_start, line_end): + # Assumes transient mark mode. + actions.edit.jump_line(line_start) + actions.edit.jump_line(line_end + 1) + actions.user.emacs("exchange-point-and-mark") + + # # Version that highlights without transient-mark-mode: + # def select_range(line_start, line_end): + # actions.edit.jump_line(line_end + 1) + # actions.key("ctrl-@ ctrl-@") + # actions.edit.jump_line(line_start) + + # dictation_peek() probably won't work in a terminal. PRs welcome. + def dictation_peek(left, right): + # clobber transient selection if it exists + actions.key("space backspace") + before, after = None, None + if left: + actions.edit.extend_word_left() + before = actions.edit.selected_text() + actions.user.emacs("pop-to-mark-command") + if right: + actions.edit.extend_line_end() + after = actions.edit.selected_text() + actions.user.emacs("pop-to-mark-command") + return (before, after) + + +@ctx.action_class("edit") +class EditActions: + def save(): + actions.user.emacs("save-buffer") + + def save_all(): + actions.user.emacs("save-some-buffers") + + def copy(): + actions.user.emacs("kill-ring-save") + + def cut(): + actions.user.emacs("kill-region") + + def undo(): + actions.user.emacs("undo") + + def paste(): + actions.user.emacs("yank") + + def delete(): + actions.user.emacs("kill-region") + + def file_start(): + actions.user.emacs("beginning-of-buffer") + + def file_end(): + actions.user.emacs("end-of-buffer") + + # works for eg 'select to top', but not if preceded by other selections :( + def extend_file_start(): + actions.user.emacs("beginning-of-buffer") + + def extend_file_end(): + actions.user.emacs("end-of-buffer") + + def select_none(): + actions.user.emacs("keyboard-quit") + + def select_all(): + actions.user.emacs("mark-whole-buffer") + # If you don't use transient-mark-mode, maybe do this: + # actions.key('ctrl-u ctrl-x ctrl-x') + + def word_left(): + actions.user.emacs("backward-word") + + def word_right(): + actions.user.emacs("forward-word") + + def extend_word_left(): + actions.user.emacs_meta("shift-b") + + def extend_word_right(): + actions.user.emacs_meta("shift-f") + + def sentence_start(): + actions.user.emacs("backward-sentence") + + def sentence_end(): + actions.user.emacs("forward-sentence") + + def extend_sentence_start(): + actions.user.emacs_meta("shift-a") + + def extend_sentence_end(): + actions.user.emacs_meta("shift-e") + + def paragraph_start(): + actions.user.emacs("backward-paragraph") + + def paragraph_end(): + actions.user.emacs("forward-paragraph") + + def line_start(): + actions.user.emacs("move-beginning-of-line") + + def line_end(): + actions.user.emacs("move-end-of-line") + + def extend_line_start(): + actions.key("shift-ctrl-a") + + def extend_line_end(): + actions.key("shift-ctrl-e") + + def line_swap_down(): + actions.key("down ctrl-x ctrl-t up") + + def line_swap_up(): + actions.key("ctrl-x ctrl-t up:2") + + def delete_line(): + actions.key("ctrl-a ctrl-k") + + def line_clone(): + actions.user.emacs_key("ctrl-a meta-1 ctrl-k ctrl-y ctrl-y up meta-m") + + def jump_line(n): + actions.user.emacs("goto-line", n) + + def select_line(n: int = None): + if n is not None: + actions.edit.jump_line(n) + else: + actions.edit.line_start() + actions.edit.extend_line_end() + actions.edit.extend_right() + # This makes it so the cursor is on the same line, which can make + # subsequent commands more convenient. + actions.user.emacs("exchange-point-and-mark") + + def indent_more(): + actions.user.emacs("indent-rigidly", 4) + + def indent_less(): + actions.user.emacs("indent-rigidly", -4) + + # These all perform text-scale-adjust, which examines the actual key pressed, so can't + # be done with actions.user.emacs. + def zoom_in(): + actions.key("ctrl-x ctrl-+") + + def zoom_out(): + actions.key("ctrl-x ctrl--") + + def zoom_reset(): + actions.key("ctrl-x ctrl-0") + + # Some modes override ctrl-s/r to do something other than isearch-forward, so we + # deliberately don't use actions.user.emacs. + def find(text: str = None): + actions.key("ctrl-s") + if text: + actions.insert(text) + + def find_next(): + actions.key("ctrl-s") + + def find_previous(): + actions.key("ctrl-r") + + +@ctx.action_class("app") +class AppActions: + def window_open(): + actions.user.emacs("make-frame-command") + + def tab_next(): + actions.user.emacs("tab-next") + + def tab_previous(): + actions.user.emacs("tab-previous") + + def tab_close(): + actions.user.emacs("tab-close") + + def tab_reopen(): + actions.user.emacs("tab-undo") + + def tab_open(): + actions.user.emacs("tab-new") + + +@ctx.action_class("code") +class CodeActions: + def toggle_comment(): + actions.user.emacs("comment-dwim") + + def language(): + # Assumes win.filename() gives buffer name. + if "*scratch*" == actions.win.filename(): + return "elisp" + return actions.next() + + +@ctx.action_class("win") +class WinActions: + # This assumes the title is/contains the filename. + # To do this, put this in init.el: + # (setq-default frame-title-format '((:eval (buffer-name (window-buffer (minibuffer-selected-window)))))) + def filename(): + return actions.win.title() diff --git a/community/apps/emacs/emacs.talon b/community/apps/emacs/emacs.talon new file mode 100644 index 0000000..a144915 --- /dev/null +++ b/community/apps/emacs/emacs.talon @@ -0,0 +1,367 @@ +app: emacs +- +tag(): user.tabs +tag(): user.splits +tag(): user.line_commands + +# ----- GENERAL ----- # +#suplex: key(ctrl-x) +cancel: user.emacs("keyboard-quit") +exchange: user.emacs("exchange-point-and-mark") +execute: user.emacs("execute-extended-command") +execute {user.emacs_command}$: user.emacs(emacs_command) +execute $: + user.emacs("execute-extended-command") + user.insert_formatted(text, "DASH_SEPARATED") +evaluate | (evaluate | eval) (exper | expression): user.emacs("eval-expression") +prefix: user.emacs_prefix() +prefix : user.emacs_prefix(number_signed_small) + +abort recursive [edit]: user.emacs("abort-recursive-edit") +browse kill ring: user.emacs("browse-kill-ring") +fill paragraph: user.emacs("fill-paragraph") +insert char: user.emacs("insert-char") +occurs: user.emacs("occur") +other scroll [down]: user.emacs("scroll-other-window") +other scroll up: user.emacs("scroll-other-window-down") +package autoremove: user.emacs("package-autoremove") +package list | [package] list packages: user.emacs("list-packages") +reverse (lines | region): user.emacs("reverse-region") +save buffers kill emacs: user.emacs("save-buffers-kill-emacs") +save some buffers: user.emacs("save-some-buffers") +sort lines: user.emacs("sort-lines") +sort words: user.emacs("sort-words") +file [loop] continue: user.emacs("fileloop-continue") + +go directory: user.emacs("dired-jump") +other go directory: user.emacs("dired-jump-other-window") + +[toggle] debug on error: user.emacs("toggle-debug-on-error") +[toggle] debug on quit: user.emacs("toggle-debug-on-quit") +[toggle] input method: user.emacs("toggle-input-method") +[toggle] truncate lines: user.emacs("toggle-truncate-lines") +[toggle] word wrap: user.emacs("toggle-word-wrap") + +manual: user.emacs("man") +manual : + user.emacs("man") + user.insert_formatted(text, "DASH_SEPARATED") + +# BUFFER SWITCHING # +switch: user.emacs("switch-to-buffer") +other switch: user.emacs("switch-to-buffer-other-window") +display: user.emacs("display-buffer") + +# SHELL COMMANDS # +shell command: user.emacs("shell-command") +shell command inserting: + user.emacs_prefix() + user.emacs("shell-command") +shell command on region: user.emacs("shell-command-on-region") +shell command on region replacing: + user.emacs_prefix() + user.emacs("shell-command-on-region") + +# CUSTOMIZE # +customize face: user.emacs("customize-face") +customize face $: + user.emacs("customize-face") + user.insert_formatted(text, "DASH_SEPARATED") +customize group: user.emacs("customize-group") +customize variable: user.emacs("customize-variable") +(customize | custom) [theme] visit theme: user.emacs("custom-theme-visit-theme") + +# MODE COMMANDS # +auto fill mode: user.emacs("auto-fill-mode") +dired omit mode: user.emacs("dired-omit-mode") +display line numbers mode: user.emacs("display-line-numbers-mode") +electric quote local mode: user.emacs("electric-quote-local-mode") +emacs lisp mode: user.emacs("emacs-lisp-mode") +fundamental mode: user.emacs("fundamental-mode") +global display line numbers mode: user.emacs("global-display-line-numbers-mode") +global highlight line mode: user.emacs("global-hl-line-mode") +global visual line mode: user.emacs("global-visual-line-mode") +highlight line mode: user.emacs("hl-line-mode") +lisp interaction mode: user.emacs("lisp-interaction-mode") +markdown mode: user.emacs("markdown-mode") +menu bar mode: user.emacs("menu-bar-mode") +overwrite mode: user.emacs("overwrite-mode") +paredit mode: user.emacs("paredit-mode") +rainbow mode: user.emacs("rainbow-mode") +read only mode: user.emacs("read-only-mode") +shell script mode: user.emacs("sh-mode") +sub word mode: user.emacs("subword-mode") +tab bar mode: user.emacs("tab-bar-mode") +talon script mode: user.emacs("talonscript-mode") +text mode: user.emacs("text-mode") +transient mark mode: user.emacs("transient-mark-mode") +visual line mode: user.emacs("visual-line-mode") +whitespace mode: user.emacs("whitespace-mode") + +# MACROS # +emacs record: user.emacs("kmacro-start-macro") +emacs stop: user.emacs("kmacro-end-macro") +emacs play: user.emacs("kmacro-end-and-call-macro") + +# PROFILER # +profiler start: user.emacs("profiler-start") +profiler stop: user.emacs("profiler-stop") +profiler report: user.emacs("profiler-report") + +# WINDOW/SPLIT MANAGEMENT # +# What emacs calls windows, we call splits. +split solo: user.emacs("delete-other-windows") +[split] rebalance: user.emacs("balance-windows") +split shrink: user.emacs("shrink-window-if-larger-than-buffer") +other [split] shrink: + user.split_next() + user.emacs("shrink-window-if-larger-than-buffer") + user.split_last() +split grow: user.emacs("enlarge-window") +split grow : user.emacs("enlarge-window", number_small) +split shrink : + amount = number_small or 1 + user.emacs("enlarge-window", 0 - amount) +split widen []: + user.emacs("enlarge-window-horizontally", number_small or 1) +split narrow []: + user.emacs("shrink-window-horizontally", number_small or 1) + +# ----- HELP ----- # +apropos: user.emacs_help("a") +describe (fun | function): user.emacs_help("f") +describe key: user.emacs_help("k") +describe key briefly: user.emacs_help("c") +describe symbol: user.emacs_help("o") +describe variable: user.emacs_help("v") +describe mode: user.emacs_help("m") +describe bindings: user.emacs_help("b") +describe (char | character): user.emacs("describe-character") +describe text properties: user.emacs("describe-text-properties") +describe face: user.emacs("describe-face") +view lossage: user.emacs_help("l") + +apropos $: + user.emacs_help("a") + user.insert_formatted(text, "DASH_SEPARATED") + key(enter) +describe (fun | function) $: + user.emacs_help("f") + user.insert_formatted(text, "DASH_SEPARATED") + key(enter) +describe symbol $: + user.emacs_help("o") + user.insert_formatted(text, "DASH_SEPARATED") + key(enter) +describe variable $: + user.emacs_help("v") + user.insert_formatted(text, "DASH_SEPARATED") + key(enter) + +# ----- FILES & BUFFERS ----- +file open: user.emacs("find-file") +file rename: user.emacs("rename-file") +(file open | find file) at point: user.emacs("ffap") +other file open: user.emacs("find-file-other-window") +(file | buffer) close: + user.emacs("kill-buffer") + key(enter) + +buffer kill: user.emacs("kill-buffer") +buffer bury: user.emacs("bury-buffer") +buffer revert | revert buffer: user.emacs("revert-buffer") +buffer finish: + edit.save() + user.emacs("server-edit") +buffer list: user.emacs("buffer-menu") +buffer next: user.emacs("next-buffer") +buffer last: user.emacs("previous-buffer") +buffer rename: user.emacs("rename-buffer") +buffer widen: user.emacs("widen") +buffer narrow | [buffer] narrow to region: user.emacs("narrow-to-region") + +diff (buffer | [buffer] with file): + user.emacs("diff-buffer-with-file") + key(enter) + +# ----- MOTION AND EDITING ----- # +mark: user.emacs("set-mark-command") +go back: user.emacs("pop-to-mark-command") +global [go] back: user.emacs("pop-global-mark") + +auto indent: user.emacs("indent-region") +indent : user.emacs("indent-rigidly", number_signed_small) + +search back: user.emacs("isearch-backward") +(search regex | regex search): user.emacs("isearch-forward-regexp") +(search regex | regex search) back: user.emacs("isearch-backward-regexp") +replace: user.emacs("query-replace") +replace regex | regex replace: user.emacs("query-replace-regexp") +# These start a word/symbol-search or toggle an existing search's mode. +search [toggle] words: user.emacs("isearch-forward-word") +search [toggle] symbol: user.emacs("isearch-forward-symbol") +# These keybindings are only active in isearch-mode. +search edit: user.emacs_meta("e") +search toggle case [fold | sensitive]: user.emacs_meta("c") +search toggle regex: user.emacs_meta("r") + +highlight lines matching [regex]: user.emacs("highlight-lines-matching-regexp") +highlight phrase: user.emacs("highlight-phrase") +highlight regex: user.emacs("highlight-regexp") +unhighlight (regex | phrase): user.emacs("unhighlight-regexp") +unhighlight all: + user.emacs_prefix() + user.emacs("unhighlight-regexp") + +recenter: + user.emacs_prefix() + user.emacs("recenter-top-bottom") +(center | [center] from) top: + user.emacs("recenter-top-bottom", number_small or 0) +(center | [center] from) bottom: + number = number_small or 0 + user.emacs("recenter-top-bottom", -1 - number) +go top: + edit.jump_line(number) + user.emacs("recenter-top-bottom", 0) +go bottom: + edit.jump_line(number) + user.emacs("recenter-top-bottom", -2) + +next error | error next: user.emacs("next-error") +last error | error last: user.emacs("previous-error") + +term right: user.emacs("forward-sexp") +term left: user.emacs("backward-sexp") +term up: user.emacs("backward-up-list") +term end: user.emacs("up-list") +term down: user.emacs("down-list") +term kill: user.emacs("kill-sexp") +term wipe: user.emacs("kill-sexp", -1) +term (mark | select): user.emacs("mark-sexp") +term copy: + user.emacs("mark-sexp") + edit.copy() +term freeze: + user.emacs("mark-sexp") + user.emacs("comment-region") +term [auto] indent: + user.emacs("mark-sexp") + user.emacs("indent-region") + +(sentence | sent) (right | end): edit.sentence_end() +(sentence | sent) (left | start): edit.sentence_start() +(sentence | sent) kill: user.emacs("kill-sentence") + +graph kill: user.emacs("kill-paragraph") +graph up: edit.paragraph_start() +graph down: edit.paragraph_end() +graph mark: user.emacs("mark-paragraph") +graph copy: + user.emacs("mark-paragraph") + edit.copy() +graph cut: + user.emacs("mark-paragraph") + edit.cut() + +# NB. can use these to implement "drag left/right/up/down" commands, +# but note that 'transpose line' and 'drag line down' are different. +transpose [word | words]: user.emacs("transpose-words") +transpose (term | terms): user.emacs("transpose-sexps") +transpose (char | chars): user.emacs("transpose-chars") +transpose (line | lines): user.emacs("transpose-lines") +transpose (sentence | sentences): user.emacs("transpose-sentences") +transpose (graph | graphs | paragraphs): user.emacs("transpose-paragraphs") + +register (copy | save): user.emacs("copy-to-register") +register (paste | insert): user.emacs("insert-register") +register jump: user.emacs("jump-to-register") +register (copy | save) rectangle: user.emacs("copy-rectangle-to-register") + +rectangle clear: user.emacs("clear-rectangle") +rectangle delete: user.emacs("delete-rectangle") +rectangle kill: user.emacs("kill-rectangle") +rectangle open: user.emacs("open-rectangle") +rectangle (copy | save) [to] register: user.emacs("copy-rectangle-to-register") +rectangle (yank | paste): user.emacs("yank-rectangle") +rectangle copy: user.emacs("copy-rectangle-as-kill") +rectangle number lines: user.emacs("rectangle-number-lines") + +# ----- XREF SUPPORT ----- # +[xref] find definition: user.emacs("xref-find-definitions") +[xref] find definition other window: user.emacs("xref-find-definitions-other-window") +[xref] find definition other frame: user.emacs("xref-find-definitions-other-frame") +[xref] find references: user.emacs("xref-find-references") +[xref] find references [and] replace: user.emacs("xref-find-references-and-replace") +xref find apropos: user.emacs("xref-find-apropos") +xref go back: user.emacs("xref-go-back") +visit tags table: user.emacs("visit-tags-table") + +# ----- PROJECT SUPPORT ----- # +project [find] file: user.emacs("project-find-file") +project [find] (regex | grep): user.emacs("project-find-regexp") +project [query] replace regex: user.emacs("project-query-replace-regexp") +project (dired | directory): user.emacs("projectile-dired") +project [run] shell: user.emacs("projectile-run-shell") +project [run] eshell: user.emacs("projectile-run-eshell") +project search: user.emacs("project-search") +project vc dir: user.emacs("project-vc-dir") +project compile [project]: user.emacs("projectile-compile-project") +project [run] shell command: user.emacs("projectile-run-shell-command-in-root") +project [run] async shell command: + user.emacs("projectile-run-async-shell-command-in-root") +project (switch [to buffer] | buffer | buff): user.emacs("projectile-switch-to-buffer") +project kill [buffers]: user.emacs("projectile-kill-buffers") +project switch [project]: user.emacs("project-switch-project") + +# ----- VC/GIT SUPPORT ----- # +vc (annotate | blame): user.emacs("vc-annotate") + +# ----- MAJOR & MINOR MODES ----- # +# python-mode # +python mode: user.emacs("python-mode") +run python: user.emacs("run-python") +python [shell] send buffer: user.emacs("python-shell-send-buffer") +python [shell] send file: user.emacs("python-shell-send-file") +python [shell] send region: user.emacs("python-shell-send-region") +python [shell] send (function | defun): user.emacs("python-shell-send-defun") +python [shell] send statement: user.emacs("python-shell-send-statement") +python (shell switch | switch [to] shell): user.emacs("python-shell-switch-to-shell") + +# smerge-mode # +smerge mode: user.emacs("smerge-mode") +merge next: user.emacs("smerge-next") +merge last: user.emacs("smerge-prev") +merge keep upper: user.emacs("smerge-keep-upper") +merge keep lower: user.emacs("smerge-keep-lower") +merge keep base: user.emacs("smerge-keep-base") +merge keep (this | current): user.emacs("smerge-keep-current") +merge refine: user.emacs("smerge-refine") +merge split: user.emacs("smerge-resolve") + +# outline-minor-mode # +# frequent: overview, show, hide, next, last, forward, backward, up +outline minor mode: user.emacs("outline-minor-mode") +outline show all: user.emacs("outline-show-all") +outline show entry: user.emacs("outline-show-entry") +outline hide entry: user.emacs("outline-hide-entry") +outline show [subtree]: user.emacs("outline-show-subtree") +outline hide [subtree]: user.emacs("outline-hide-subtree") +outline show children: user.emacs("outline-show-children") +outline show branches: user.emacs("outline-show-branches") +outline hide leaves: user.emacs("outline-hide-leaves") +outline hide sublevels: user.emacs("outline-hide-sublevels") +outline (hide body | [show] (overview | outline)): user.emacs("outline-hide-body") +outline hide other: user.emacs("outline-hide-other") +outline forward [same level]: user.emacs("outline-forward-same-level") +outline (backward | back) [same level]: user.emacs("outline-backward-same-level") +outline next [visible heading]: user.emacs("outline-next-visible-heading") +outline (previous | last) [visible heading]: + user.emacs("outline-previous-visible-heading") +outline insert [heading]: user.emacs("outline-insert-heading") +outline up [heading]: user.emacs("outline-up-heading") +outline promote: user.emacs("outline-promote") +outline demote: user.emacs("outline-demote") +outline move [subtree] down: user.emacs("outline-move-subtree-down") +outline move [subtree] up: user.emacs("outline-move-subtree-up") +outline mark [subtree]: user.emacs("outline-mark-subtree") diff --git a/community/apps/emacs/emacs_commands.csv b/community/apps/emacs/emacs_commands.csv new file mode 100644 index 0000000..7c09c33 --- /dev/null +++ b/community/apps/emacs/emacs_commands.csv @@ -0,0 +1,231 @@ +Command, Key binding, Short form, Spoken form +abort-recursive-edit, ctrl-] +auto-fill-mode,, auto-f +backward-paragraph, meta-{ +backward-sentence, meta-a +backward-sexp, meta-ctrl-b +backward-up-list, meta-ctrl-up +backward-word, meta-b +balance-windows, ctrl-x + +beginning-of-buffer, meta-< +browse-kill-ring,, b-k-r +buffer-menu, ctrl-x ctrl-b +bury-buffer,, bur +byte-compile-file,, by-c +byte-recompile-directory +byte-recompile-file +clear-rectangle, ctrl-x r c +clone-indirect-buffer,, clo-i +comment-dwim, meta-; +comment-line,, comment-l +comment-region, ctrl-c ; +compile +compile-defun,, co-def +copy-rectangle-as-kill, ctrl-x r meta-w +copy-rectangle-to-register, ctrl-x r r +copy-to-register, ctrl-x r s +custom-theme-visit-theme +customize-face +customize-group +customize-variable +delete-other-windows, ctrl-x 1 +delete-rectangle, ctrl-x r d +delete-window, ctrl-x 0 +describe-character,, desc-char +diff-buffer-with-file,, d-b-w-f +display-buffer, ctrl-x 4 ctrl-o +dired-jump, ctrl-x ctrl-j +dired-jump-other-window, ctrl-x 4 ctrl-j +display-buffer, ctrl-x 4 ctrl-o +display-line-numbers-mode,, dis-num +down-list, meta-ctrl-down +electric-quote-local-mode +electric-quote-mode +end-of-buffer, meta-> +enlarge-window, ctrl-x ^ +enlarge-window-horizontally, ctrl-x } +eval-expression, meta-: +eval-print-last-sexp,, ev-p +eval-region,, ev-r +exchange-point-and-mark, ctrl-x ctrl-x +execute-extended-command, meta-x +fileloop-continue,, filel +fill-paragraph, meta-q +find-file, ctrl-x ctrl-f +font-lock-update, ctrl-x x f +forward-paragraph, meta-} +forward-sentence, meta-e +forward-sexp, meta-ctrl-f +forward-word, meta-f +fundamental-mode,, fun-m +global-display-line-numbers-mode,, g-d-l-n-m +global-hl-line-mode,, g-hl-l-m +global-visual-line-mode,, gl-v-l-m +goto-line, meta-g meta-g +highlight-lines-matching-regexp, meta-s h l +highlight-phrase, meta-s h p +highlight-regexp, meta-s h r +hl-line-mode,, hl-l-m +indent-region, meta-ctrl-\ +indent-rigidly, ctrl-x tab +insert-char, ctrl-x 8 enter +insert-register, ctrl-x r i +isearch-backward, ctrl-r +isearch-backward-regexp, meta-ctrl-r +isearch-forward, ctrl-s +isearch-forward-regexp, meta-ctrl-s +isearch-forward-symbol, meta-s _ +isearch-forward-word, meta-s w +jump-to-register, ctrl-x r j +keyboard-quit, ctrl-g +kill-buffer, ctrl-x k +kill-line, ctrl-k +kill-paragraph,, kill-par +kill-rectangle, ctrl-x r k +kill-region, ctrl-w +kill-ring-save, meta-w +kill-sentence, meta-k +kill-sexp, meta-ctrl-k +kill-word, meta-d +kmacro-end-and-call-macro, ctrl-x e +kmacro-end-macro, ctrl-x ) +kmacro-start-macro, ctrl-x ( +list-packages +make-frame-command, ctrl-x 5 2 +mark-paragraph, meta-h +mark-sexp, meta-ctrl-@ +mark-whole-buffer, ctrl-x h +move-beginning-of-line, ctrl-a +move-end-of-line, ctrl-e +narrow-to-region, ctrl-x n n +next-buffer, ctrl-x right +next-error, meta-g n +occur, meta-s o +open-rectangle, ctrl-x r o +other-frame, ctrl-x 5 o +other-window, ctrl-x o +outline-backward-same-level, ctrl-c @ ctrl-b +outline-demote, ctrl-c @ ctrl-> +outline-forward-same-level, ctrl-c @ ctrl-f +outline-hide-body, ctrl-c @ ctrl-t +outline-hide-entry, ctrl-c @ ctrl-c +outline-hide-leaves, ctrl-c @ ctrl-l +outline-hide-other, ctrl-c @ ctrl-o +outline-hide-sublevels, ctrl-c @ ctrl-q +outline-hide-subtree, ctrl-c @ ctrl-d +outline-insert-heading, ctrl-c @ RET +outline-mark-subtree, ctrl-c @ @ +outline-move-subtree-down, ctrl-c @ ctrl-v +outline-move-subtree-up, ctrl-c @ ctrl-^ +outline-next-visible-heading, ctrl-c @ ctrl-n +outline-previous-visible-heading, ctrl-c @ ctrl-p +outline-promote, ctrl-c @ ctrl-< +outline-show-all, ctrl-c @ ctrl-a +outline-show-branches, ctrl-c @ ctrl-k +outline-show-children, ctrl-c @ tab +outline-show-entry, ctrl-c @ ctrl-e +outline-show-subtree, ctrl-c @ ctrl-s +outline-up-heading, ctrl-c @ ctrl-u +overwrite-mode,, overwr +package-autoremove +pop-global-mark, ctrl-x ctrl-@ +pop-to-mark-command, ctrl-u ctrl-space +previous-buffer, ctrl-x left +previous-error, meta-g p +project-find-file, ctrl-x p f +project-find-regexp, ctrl-x p g +project-query-replace-regexp, ctrl-x p r +project-search, ctrl-x p ctrl-s +project-switch-project +project-vc-dir, ctrl-x p v +projectile-compile-project, ctrl-x p c +projectile-dired, ctrl-x p d +projectile-kill-buffers, ctrl-x p k +projectile-run-async-shell-command-in-root, ctrl-x p & +projectile-run-eshell, ctrl-x p e +projectile-run-shell, ctrl-x p s +projectile-run-shell-command-in-root, ctrl-x p ! +projectile-switch-to-buffer, ctrl-x p b +query-replace, meta-% +query-replace-regexp, meta-ctrl-% +read-only-mode, ctrl-x ctrl-q +recenter-top-bottom, ctrl-l +recenter-top-bottom, ctrl-l +rectangle-number-lines, ctrl-x r N +rename-buffer, ctrl-x x r, ren-b +rename-uniquely, ctrl-x x u +reverse-region +revert-buffer,, rev-buf +revert-buffer-quick, ctrl-x x g +save-buffer, ctrl-x ctrl-s +save-buffers-kill-emacs,, s-b-k-e +save-some-buffers, ctrl-x s +scroll-other-window, meta-pagedown +scroll-other-window-down, meta-pageup +server-edit, ctrl-x # +set-mark-command, ctrl-@ +sh-mode +shell-command, meta-! +shell-command-on-region, meta-| +shell-script-mode +shrink-window-horizontally, ctrl-x { +shrink-window-if-larger-than-buffer, ctrl-x - +smerge-combine-with-next +smerge-diff-base-lower +smerge-diff-base-upper +smerge-diff-upper-lower +smerge-ediff +smerge-keep-all +smerge-keep-base +smerge-keep-current, ctrl-c ^ enter +smerge-keep-lower, ctrl-c ^ l +smerge-keep-upper, ctrl-c ^ u +smerge-next, ctrl-c ^ n +smerge-prev, ctrl-c ^ p +smerge-refine, ctrl-c ^ R +smerge-resolve, ctrl-c ^ r +sort-lines +sort-words +split-window-below, ctrl-x 2 +split-window-right, ctrl-x 3 +switch-to-buffer, ctrl-x b +switch-to-buffer-other-window, ctrl-x 4 b +tab-bar-mode +tab-close, ctrl-x t 0 +tab-close-other, ctrl-x t 1 +tab-new, ctrl-x t 2 +tab-next, ctrl-x t o +tab-previous, ctrl-x t O +tab-undo, ctrl-x t u +text-mode,, text-m +toggle-debug-on-error,, t-d-on-e +toggle-debug-on-quit,, t-d-on-q +toggle-input-method, ctrl-\ +toggle-truncate-lines, ctrl-x x t +toggle-word-wrap +transient-mark-mode,, tr-m-m +transpose-chars, ctrl-t +transpose-lines, ctrl-x ctrl-t +transpose-paragraphs,, tr-par +transpose-sentences,, tr-sen +transpose-sexps, meta-ctrl-t +transpose-words, meta-t +undo, ctrl-_ +unhighlight-regexp, meta-s h u +universal-argument, ctrl-u +vc-annotate, ctrl-x v g +view-lossage, ctrl-h l +visit-tags-table,, v-t-t +visual-line-mode,, visu-l-m +whitespace-cleanup,, wh-cl +whitespace-mode,, white-m +widen, ctrl-x n w +xref-find-apropos, meta-ctrl-. +xref-find-definitions, meta-. +xref-find-definitions-other-frame, ctrl-x 5 . +xref-find-definitions-other-window, ctrl-x 4 . +xref-find-references, meta-? +xref-find-references-and-replace +xref-go-back,"meta-," +yank, ctrl-y +yank-rectangle, ctrl-x r y diff --git a/community/apps/emacs/emacs_commands.py b/community/apps/emacs/emacs_commands.py new file mode 100644 index 0000000..12fd817 --- /dev/null +++ b/community/apps/emacs/emacs_commands.py @@ -0,0 +1,71 @@ +import csv +from dataclasses import dataclass +from pathlib import Path +from typing import NamedTuple, Optional + +from talon import Context, Module, actions, app, resource + +mod = Module() +mod.list("emacs_command", desc="Emacs commands") + +ctx = Context() + + +class Command(NamedTuple): + name: str + keys: Optional[str] = None + short: Optional[str] = None + spoken: Optional[str] = None + + +# Maps command name to Command. +emacs_commands = {} + + +@mod.action_class +class Actions: + def emacs_command_keybinding(command_name: str) -> Optional[str]: + "Looks up the keybinding for command_name in emacs_commands.csv." + return emacs_commands.get(command_name, Command(command_name)).keys + + def emacs_command_short_form(command_name: str) -> Optional[str]: + "Looks up the short form for command_name in emacs_commands.csv." + return emacs_commands.get(command_name, Command(command_name)).short + + +@resource.watch("emacs_commands.csv") +def load_commands(f): + rows = list(csv.reader(f)) + # Check headers + assert rows[0] == ["Command", " Key binding", " Short form", " Spoken form"] + + commands = [] + for row in rows[1:]: + if 0 == len(row): + continue + if len(row) > 4: + print( + f"emacs_commands.csv: More than four values in row: {row}. " + + " Ignoring the extras" + ) + name, keys, short, spoken = ( + [x.strip() or None for x in row] + [None, None, None] + )[:4] + commands.append(Command(name=name, keys=keys, short=short, spoken=spoken)) + + # Update global command info. + global emacs_commands + emacs_commands = {c.name: c for c in commands} + + # Generate spoken forms and apply overrides. + try: + command_list = actions.user.create_spoken_forms_from_list( + [c.name for c in commands], generate_subsequences=False + ) + except: + pass + else: + for c in commands: + if c.spoken: + command_list[c.spoken] = c.name + ctx.lists["self.emacs_command"] = command_list diff --git a/community/apps/evernote/mac.talon b/community/apps/evernote/mac.talon new file mode 100644 index 0000000..fb478fc --- /dev/null +++ b/community/apps/evernote/mac.talon @@ -0,0 +1,6 @@ +app: evernote +os: mac +- +settings(): + # Necessary to stop commands like 'slap' getting jumbled + key_wait = 9.0 diff --git a/community/apps/evince/evince.talon b/community/apps/evince/evince.talon new file mode 100644 index 0000000..68b9f87 --- /dev/null +++ b/community/apps/evince/evince.talon @@ -0,0 +1,4 @@ +app: evince +- +# Set tags +tag(): user.pages diff --git a/community/apps/evince/evince_linux.py b/community/apps/evince/evince_linux.py new file mode 100644 index 0000000..0820799 --- /dev/null +++ b/community/apps/evince/evince_linux.py @@ -0,0 +1,39 @@ +from talon import Context, Module, actions + +# --- App definition --- +mod = Module() +mod.apps.evince = """ +os: linux +and app.name: Evince +""" + +# Context matching +ctx = Context() +ctx.matches = r""" +app: evince +""" + + +# --- Implement actions --- +@ctx.action_class("user") +class UserActions: + # user.pages + def page_current(): + actions.key("ctrl-l") + page = actions.edit.selected_text() + actions.key("escape") + return int(page) + + def page_next(): + actions.key("n") + + def page_previous(): + actions.key("p") + + def page_jump(number: int): + actions.key("ctrl-l") + actions.insert(str(number)) + actions.key("enter") + + def page_final(): + actions.key("ctrl-end") diff --git a/community/apps/finder/finder.py b/community/apps/finder/finder.py new file mode 100644 index 0000000..74d25a4 --- /dev/null +++ b/community/apps/finder/finder.py @@ -0,0 +1,86 @@ +import os + +from talon import Context, actions, ui +from talon.mac import applescript + +ctx = Context() +ctx.matches = r""" +app: finder +""" +directories_to_remap = {"": "/Volumes"} +directories_to_exclude = {} + + +@ctx.action_class("user") +class UserActions: + def file_manager_open_parent(): + actions.key("cmd-up") + + def file_manager_current_path(): + title = ui.active_window().title + + if "~" in title: + title = os.path.expanduser(title) + + if title in directories_to_remap: + title = directories_to_remap[title] + + if title in directories_to_exclude: + title = "" + + return title + + def file_manager_terminal_here(): + applescript.run( + r""" + tell application "Finder" + set myWin to window 1 + set thePath to (quoted form of POSIX path of (target of myWin as alias)) + tell application "Terminal" + activate + tell window 1 + do script "cd " & thePath + end tell + end tell + end tell""" + ) + + def file_manager_show_properties(): + """Shows the properties for the file""" + actions.key("cmd-i") + + def file_manager_open_directory(path: str): + """opens the directory that's already visible in the view""" + actions.key("cmd-shift-g") + actions.sleep("50ms") + actions.insert(path) + actions.key("enter") + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(path) + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + actions.key("cmd-shift-n") + actions.insert(name) + + def file_manager_open_file(path: str): + """opens the file""" + actions.key("home") + actions.insert(path) + actions.key("cmd-o") + + 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/community/apps/finder/finder.talon b/community/apps/finder/finder.talon new file mode 100644 index 0000000..9400148 --- /dev/null +++ b/community/apps/finder/finder.talon @@ -0,0 +1,29 @@ +os: mac +app: finder +- +tag(): user.address +tag(): user.file_manager +tag(): user.navigation +tag(): user.tabs +preferences: key(cmd-,) +options: key(cmd-j) +search: key(cmd-alt-f) + +# bit of a mouthful, but it's probably not the kind of thing you'd be saying frequently +sort by none: key(ctrl-alt-cmd-0) +sort by name: key(ctrl-alt-cmd-1) +sort by kind: key(ctrl-alt-cmd-2) +sort by date opened: key(ctrl-alt-cmd-3) +sort by date added: key(ctrl-alt-cmd-4) +sort by date modified: key(ctrl-alt-cmd-5) +sort by size: key(ctrl-alt-cmd-6) + +icon view: key(cmd-1) +column view: key(cmd-3) +list view: key(cmd-2) +gallery view: key(cmd-4) + +trash it: key(cmd-backspace) + +hide [finder]: key(cmd-h) +hide others: app.window_hide_others() diff --git a/community/apps/firefox/firefox.py b/community/apps/firefox/firefox.py new file mode 100644 index 0000000..75f2738 --- /dev/null +++ b/community/apps/firefox/firefox.py @@ -0,0 +1,61 @@ +from talon import Context, Module, actions, app + +ctx = Context() +mod = Module() +apps = mod.apps +apps.firefox = "app.name: Firefox" +apps.firefox = "app.name: Firefox Developer Edition" +apps.firefox = "app.name: firefox" +apps.firefox = "app.name: org.mozilla.firefox" +apps.firefox = "app.name: Firefox-esr" +apps.firefox = "app.name: firefox-esr" +apps.firefox = "app.name: LibreWolf" +apps.firefox = "app.name: waterfox" +apps.firefox = r""" +os: windows +and app.name: Firefox +os: windows +and app.exe: /^firefox\.exe$/i +""" +apps.firefox = """ +os: mac +and app.bundle: org.mozilla.firefox +""" + +# Make the context match more specifically than anything else. This is important, eg. to +# override the browser.go_home() implementation in tags/browser/browser_mac.py. +ctx.matches = r""" +os: windows +os: linux +os: mac +tag: browser +app: firefox +""" + + +@mod.action_class +class Actions: + def firefox_bookmarks_sidebar(): + """Toggles the Firefox bookmark sidebar""" + + def firefox_history_sidebar(): + """Toggles the Firefox history sidebar""" + + +@ctx.action_class("user") +class UserActions: + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + +@ctx.action_class("browser") +class BrowserActions: + def focus_page(): + actions.browser.focus_address() + actions.edit.find() + actions.sleep("180ms") + actions.key("escape") + + def go_home(): + actions.key("alt-home") diff --git a/community/apps/firefox/firefox.talon b/community/apps/firefox/firefox.talon new file mode 100644 index 0000000..db3224d --- /dev/null +++ b/community/apps/firefox/firefox.talon @@ -0,0 +1,15 @@ +app: firefox +- +tag(): browser +tag(): user.tabs + +tab search: + browser.focus_address() + insert("% ") +tab search $: + browser.focus_address() + insert("% {text}") + key(down) + +(sidebar | panel) bookmarks: user.firefox_bookmarks_sidebar() +(sidebar | panel) history: user.firefox_history_sidebar() diff --git a/community/apps/firefox/firefox_mac.py b/community/apps/firefox/firefox_mac.py new file mode 100644 index 0000000..d8418c8 --- /dev/null +++ b/community/apps/firefox/firefox_mac.py @@ -0,0 +1,33 @@ +from talon import Context, actions + +ctx = Context() + +ctx.matches = r""" +os: mac +tag: browser +app: firefox +""" + + +@ctx.action_class("user") +class UserActions: + def firefox_bookmarks_sidebar(): + actions.key("cmd-b") + + def firefox_history_sidebar(): + actions.key("cmd-shift-h") + + +@ctx.action_class("browser") +class BrowserActions: + def bookmarks(): + actions.key("cmd-shift-o") + + def open_private_window(): + actions.key("cmd-shift-p") + + def show_downloads(): + actions.key("cmd-j") + + def show_extensions(): + actions.key("cmd-shift-a") diff --git a/community/apps/firefox/firefox_win_linux.py b/community/apps/firefox/firefox_win_linux.py new file mode 100644 index 0000000..f93eebf --- /dev/null +++ b/community/apps/firefox/firefox_win_linux.py @@ -0,0 +1,44 @@ +from talon import Context, actions, app + +ctx = Context() + +ctx.matches = r""" +os: windows +os: linux +tag: browser +app: firefox +""" + + +@ctx.action_class("user") +class UserActions: + def firefox_bookmarks_sidebar(): + actions.key("ctrl-b") + + def firefox_history_sidebar(): + actions.key("ctrl-h") + + +@ctx.action_class("browser") +class BrowserActions: + def focus_address(): + # Only using "ctrl-l" might fail and clear the console if the user + # is focused in the devtools + actions.key("f6") + actions.sleep("100ms") + actions.key("ctrl-l") + + def open_private_window(): + actions.key("ctrl-shift-p") + + def show_downloads(): + if app.platform == "linux": + actions.key("ctrl-shift-y") + else: + actions.key("ctrl-j") + + def show_extensions(): + actions.key("ctrl-shift-a") + + def show_history(): + actions.key("ctrl-shift-h") diff --git a/community/apps/foxit_reader/foxit_reader.py b/community/apps/foxit_reader/foxit_reader.py new file mode 100644 index 0000000..ca0c441 --- /dev/null +++ b/community/apps/foxit_reader/foxit_reader.py @@ -0,0 +1,70 @@ +from talon import Context, Module, actions + +mod = Module() +ctx = Context() + +# --- App definition --- +mod.apps.foxit_reader = r""" +os: windows +and app.name: /^Foxit Reader/ +os: windows +and app.exe: /^foxitreader\.exe$/i +os: windows +and app.name: Foxit PDF Reader +os: windows +and app.exe: /^foxitpdfreader\.exe$/i +""" +# Context matching +ctx.matches = """ +app: foxit_reader +""" + + +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_open(): + actions.key("ctrl-o") + + def tab_reopen(): + actions.app.notify("Foxit does not support this action.") + + +@ctx.action_class("user") +class UserActions: + # user.tabs + def tab_jump(number): + actions.app.notify("Foxit does not support this action.") + + def tab_final(): + actions.app.notify("Foxit does not support this action.") + + def tab_duplicate(): + actions.app.notify("Foxit does not support this action.") + + # user.pages + def page_current() -> int: + actions.key("ctrl-g") + page = actions.edit.selected_text() + return int(page) + + def page_next(): + actions.key("right") + + def page_previous(): + actions.key("left") + + def page_jump(number: int): + actions.key("ctrl-g") + actions.insert(str(number)) + actions.key("enter") + + 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/community/apps/foxit_reader/foxit_reader.talon b/community/apps/foxit_reader/foxit_reader.talon new file mode 100644 index 0000000..8390889 --- /dev/null +++ b/community/apps/foxit_reader/foxit_reader.talon @@ -0,0 +1,6 @@ +app: foxit_reader +- +tag(): user.tabs +tag(): user.pages + +tab close all: key(ctrl-shift-w) diff --git a/community/apps/gdb/gdb.py b/community/apps/gdb/gdb.py new file mode 100644 index 0000000..33db895 --- /dev/null +++ b/community/apps/gdb/gdb.py @@ -0,0 +1,117 @@ +from talon import Context, Module, actions + +mod = Module() +mod.tag("gdb", "Tag to enabled gdb-related functionality") + +# user.gdb-specific context +ctx_gdb_enabled = Context() +ctx_gdb_enabled.matches = r""" +tag: user.gdb +""" + +# global context for enabling and disabling user.gdb tag +ctx_global = Context() + + +@mod.action_class +class Actions: + def gdb_enable(): + """Enables the gdb tag""" + ctx_global.tags = ["user.gdb"] + + def gdb_disable(): + """Disables the gdb tag""" + ctx_global.tags = [] + + +@ctx_gdb_enabled.action_class("user") +class UserActions: + ## + # Generic debugger actions + ## + + # Code execution + def debugger_step_into(): + actions.auto_insert("stepi\n") + + def debugger_step_over(): + actions.auto_insert("nexti\n") + + def debugger_step_line(): + actions.auto_insert("step\n") + + def debugger_step_over_line(): + actions.auto_insert("next\n") + + def debugger_step_out(): + actions.auto_insert("finish\n") + + def debugger_continue(): + actions.auto_insert("c\n") + + def debugger_stop(): + actions.key("ctrl-c") + + def debugger_start(): + actions.auto_insert("run\n") + + def debugger_restart(): + actions.auto_insert("run\n") + + # XXX - + def debugger_detach(): + actions.auto_insert("") + + # Registers + def debugger_show_registers(): + actions.auto_insert("info registers\n") + + def debugger_get_register(): + actions.auto_insert("r ") + + def debugger_set_register(): + actions.user.insert_between("set $", "=") + # Breakpoints + + def debugger_show_breakpoints(): + actions.auto_insert("info breakpoints\n") + + def debugger_add_sw_breakpoint(): + actions.auto_insert("break ") + + # XXX - + def debugger_add_hw_breakpoint(): + actions.auto_insert("") + + def debugger_break_now(): + actions.key("ctrl-c") + + def debugger_break_here(): + actions.auto_insert("break\n") + + def debugger_clear_all_breakpoints(): + actions.auto_insert("d br\n") + + def debugger_clear_breakpoint(): + actions.insert("d br ") + + def debugger_enable_all_breakpoints(): + actions.insert("enable br\n") + + def debugger_enable_breakpoint(): + actions.insert("enable br ") + + def debugger_disable_all_breakpoints(): + actions.insert("disable br\n") + + def debugger_disable_breakpoint(): + actions.insert("disable br ") + + def debugger_clear_breakpoint_id(number_small: int): + actions.insert(f"d br {number_small}\n") + + def debugger_disable_breakpoint_id(number_small: int): + actions.insert(f"disable br {number_small}\n") + + def debugger_enable_breakpoint_id(number_small: int): + actions.insert(f"enable br {number_small}\n") diff --git a/community/apps/gdb/gdb_active.talon b/community/apps/gdb/gdb_active.talon new file mode 100644 index 0000000..ba73105 --- /dev/null +++ b/community/apps/gdb/gdb_active.talon @@ -0,0 +1,102 @@ +os: linux +# XXX - this matches .gdb files atm +#win.title: /gdb/ +tag: terminal +and tag: user.gdb +- +tag(): user.debugger +until : "until {number}" +force clear all break points: + insert("d br\n") + insert("y\n") +break [on] clipboard: + insert("break ") + key(ctrl-shift-v) + key(enter) + +# information +list [source]: "list\n" +info source: "info source\n" + +print: "p " +print [variable] : "p {text}" +print hex: "p/x " +print hex [variable] : "p/x {text}" +print string: "p/s " + +# hexdumping +# XXX - switch the sizes to a list in python? +# XXX - should cache the last used size +hex dump bytes: "x/{number}bx " +hex dump (half | short) words: "x/{number}hx " +hex dump (d | long) words: "x/{number}dx " +hex dump quad words: "x/{number}gx " +# this is some arbitrary default for convenience +hex dump: "x/100gx " +hex dump highlighted: + insert("x/100gx ") + edit.copy() + edit.paste() + key(enter) +hex dump clipboard: + insert("x/100gx ") + edit.paste() + key(enter) + +# execution +source: "source \t\t" + +# displays +# XXX - move thee invoke command into a python script +(list | show | info) display: "info display\n" +display assembly line$: "display /i $pc\n" +display source: "display " +enable display : "enable display {number_small}\n" +disable display : "disable display {number_small}\n" +undisplay: "undisplay\n" + +# variables +(list | show | info) local: "info local " +(list | show | info) local typed: "info local -t " +(list | show | info) variable: "info variable " +(list | show | info) variable typed: "info variable -t " +(list | show | info) locals: "info local\n" +(list | show | info) variables: "info variables\n" + +# threads +info threads: "info threads\n" + +restart [program]: "r\n" +continue: "c\n" +back trace: "bt\n" +debug quit: "quit\n" +# more quickly quit when there are inferiors +debug force quit: "quit\ny\n" +(show | info) (inf | inferiors): "info inferiors\n" +inferior $: "inferior {number_small}\n" +inferior: "inferior " +resume main (inf | inferior): + insert("inferior 1\n") + insert("c\n") +resume [from] (inf | inferior) $: + insert("inferior {number_small}\n") + insert("c\n") + +# arguments +set args: "set args " + +# settings +show follow (fork | forks) [mode]: "show follow-fork-mode\n" +[set] follow (fork | forks) [mode] child: "set follow-fork-mode child\n" +[set] follow (fork | forks) [mode] parent: "set follow-fork-mode parent\n" + +show detach on fork: "show detach-on-fork\n" +set detach on fork: "set detach-on-fork on\n" +unset detach on fork: "set detach-on-fork off\n" + +# list +show list size: "show listsize\n" +set list size : "set listsize {number_small}\n" + +# misc +clear screen: "shell clear\n" diff --git a/community/apps/gdb/gdb_global.talon b/community/apps/gdb/gdb_global.talon new file mode 100644 index 0000000..75dddbd --- /dev/null +++ b/community/apps/gdb/gdb_global.talon @@ -0,0 +1,2 @@ +[enable] debug mode: user.gdb_enable() +disable debug mode: user.gdb_disable() diff --git a/community/apps/git/git.py b/community/apps/git/git.py new file mode 100644 index 0000000..01acd5d --- /dev/null +++ b/community/apps/git/git.py @@ -0,0 +1,17 @@ +import csv +import os +from pathlib import Path + +from talon import Context, Module, actions, resource + +mod = Module() +ctx = Context() + +mod.list("git_command", desc="Git commands.") +mod.list("git_argument", desc="Command-line git options and arguments.") + + +@mod.capture(rule="{user.git_argument}+") +def git_arguments(m) -> str: + """A non-empty sequence of git command arguments, preceded by a space.""" + return " " + " ".join(m.git_argument_list) diff --git a/community/apps/git/git.talon b/community/apps/git/git.talon new file mode 100644 index 0000000..9ba5687 --- /dev/null +++ b/community/apps/git/git.talon @@ -0,0 +1,51 @@ +tag: terminal +and tag: user.git +- +git {user.git_command} []: + args = git_arguments or "" + "git {git_command}{args} " +git commit [] message []: + args = git_arguments or "" + message = prose or "" + user.insert_between('git commit{args} --message "{message}', '"') +git stash [push] [] message []: + args = git_arguments or "" + message = prose or "" + user.insert_between('git stash push{args} --message "{message}', '"') + +# Optimistic execution for frequently used commands that are harmless (don't +# change repository or index state). +git status$: "git status\n" +git add patch$: "git add --patch\n" +git show head$: "git show HEAD\n" +git diff$: "git diff\n" +git diff (cached | cashed)$: "git diff --cached\n" + +# Convenience +git clone clipboard: + insert("git clone ") + edit.paste() + key(enter) +git diff highlighted: + edit.copy() + insert("git diff ") + edit.paste() + key(enter) +git diff clipboard: + insert("git diff ") + edit.paste() + key(enter) +git add highlighted: + edit.copy() + insert("git add ") + edit.paste() + key(enter) +git add clipboard: + insert("git add ") + edit.paste() + key(enter) +git commit highlighted: + edit.copy() + insert("git add ") + edit.paste() + insert("\ngit commit\n") diff --git a/community/apps/git/git_add_patch.talon b/community/apps/git/git_add_patch.talon new file mode 100644 index 0000000..d495002 --- /dev/null +++ b/community/apps/git/git_add_patch.talon @@ -0,0 +1,19 @@ +tag: terminal +and tag: user.git +title: /git add .*\-p/ +- +yank: + key(y) + key(enter) +near: + key(n) + key(enter) +quench: + key(q) + key(enter) +drum: + key(d) + key(enter) +air: + key(a) + key(enter) diff --git a/community/apps/git/git_argument.talon-list b/community/apps/git/git_argument.talon-list new file mode 100644 index 0000000..ac991af --- /dev/null +++ b/community/apps/git/git_argument.talon-list @@ -0,0 +1,69 @@ +list: user.git_argument +- +abort: --abort +all: --all +allow empty: --allow-empty +amend: --amend +cached: --cached +cashed: --cached +color words: --color-words +colour words: --color-words +continue: --continue +copy: --copy +create: --create +delete: --delete +detach: --detach +dir diff: --dir-diff +directory diff: --dir-diff +dry run: --dry-run +edit: --edit +fast forward only: --ff-only +force: --force +force create: --force-create +force with lease: --force-with-lease +global: --global +global: --global +hard: --hard +ignore case: --ignore-case +include untracked: --include-untracked +interactive: --interactive +keep index: --keep-index +list: --list +local: --local +mixed: --mixed +move: --move +no edit: --no-edit +no keep index: --no-keep-index +no rebase: --no-rebase +no track: --no-track +no verify: --no-verify +orphan: --orphan +patch: --patch +prune: --prune +quiet: --quiet +quit: --quit +rebase: --rebase +remote: --remote +set up stream: --set-upstream +set up stream to: --set-upstream-to +short: --short +short stat: --shortstat +skip: --skip +soft: --soft +staged: --staged +stat: --stat +system: --system +track: --track +update: --update +verbose: --verbose +branch: -b +combined: -c +deep: -d +very verbose: -vv +HEAD +main +master +origin +upstream +origin main: origin/main +origin master: origin/master diff --git a/community/apps/git/git_command.talon-list b/community/apps/git/git_command.talon-list new file mode 100644 index 0000000..04655a3 --- /dev/null +++ b/community/apps/git/git_command.talon-list @@ -0,0 +1,71 @@ +list: user.git_command +- +add +archive +bisect +blame +branch +checkout +cherry pick: cherry-pick +clean +clone +commit +config +diff +diff tool: difftool +fetch +gc +grep +help +in it: init +log +ls files: ls-files +merge +merge tool: mergetool +move: mv +pull +push +range diff: range-diff +rebase +ref log: reflog +remote +remote add +remote remove +remote rename +remote set url: remote set-url +remote set you are el: remote set-url +remote show +rerere +rerere diff +rerere status +reset +restore +revert +remove: rm +short log: shortlog +show +sparse checkout: sparse-checkout +stash +stash apply +stash list +stash pop +stash push +stash show +stash save +status +submodule +submodule add +submodule in it: submodule init +submodule status +submodule update +switch +tag +worktree +worktree add +worktree list +worktree lock +worktree move +worktree prune +worktree remove +worktree repair +worktree unlock diff --git a/community/apps/github/github_web.talon b/community/apps/github/github_web.talon new file mode 100644 index 0000000..3c709c8 --- /dev/null +++ b/community/apps/github/github_web.talon @@ -0,0 +1,70 @@ +# https://help.github.com/en/github/getting-started-with-github/keyboard-shortcuts +tag: browser +browser.host: github.com +- + +# site wide shortcuts +focus search: key(s) +go to notifications: insert("gn") +go to dashboard: insert("gd") +(keyboard shortcuts show | show keyboard shortcuts): key(?) +(selection move down | move selection down): key(j) +(selection move up | move selection up): key(k) +(selection toggle | toggle selection): key(x) +(selection open | open selection): key(o) + +# repositories +go to code: insert("gc") +go to issues: insert("gi") +go to pull requests: insert("gp") +go to wiki: insert("gw") +go to actions: insert("ga") +go to projects: insert("gb") +go to discussions: insert("gg") + +# source code editing +[web] editor open: key(.) + +# source code browsing +(file find | find file): key(t) +jump to line: key(l) +((branch | tag) switch | switch (branch | tag)): key(w) +(url expand | expand url): key(y) +(show | hide) comments: key(i) +blame view open: key(b) +(show | hide) annotations: key(a) + +# issues +(issue create | create [an] issue): key(c) +search (issues | [pull] requests): key(/) +(filter by | edit) labels: key(l) +(filter by | edit) milestones: key(m) +(filter by | edit) assignee: key(a) +reply: key(r) +(comment submit | submit comment): key(ctrl-enter) +(comment preview | preview comment): key(ctrl-shift-p) +git hub full screen: key(ctrl-shift-l) + +# browsing commit +(form close | close form): key(escape) +parent commit: key(p) +other parent commit: key(o) + +# notifications +mark as read: key(y) +(thread mute | mute thread): key(shift-m) + +# issue or pull request list +(issue open | open issue): key(o) +(issue create | create issue): key(c) + +# issues and pull requests +reviewer request: key(q) +milestone set: key(m) +assignee set: key(a) +label set: key(l) + +# actions +go to workflow: insert("gf") +timestamps toggle: key(shift-t) +fullscreen toggle: key(shift-f) diff --git a/community/apps/gitlab/gitlab.talon b/community/apps/gitlab/gitlab.talon new file mode 100644 index 0000000..a138b53 --- /dev/null +++ b/community/apps/gitlab/gitlab.talon @@ -0,0 +1,60 @@ +# Shortcuts taken from: https://docs.gitlab.com/ee/user/shortcuts.html +# +tag: browser +browser.host: /gitlab\.com/ +#win.title: /GitLab/ +- + +# global shortcuts +show shortcuts: key(?) +go to projects [page]: key(shift-p) +go to groups [page]: key(shift-g) +go to activity [page]: key(shift-a) +go to milestones [page]: key(shift-l) +go to snippets [page]: key(shift-s) +search page: key(s) +go to issues [page]: key(shift-i) +go to merge requests [page]: key(shift-m) +go to to do [list] [page]: key(shift-t) +(show | hide) performance bar: key(p) + +edit last comment: key(1) +toggle mark down [preview]: key(ctrl-shift-p) + +# projects +go [to] project home [page]: insert("gp") +go [to] project activity [feed]: insert("gv") +go [to] project releases [list]: insert("gr") +go [to] project files [list]: insert("gf") +go [to] project file search [page]: key(t) +go [to] project (commit | commits) [list]: insert("gc") +go [to] (repository | repo) graph [page]: insert("gn") +go [to] (repository | repo) charts: insert("gd") +go [to] project issues [list]: insert("gi") +go [to] new issues [list]: insert("i") +go [to] project issues boards [list]: insert("gb") +go [to] project merge requests [list]: insert("gm") +go [to] jobs [list]: insert("gj") +go [to] project metrics: insert("gl") +go [to] project environments: insert("ge") +go [to] project cubes: insert("gk") +go [to] project snippets [list]: insert("gs") +go [to] project wiki: insert("gw") + +# issues and merge requests +edit description: key(e) +change assignee: key(a) +change milestone: key(m) +change label: key(l) +right comment: key(r) +next [unresolved] discussion: key(n) +previous [unresolved] discussion: key(p) +next file: key(]) +previous file: key([) + +# project files +back to files: key(escape) +open permalink: key(y) + +# wiki pages +edit page: key(e) diff --git a/community/apps/gnome_terminal/gnome_terminal.py b/community/apps/gnome_terminal/gnome_terminal.py new file mode 100644 index 0000000..5946e4d --- /dev/null +++ b/community/apps/gnome_terminal/gnome_terminal.py @@ -0,0 +1,93 @@ +from talon import Context, Module, actions + +# App definition +mod = Module() +mod.apps.gnome_terminal = """ +os: linux +and app.exe: gnome-terminal-server +os: linux +and app.name: Gnome-terminal +os: linux +and app.name: Mate-terminal +""" + +# Context matching +ctx = Context() +ctx.matches = r""" +app: gnome_terminal +""" + + +# --- Implement actions --- +@ctx.action_class("user") +class user_actions: + # user.tabs + def tab_jump(number): + actions.key(f"alt-{number}") + + +@ctx.action_class("app") +class app_actions: + # app.tabs + def tab_open(): + actions.key("ctrl-shift-t") + + def tab_previous(): + actions.key("ctrl-pageup") + + def tab_next(): + actions.key("ctrl-pagedown") + + def tab_close(): + actions.key("ctrl-shift-w") + + # global (overwrite linux/app.py) + def window_open(): + actions.key("ctrl-shift-n") + + def window_close(): + actions.key("ctrl-shift-q") + + +# global (overwrite linux/edit.py) +@ctx.action_class("edit") +class EditActions: + def page_down(): + actions.key("shift-pagedown") + + def page_up(): + actions.key("shift-pageup") + + def paste(): + actions.key("ctrl-shift-v") + + def copy(): + actions.key("ctrl-shift-c") + + def find(text: str = None): + actions.key("ctrl-shift-f") + if text: + actions.insert(text) + + def delete_line(): + actions.edit.line_start() + actions.key("ctrl-k") + + # afaik not possible in gnome-terminal + def extend_left(): + pass + + def extend_right(): + pass + + def extend_up(): + pass + + def extend_down(): + pass + + def extend_word_left(): + pass + + def extend_word_right(): + pass diff --git a/community/apps/gnome_terminal/gnome_terminal.talon b/community/apps/gnome_terminal/gnome_terminal.talon new file mode 100644 index 0000000..e86530c --- /dev/null +++ b/community/apps/gnome_terminal/gnome_terminal.talon @@ -0,0 +1,8 @@ +app: gnome_terminal +- +# Set tags +tag(): terminal +tag(): user.tabs +tag(): user.generic_unix_shell +tag(): user.git +tag(): user.kubectl diff --git a/community/apps/guake/guake_linux.py b/community/apps/guake/guake_linux.py new file mode 100644 index 0000000..34c9909 --- /dev/null +++ b/community/apps/guake/guake_linux.py @@ -0,0 +1,23 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: linux +app: Guake +""" +ctx.tags = ["user.git", "user.kubectl", "user.tabs", "terminal"] + + +@ctx.action_class("app") +class AppActions: + def tab_open(): + actions.key("ctrl-shift-t") + + def tab_close(): + actions.key("ctrl-shift-w") + + def tab_next(): + actions.key("ctrl-pagedown") + + def tab_previous(): + actions.key("ctrl-pageup") diff --git a/community/apps/i3wm/i3wm.py b/community/apps/i3wm/i3wm.py new file mode 100644 index 0000000..3e5840a --- /dev/null +++ b/community/apps/i3wm/i3wm.py @@ -0,0 +1,118 @@ +import subprocess +from typing import Optional, Union + +from talon import Context, Module, actions, settings + +mod = Module() +ctx = Context() + +mod.tag("i3wm", desc="tag for loading i3wm related files") +mod.setting( + "i3_config_path", + type=str, + default="~/.i3/config", + desc="Where to find the configuration path", +) +mod.setting( + "i3_mod_key", + type=str, + default="super", + desc="The default key to use for i3wm commands", +) + +ctx.matches = """ +tag: user.i3wm +""" + + +@ctx.action_class("app") +class AppActions: + def window_close(): + subprocess.check_call(("i3-msg", "kill")) + + +@mod.action_class +class Actions: + def i3wm_mode(name: str): + """Switch i3 mode""" + subprocess.check_call(("i3-msg", "mode", name)) + + def i3wm_reload(): + """Reload the i3 config""" + subprocess.check_call(("i3-msg", "reload")) + + def i3wm_restart(): + """Restart the window manager""" + subprocess.check_call(("i3-msg", "restart")) + + def i3wm_layout(layout: Optional[str] = None): + """Change to specified layout. Toggle split if unspecified.""" + if layout is None: + subprocess.check_call(("i3-msg", "layout", "toggle", "split")) + else: + subprocess.check_call(("i3-msg", "layout", layout)) + + def i3wm_fullscreen(): + """Fullscreen the current container""" + subprocess.check_call(("i3-msg", "fullscreen")) + + def i3wm_split(direction: str): + """Split the focused container""" + subprocess.check_call(("i3-msg", "split", direction)) + + def i3wm_float(): + """Toggle whether the focused container should float.""" + subprocess.check_call(("i3-msg", "floating", "toggle")) + + def i3wm_launch(): + """Trigger the i3 launcher: ex rofi""" + key = settings.get("user.i3_mod_key") + actions.key(f"{key}-d") + + def i3wm_shell(): + """Launch a shell""" + key = settings.get("user.i3_mod_key") + actions.key(f"{key}-enter") + + def i3wm_focus(what: str): + """Move focus""" + subprocess.check_call(("i3-msg", "focus", what)) + + def i3wm_switch_to_workspace(which: Union[str, int]): + """Focus the specified workspace""" + if isinstance(which, int): + subprocess.check_call(("i3-msg", "workspace", "number", str(which))) + else: + subprocess.check_call(("i3-msg", "workspace", which)) + + def i3wm_show_scratchpad(): + """Focus/cycle/hide the scratchpad""" + subprocess.check_call(("i3-msg", "scratchpad", "show")) + + def i3wm_move(to: str): + """Move the focused container""" + subprocess.check_call(("i3-msg", "move", to)) + + def i3wm_move_to_workspace(which: Union[str, int]): + """Move the focused container to the specified workspace""" + if isinstance(which, int): + subprocess.check_call( + ("i3-msg", "move", "container", "to", "workspace", "number", str(which)) + ) + else: + subprocess.check_call( + ("i3-msg", "move", "container", "to", "workspace", which) + ) + + def i3wm_move_to_output(which: str): + """Move the focused container to the specified output.""" + subprocess.check_call(("i3-msg", "move", "container", "to", "output", which)) + + def i3wm_move_position(where: str): + """Move the focused container to the specified position.""" + subprocess.check_call(("i3-msg", "move", "position", where)) + + def i3wm_lock(): + """Trigger the lock screen""" + key = settings.get("user.i3_mod_key") + actions.key(f"{key}-shift-x") diff --git a/community/apps/i3wm/i3wm.talon b/community/apps/i3wm/i3wm.talon new file mode 100644 index 0000000..fafe4c8 --- /dev/null +++ b/community/apps/i3wm/i3wm.talon @@ -0,0 +1,99 @@ +# NOTE: If you want to use i3wm you must enable the tag settings.talon. i.e.: `tag(): user.i3wm` +os: linux +tag: user.i3wm +- +port : user.i3wm_switch_to_workspace(number_small) +(port flip | flipper): user.i3wm_switch_to_workspace("back_and_forth") +port right: user.i3wm_switch_to_workspace("next") +port left: user.i3wm_switch_to_workspace("prev") + +(win | window) left: user.i3wm_focus("left") +(win | window) right: user.i3wm_focus("right") +(win | window) up: user.i3wm_focus("up") +(win | window) down: user.i3wm_focus("down") +(win | window) kill: app.window_close() +(win | window) stacking: user.i3wm_layout("stacking") +(win | window) default: user.i3wm_layout() +(win | window) tabbed: user.i3wm_layout("tabbed") + +reload i three config: user.i3wm_reload() +restart i three: user.i3wm_restart() + +(full screen | scuba): user.i3wm_fullscreen() +toggle floating: user.i3wm_float() +focus floating: user.i3wm_focus("mode_toggle") +center window: user.i3wm_move_position("center") +resize mode: user.i3wm_mode("resize") +focus parent: user.i3wm_focus("parent") +focus child: user.i3wm_focus("child") + +# resize helpers +grow window: + user.i3wm_mode("resize") + key(right:10) + key(down:10) + # escape resize mode + key(escape) + # center window + sleep(200ms) + user.i3wm_move_position("center") + +# resize helpers +shrink window: + user.i3wm_mode("resize") + key(left:10) + key(up:10) + # escape resize mode + key(escape) + # center window + sleep(200ms) + user.i3wm_move_position("center") + +horizontal (shell | terminal): + user.i3wm_split("h") + user.i3wm_shell() + +vertical (shell | terminal): + user.i3wm_split("v") + user.i3wm_shell() + +# XXX - just replace with shuffle eventually? +# XXX - like also need to match the generic talon commands +(shuffle | move (win | window) [to] port) : + user.i3wm_move_to_workspace(number_small) +(shuffle | move (win | window) [to] last port): + user.i3wm_move_to_workspace("back_and_forth") +(shuffle | move) flipper: user.i3wm_move_to_workspace("back_and_forth") +(shuffle | move (win | window) left): user.i3wm_move("left") +(shuffle | move (win | window) right): user.i3wm_move("right") +(shuffle | move (win | window) up): user.i3wm_move("up") +(shuffle | move (win | window) down): user.i3wm_move("down") + +(win | window) horizontal: user.i3wm_split("h") +(win | window) vertical: user.i3wm_split("v") + +make scratch: user.i3wm_move("scratchpad") +[(show | hide)] scratch: user.i3wm_show_scratchpad() +next scratch: + user.i3wm_show_scratchpad() + user.i3wm_show_scratchpad() + +# these rely on the user settings for the mod key. see i3wm.py Actions class +launch: user.i3wm_launch() +launch : + user.i3wm_launch() + sleep(100ms) + insert("{text}") +lock screen: user.i3wm_lock() + +(launch shell | koopa): user.i3wm_shell() + +new scratch (shell | window): + user.i3wm_shell() + sleep(200ms) + user.i3wm_move("scratchpad") + user.i3wm_show_scratchpad() + +murder: + user.deprecate_command("2023-02-04", "murder", "win kill") + app.window_close() diff --git a/community/apps/iterm/iterm.py b/community/apps/iterm/iterm.py new file mode 100644 index 0000000..3ea8060 --- /dev/null +++ b/community/apps/iterm/iterm.py @@ -0,0 +1,28 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.apps.iterm2 = """ +os: mac +and app.bundle: com.googlecode.iterm2 +""" +ctx.matches = r""" +app: iterm2 +""" + +directories_to_remap = {} +directories_to_exclude = {} + + +@ctx.action_class("user") +class UserActions: + def tab_jump(number: int): + actions.key(f"cmd-{number}") + + def tab_final(): + actions.key("cmd-9") + + def terminal_clear_screen(): + """Clear screen""" + actions.key("ctrl-l") diff --git a/community/apps/iterm/iterm.talon b/community/apps/iterm/iterm.talon new file mode 100644 index 0000000..f779f05 --- /dev/null +++ b/community/apps/iterm/iterm.talon @@ -0,0 +1,11 @@ +os: mac +app: iterm2 +- +tag(): terminal +# todo: filemanager support +#tag(): user.file_manager +tag(): user.generic_unix_shell +tag(): user.git +tag(): user.kubectl +tag(): user.tabs +tag(): user.readline diff --git a/community/apps/jetbrains/jetbrains.py b/community/apps/jetbrains/jetbrains.py new file mode 100644 index 0000000..300ede8 --- /dev/null +++ b/community/apps/jetbrains/jetbrains.py @@ -0,0 +1,405 @@ +import os +import os.path +import tempfile +from pathlib import Path +from typing import Optional + +import requests +from talon import Context, Module, actions, app, clip, ui + +# Courtesy of https://github.com/anonfunc/talon-user/blob/master/apps/jetbrains.py + +# 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... +port_mapping = { + "com.google.android.studio": 8652, + "com.jetbrains.AppCode": 8655, + "com.jetbrains.CLion": 8657, + "com.jetbrains.datagrip": 8664, + "com.jetbrains.goland-EAP": 8659, + "com.jetbrains.goland": 8659, + "com.jetbrains.intellij-EAP": 8653, + "com.jetbrains.intellij.ce": 8654, + "com.jetbrains.intellij": 8653, + "com.jetbrains.PhpStorm": 8662, + "com.jetbrains.pycharm": 8658, + "com.jetbrains.rider": 8660, + "com.jetbrains.rubymine": 8661, + "com.jetbrains.rubymine-EAP": 8661, + "com.jetbrains.WebStorm": 8663, + "google-android-studio": 8652, + "idea64.exe": 8653, + "IntelliJ IDEA": 8653, + "IntelliJ IDEA Community Edition": 8654, + "jetbrains-appcode": 8655, + "jetbrains-clion": 8657, + "jetbrains-datagrip": 8664, + "jetbrains-goland-eap": 8659, + "jetbrains-goland": 8659, + "jetbrains-idea-ce": 8654, + "jetbrains-idea-eap": 8653, + "jetbrains-idea": 8653, + "jetbrains-phpstorm": 8662, + "jetbrains-pycharm-ce": 8658, + "jetbrains-pycharm": 8658, + "jetbrains-rider": 8660, + "JetBrains Rider": 8660, + "jetbrains-rubymine": 8661, + "jetbrains-rubymine-eap": 8661, + "jetbrains-studio": 8652, + "jetbrains-webstorm": 8663, + "RubyMine": 8661, + "RubyMine-EAP": 8661, + "PyCharm": 8658, + "pycharm64.exe": 8658, + "WebStorm": 8663, + "webstorm64.exe": 8663, + "PhpStorm": 8662, + # Local plugin development: + "com.jetbrains.jbr.java": 8666, +} + + +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: + try: + with open(Path.home() / file_name) as fh: + return fh.read() + except FileNotFoundError: + print(f"Could not find {file_name} in tmp or home") + return None + except OSError as e: + print(e) + return None + + +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_") + 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 get_idea_location() -> list[str]: + return send_idea_command("location").split() + + +ctx = Context() +mod = Module() + +mod.apps.jetbrains = "app.name: /jetbrains/" +appNames = [ + "CLion", + "IntelliJ IDEA", + "PhpStorm", + "PyCharm", + "Webstorm", + "RubyMine", + "DataGrip", +] +for appName in appNames: + mod.apps.jetbrains = f"app.name: {appName}" + mod.apps.jetbrains = f"app.name: {appName}-EAP" +mod.apps.jetbrains = """ +os: mac +and app.bundle: com.google.android.studio +""" +# windows +mod.apps.jetbrains = r"app.exe: /^idea64\.exe$/i" +mod.apps.jetbrains = r"app.exe: /^PyCharm64\.exe$/i" +mod.apps.jetbrains = r"app.exe: /^webstorm64\.exe$/i" +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 +os: windows +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: + + def idea(commands: str): + """Send a command to Jetbrains product""" + 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(str(e)) + raise + + def idea_grab(times: int): + """Copies specified number of words to the left""" + old_clip = clip.get() + try: + original_line, original_column = get_idea_location() + for _ in range(times): + send_idea_command("action EditorSelectWord") + send_idea_command("action EditorCopy") + send_idea_command(f"goto {original_line} {original_column}") + send_idea_command("action EditorPaste") + finally: + clip.set(old_clip) + + +ctx.matches = r""" +app: jetbrains +""" + + +@ctx.action_class("app") +class AppActions: + def tab_next(): + actions.user.idea("action NextTab") + + def tab_previous(): + actions.user.idea("action PreviousTab") + + def tab_close(): + actions.user.idea("action CloseContent") + + def tab_reopen(): + actions.user.idea("action ReopenClosedTab") + + +@ctx.action_class("code") +class CodeActions: + # talon code actions + def toggle_comment(): + actions.user.idea("action CommentByLineComment") + + +@ctx.action_class("edit") +class EditActions: + # talon edit actions + def copy(): + actions.user.idea("action EditorCopy") + + def cut(): + actions.user.idea("action EditorCut") + + def delete(): + actions.user.idea("action EditorBackSpace") + + def paste(): + actions.user.idea("action EditorPaste") + + def find_next(): + actions.user.idea("action FindNext") + + def find_previous(): + actions.user.idea("action FindPrevious") + + def find(text: str = None): + actions.user.idea("action Find") + if text: + actions.insert(text) + + def line_clone(): + actions.user.idea("action EditorDuplicate") + + def line_swap_down(): + actions.user.idea("action MoveLineDown") + + def line_swap_up(): + actions.user.idea("action MoveLineUp") + + def indent_more(): + actions.user.idea("action EditorIndentLineOrSelection") + + def indent_less(): + actions.user.idea("action EditorUnindentSelection") + + def select_line(n: int = None): + actions.user.idea("action EditorSelectLine") + + def select_word(): + actions.user.idea("action EditorSelectWord") + + def select_all(): + actions.user.idea("action $SelectAll") + + def file_start(): + actions.user.idea("action EditorTextStart") + + def file_end(): + actions.user.idea("action EditorTextEnd") + + def extend_file_start(): + actions.user.idea("action EditorTextStartWithSelection") + + def extend_file_end(): + actions.user.idea("action EditorTextEndWithSelection") + + def extend_word_left(): + actions.user.idea("action EditorPreviousWordWithSelection") + + def extend_word_right(): + actions.user.idea("action EditorNextWordWithSelection") + + def jump_line(n: int): + actions.user.idea(f"goto {n} 0") + # move the cursor to the first nonwhite space character of the line + actions.user.idea("action EditorLineEnd") + actions.user.idea("action EditorLineStart") + + +@ctx.action_class("win") +class WinActions: + def filename() -> str: + title: str = actions.win.title() + result = title.split() + + # iterate over reversed result + # to support titles such as + # Class.Library2 – a.js [.workspace] + for word in reversed(result): + if not word.startswith("[") and "." in word: + return word + + return "" + + +@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: + actions.user.idea(f"action GoToTab{number}") + + def extend_until_line(line: int): + actions.user.idea(f"extend {line}") + + def select_range(line_start: int, line_end: int): + # if it's a single line, select the entire thing including the ending new-line5 + if line_start == line_end: + actions.user.idea(f"goto {line_start} 0") + actions.user.idea("action EditorSelectLine") + else: + actions.user.idea(f"range {line_start} {line_end}") + + def extend_camel_left(): + actions.user.idea("action EditorPreviousWordInDifferentHumpsModeWithSelection") + + def extend_camel_right(): + actions.user.idea("action EditorNextWordInDifferentHumpsModeWithSelection") + + def camel_left(): + actions.user.idea("action EditorPreviousWordInDifferentHumpsMode") + + def camel_right(): + actions.user.idea("action EditorNextWordInDifferentHumpsMode") + + def command_search(command: str = ""): + actions.user.idea("action GotoAction") + if command != "": + actions.insert(command) + + def line_clone(line: int): + actions.user.idea(f"clone {line}") + + # multi-cursor tag functions + def multi_cursor_enable(): + actions.skip() + + def multi_cursor_disable(): + actions.key("escape") + + def multi_cursor_add_above(): + actions.user.idea("action EditorCloneCaretAbove") + + def multi_cursor_add_below(): + actions.user.idea("action EditorCloneCaretBelow") + + def multi_cursor_select_fewer_occurrences(): + actions.user.idea("action UnselectPreviousOccurrence") + + def multi_cursor_select_more_occurrences(): + actions.user.idea("action SelectNextOccurrence") + + # def multi_cursor_skip_occurrence(): + def multi_cursor_select_all_occurrences(): + actions.user.idea("action SelectAllOccurrences") + + def multi_cursor_add_to_line_ends(): + actions.user.idea("action EditorAddCaretPerSelectedLine") + + # splits tag functions + # def split_window_right(): + # actions.user.idea("action OpenInRightSplit") + # def split_window_left(): + # def split_window_down(): + # def split_window_up(): + def split_window_vertically(): + actions.user.idea("action SplitVertically") + + def split_window_horizontally(): + actions.user.idea("action SplitHorizontally") + + def split_flip(): + actions.user.idea("action ChangeSplitOrientation") + + def split_maximize(): + actions.key("ctrl-shift-f12") + + def split_reset(): + actions.key("shift-f12") + + # def split_window(): + def split_clear(): + actions.user.idea("action Unsplit") + + def split_clear_all(): + actions.user.idea("action UnsplitAll") + + def split_next(): + actions.user.idea("action NextSplitter") + + # def split_last(): + # def split_number(index: int): diff --git a/community/apps/jetbrains/jetbrains.talon b/community/apps/jetbrains/jetbrains.talon new file mode 100644 index 0000000..86a9470 --- /dev/null +++ b/community/apps/jetbrains/jetbrains.talon @@ -0,0 +1,288 @@ +# Requires https://plugins.jetbrains.com/plugin/10504-voice-code-idea +app: jetbrains +- +tag(): user.line_commands +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 +complete: user.idea("action CodeCompletion") +perfect: user.idea("action CodeCompletion,action CodeCompletion") +smart: user.idea("action SmartTypeCompletion") +(done | finish): user.idea("action EditorCompleteStatement") +# Copying +grab : user.idea_grab(number) +action []: user.deprecate_command("2024-09-02", "action", "please") +# Refactoring +refactor: user.idea("action Refactorings.QuickListPopupAction") +refactor : + user.idea("action Refactorings.QuickListPopupAction") + insert(text) +extract variable: user.idea("action IntroduceVariable") +extract field: user.idea("action IntroduceField") +extract constant: user.idea("action IntroduceConstant") +extract parameter: user.idea("action IntroduceParameter") +extract interface: user.idea("action ExtractInterface") +extract method: user.idea("action ExtractMethod") +refactor in line: user.idea("action Inline") +refactor move: user.idea("action Move") +refactor rename: user.idea("action RenameElement") +rename file: user.idea("action RenameFile") +fix (format | formatting): user.idea("action ReformatCode") +fix imports: user.idea("action OptimizeImports") +#navigation +(go declaration | follow): user.idea("action GotoDeclaration") +go implementation: user.idea("action GotoImplementation") +go usage: user.idea("action FindUsages") +go type: user.idea("action GotoTypeDeclaration") +go test: user.idea("action GotoTest") +go back: user.idea("action Back") +go forward: user.idea("action Forward") +# Search +find (everywhere | all): user.idea("action SearchEverywhere") +find (everywhere | all) [over]: + user.idea("action SearchEverywhere") + sleep(500ms) + insert(text) +(search | find) class: user.idea("action GotoClass") +(search | find) file: user.idea("action GotoFile") +(search | find) path: user.idea("action FindInPath") +(search | find) symbol: user.idea("action GotoSymbol") +(search | find) symbol $: + user.idea("action GotoSymbol") + insert(text) + key("enter") +recent: user.idea("action RecentFiles") + +surround [this] with [over]: + user.idea("action SurroundWith") + sleep(500ms) + insert(text) +# Making these longer to reduce collisions with real code dictation. +insert generated [over]: + user.idea("action Generate") + sleep(500ms) + insert(text) +insert template [over]: + user.idea("action InsertLiveTemplate") + sleep(500ms) + insert(text) +create (template | snippet): user.idea("action SaveAsTemplate") +# Recording +toggle recording: user.idea("action StartStopMacroRecording") +change (recording | recordings): user.idea("action EditMacros") +play recording: user.idea("action PlaybackLastMacro") +play recording [over]: + user.idea("action PlaySavedMacrosAction") + insert(text) + sleep(500ms) + Key("enter") +# Marks +go mark: user.idea("action ShowBookmarks") +toggle mark: user.idea("action ToggleBookmark") +go next mark: user.idea("action GotoNextBookmark") +go last mark: user.idea("action GotoPreviousBookmark") +toggle mark : user.idea("action ToggleBookmark{number}") +go mark : user.idea("action GotoBookmark{number}") +# Folding +expand deep: user.idea("action ExpandRegionRecursively") +expand all: user.idea("action ExpandAllRegions") +collapse deep: user.idea("action CollapseRegionRecursively") +collapse all: user.idea("action CollapseAllRegions") +# miscellaneous +# XXX These might be better than the structural ones depending on language. +go next (method | function): user.idea("action MethodDown") +go last (method | function): user.idea("action MethodUp") +# Clipboard +clippings: user.idea("action PasteMultiple") +copy path: user.idea("action CopyPaths") +copy reference: user.idea("action CopyReference") +copy pretty: user.idea("action CopyAsRichText") +# File Creation +create sibling: user.idea("action NewElementSamePlace") +create sibling [over]: + user.idea("action NewElementSamePlace") + sleep(500ms) + insert(text) +create file: user.idea("action NewElement") +create file [over]: + user.idea("action NewElement") + sleep(500ms) + insert(text) +# Task Management +go task: user.idea("action tasks.goto") +go browser task: user.idea("action tasks.open.in.browser") +switch task: user.idea("action tasks.switch") +clear task: user.idea("action tasks.close") +configure servers: user.idea("action tasks.configure.servers") +# Git / Github (not using verb-noun-adjective pattern, mirroring terminal commands.) +git pull: user.idea("action Vcs.UpdateProject") +git commit: user.idea("action CheckinProject") +git push: user.idea("action CheckinProject") +git log: user.idea("action Vcs.ShowTabbedFileHistory") +git browse: user.idea("action Github.Open.In.Browser") +git (gets | gist): user.idea("action Github.Create.Gist") +git (pull request | request): user.idea("action Github.Create.Pull.Request") +git (view | show | list) (requests | request): + user.idea("action Github.View.Pull.Request") +git (annotate | blame): user.idea("action Annotate") +git menu: user.idea("action Vcs.QuickListPopupAction") +# Tool windows: +# Toggling various tool windows +toggle project: user.idea("action ActivateProjectToolWindow") +toggle find: user.idea("action ActivateFindToolWindow") +toggle run: user.idea("action ActivateRunToolWindow") +toggle debug: user.idea("action ActivateDebugToolWindow") +toggle events: user.idea("action ActivateEventLogToolWindow") +toggle terminal: user.idea("action ActivateTerminalToolWindow") +toggle git: user.idea("action ActivateVersionControlToolWindow") +toggle structure: user.idea("action ActivateStructureToolWindow") +toggle database: user.idea("action ActivateDatabaseToolWindow") +toggle database changes: user.idea("action ActivateDatabaseChangesToolWindow") +toggle make: user.idea("action ActivatemakeToolWindow") +toggle to do: user.idea("action ActivateTODOToolWindow") +toggle docker: user.idea("action ActivateDockerToolWindow") +toggle favorites: user.idea("action ActivateFavoritesToolWindow") +toggle last: user.idea("action JumpToLastWindow") +# Pin/dock/float +toggle pinned: user.idea("action TogglePinnedMode") +toggle docked: user.idea("action ToggleDockMode") +toggle floating: user.idea("action ToggleFloatingMode") +toggle windowed: user.idea("action ToggleWindowedMode") +toggle split: user.idea("action ToggleSideMode") +# Settings, not windows +toggle tool buttons: user.idea("action ViewToolButtons") +toggle toolbar: user.idea("action ViewToolBar") +toggle status [bar]: user.idea("action ViewStatusBar") +toggle navigation [bar]: user.idea("action ViewNavigationBar") +# Active editor settings +toggle power save: user.idea("action TogglePowerSave") +toggle whitespace: user.idea("action EditorToggleShowWhitespaces") +toggle indents: user.idea("action EditorToggleShowIndentLines") +toggle line numbers: user.idea("action EditorToggleShowLineNumbers") +toggle (bread crumbs | breadcrumbs): user.idea("action EditorToggleShowBreadcrumbs") +toggle gutter icons: user.idea("action EditorToggleShowGutterIcons") +toggle wrap: user.idea("action EditorToggleUseSoftWraps") +toggle parameters: user.idea("action ToggleInlineHintsAction") +# Toggleable views +toggle fullscreen: user.idea("action ToggleFullScreen") +toggle distraction [free mode]: user.idea("action ToggleDistractionFreeMode") +toggle presentation [mode]: user.idea("action TogglePresentationMode") +# Toggle additionals +toggle comment: code.toggle_comment() +# Quick popups +change scheme: user.idea("action QuickChangeScheme") +# Always javadoc +(toggle | pop) (doc | documentation): user.idea("action QuickJavaDoc") +(pop deaf | toggle definition): user.idea("action QuickImplementations") +pop type: user.idea("action ExpressionTypeInfo") +pop parameters: user.idea("action ParameterInfo") +# Breakpoints / debugging +go breakpoints: user.idea("action ViewBreakpoints") +toggle [line] breakpoint: user.idea("action ToggleLineBreakpoint") +toggle method breakpoint: user.idea("action ToggleMethodBreakpoint") +run menu: user.idea("action ChooseRunConfiguration") +run test: user.idea("action RunClass") +run test again: user.idea("action Rerun") +debug test: user.idea("action DebugClass") +step over: user.idea("action StepOver") +step into: user.idea("action StepInto") +step smart: user.idea("action SmartStepInto") +step to line: user.idea("action RunToCursor") +continue: user.idea("action Resume") +# Grow / Shrink +(grow | shrink) window right: user.idea("action ResizeToolWindowRight") +(grow | shrink) window left: user.idea("action ResizeToolWindowLeft") +(grow | shrink) window up: user.idea("action ResizeToolWindowUp") +(grow | shrink) window down: user.idea("action ResizeToolWindowDown") +# Movement +go next (error | air): user.idea("action GotoNextError") +go last (error | air): user.idea("action GotoPreviousError") +fix next (error | air): + user.idea("action GotoNextError") + user.idea("action ShowIntentionActions") +fix last (error | air): + user.idea("action GotoPreviousError") + user.idea("action ShowIntentionActions") +# Special Selects +select less: user.idea("action EditorUnSelectWord") +select (more | this): user.idea("action EditorSelectWord") +#jet brains-specific line commands. see line_commands.talon for generic ones +expand until : + user.select_range(number_1, number_2) + user.idea("action ExpandRegion") +collapse until : + user.select_range(number_1, number_2) + user.idea("action CollapseRegion") +paste until : + user.select_range(number_1, number_2) + user.idea("action EditorPaste") +refactor until : + user.select_range(number_1, number_2) + user.idea("action Refactorings.QuickListPopupAction") +clone : user.line_clone(number) + +#find/replace +clear last [over]: user.idea("find prev {text}, action EditorBackSpace") +clear next [over]: user.idea("find next {text}, action EditorBackSpace") +comment last [over]: + user.idea("find prev {text}, action CommentByLineComment") +comment next [over]: + user.idea("find next {text}, action CommentByLineComment") +go last [over]: user.idea("find prev {text}, action EditorRight") +go next [over]: user.idea("find next {text}, action EditorRight") +go [over]: + user.idea("goto {number} 0,find next {text}, action EditorRight") +paste last [over]: + user.idea("find prev {text}, action EditorRight, action EditorPaste") +paste next [over]: + user.idea("find next {text}, action EditorRight, action EditorPaste") +refactor [over]: + user.idea("goto {number} 0,find next {text}, action Refactorings.QuickListPopupAction") +refactor last [over]: + user.idea("find prev {text}, action Refactorings.QuickListPopupAction") +refactor next [over]: + user.idea("find next {text}, action Refactorings.QuickListPopupAction") +rename [over]: + user.idea("goto {number} 0,find next {text}, action RenameElement") +rename next [over]: user.idea("find next {text}, action RenameElement") +rename last [over]: user.idea("find prev {text}, action RenameElement") +complete [over]: + user.idea("goto {number} 0,find next {text},action CodeCompletion") +complete next [over]: user.idea("find next {text},action CodeCompletion") +complete last [over]: user.idea("find prev {text},action CodeCompletion") +quick fix [over]: + user.idea("goto {number} 0,find next {text},action ShowIntentionActions") +quick fix next [over]: + user.idea("find next {text},action ShowIntentionActions") +quick fix last [over]: + user.idea("find prev {text},action ShowIntentionActions") +replace last [over]: user.idea("find prev {text}, action EditorPaste") +replace next [over]: user.idea("find next {text}, action EditorPaste") + +follow [over]: + user.idea("goto {number} 0,find next {text},action GotoDeclaration") +follow next [over]: user.idea("find next {text},action GotoDeclaration") +follow last [over]: user.idea("find prev {text},action GotoDeclaration") + +reference [over]: + user.idea("goto {number} 0,find next {text},action FindUsages") +reference next [over]: user.idea("find next {text},action FindUsages") +reference last [over]: user.idea("find prev {text},action FindUsages") + +select last [over]: user.idea("find prev {text}") +select next [over]: user.idea("find next {text}") +select [over]: user.idea("goto {number} 0,find next {text}") + +select camel left: user.extend_camel_left() +select camel right: user.extend_camel_right() +go camel left: user.camel_left() +go camel right: user.camel_right() + +# requires plug-in: black-pycharm +blacken: user.idea("action BLACKReformatCode") diff --git a/community/apps/kde_konsole/kde_konsole.py b/community/apps/kde_konsole/kde_konsole.py new file mode 100644 index 0000000..953c730 --- /dev/null +++ b/community/apps/kde_konsole/kde_konsole.py @@ -0,0 +1,65 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +app.name: konsole +""" + + +@ctx.action_class("user") +class user_actions: + # tabs-tag functions implementations + def tab_jump(number): + actions.key(f"alt-{number}") + + # tab_final is not supported by konsole by default + # but short cut can be configured + + +@ctx.action_class("app") +class app_actions: + # tabs-tag functions implementations + def tab_open(): + actions.key("ctrl-shift-t") + + def tab_previous(): + actions.key("shift-left") + + def tab_next(): + actions.key("shift-right") + + def tab_close(): + actions.key("ctrl-shift-w") + + def tab_reopen(): + # TODO: decide whether this notification is good style + # (if this function wouldn't be defined here a wrong default would be activated) + actions.app.notify("tab reopen is not possible in kde konsole") + + def window_open(): + actions.key("ctrl-shift-n") + + +# this overwrites the unfitting parts of linux/edit.py +@ctx.action_class("edit") +class EditActions: + def page_down(): + actions.key("shift-pagedown") + + def page_up(): + actions.key("shift-pageup") + + def paste(): + actions.key("ctrl-shift-v") + + def copy(): + actions.key("ctrl-shift-c") + + def find(text: str = None): + actions.key("ctrl-shift-f") + if str: + actions.insert(text) + + # TODO: fix select line and other selection (like shift-right) + # see: https://unix.stackexchange.com/questions/485536/kde-konsole-swallows-shift-left-and-shift-right + # also fix extend_left and co diff --git a/community/apps/kde_konsole/kde_konsole.talon b/community/apps/kde_konsole/kde_konsole.talon new file mode 100644 index 0000000..e75f63e --- /dev/null +++ b/community/apps/kde_konsole/kde_konsole.talon @@ -0,0 +1,17 @@ +os: linux +and app.name: konsole +- +# makes the commands in terminal.talon available +tag(): terminal + +# activates the implementation of the commands/functions in terminal.talon +tag(): user.generic_unix_shell + +# makes commands for certain applications available +# you can deactivate them if you do not use the application +tag(): user.git +tag(): user.anaconda +# tag(): user.kubectl + +tag(): user.tabs +# TODO: add file_manager support diff --git a/community/apps/keepassx/keepassx_linux.talon b/community/apps/keepassx/keepassx_linux.talon new file mode 100644 index 0000000..0100efd --- /dev/null +++ b/community/apps/keepassx/keepassx_linux.talon @@ -0,0 +1,22 @@ +app: keepass +- +# Database +open database: key(ctrl-o) +save database: key(ctrl-s) +close database: key(ctrl-w) +lock database: key(ctrl-l) +quit: key(ctrl-q) + +# Entries +[add] new entry: key(ctrl-n) +clone entry: key(ctrl-k) +(view | edit) entry: key(ctrl-e) +delete entry: key(ctrl-d) +copy user [name]: key(ctrl-b) +copy password: key(ctrl-c) +open (earl | url | link): key(ctrl-u) +copy (earl | url | link): key(ctrl-alt-u) +find: key(ctrl-f) +find : + key(ctrl-f) + insert("{text}") diff --git a/community/apps/kindle/kindle.py b/community/apps/kindle/kindle.py new file mode 100644 index 0000000..95f31a5 --- /dev/null +++ b/community/apps/kindle/kindle.py @@ -0,0 +1,11 @@ +from talon import Module + +# --- App definition --- +mod = Module() +mod.apps.kindle = r""" +os: windows +and app.name: Kindle +os: windows +and app.exe: /^kindle\.exe$/i +""" +# TODO: mac context and implementation diff --git a/community/apps/kindle/kindle.talon b/community/apps/kindle/kindle.talon new file mode 100644 index 0000000..3c376f9 --- /dev/null +++ b/community/apps/kindle/kindle.talon @@ -0,0 +1,4 @@ +app: kindle +- +# Set tags +tag(): user.pages diff --git a/community/apps/kindle/kindle_win.py b/community/apps/kindle/kindle_win.py new file mode 100644 index 0000000..e37e118 --- /dev/null +++ b/community/apps/kindle/kindle_win.py @@ -0,0 +1,24 @@ +from talon import Context, actions + +# Context matching +ctx = Context() +ctx.matches = """ +os: windows +app: kindle +""" + + +# --- Implement actions --- +@ctx.action_class("user") +class UserActions: + # user.pages + def page_next(): + actions.key("down") + + def page_previous(): + actions.key("up") + + def page_jump(number: int): + actions.key("ctrl-g") + actions.insert(str(number)) + actions.key("enter") diff --git a/community/apps/kubectl/kubectl.py b/community/apps/kubectl/kubectl.py new file mode 100644 index 0000000..f5246f1 --- /dev/null +++ b/community/apps/kubectl/kubectl.py @@ -0,0 +1,26 @@ +from talon import Context, Module + +mod = Module() +mod.tag("kubectl", desc="tag for enabling kubectl commands in your terminal") +kubectl = "kubectl" + +ctx = Context() +ctx.matches = r""" +tag: user.kubectl +""" + +mod.list("kubectl_action", desc="actions performed by kubectl") +ctx.lists["self.kubectl_action"] = ("get", "delete", "describe", "label") + +mod.list("kubectl_object", desc="objects performed by kubectl") +ctx.lists["self.kubectl_object"] = ( + "nodes", + "jobs", + "pods", + "namespaces", + "services", + "events", + "deployments", + "replicasets", + "daemonsets", +) diff --git a/community/apps/kubectl/kubectl.talon b/community/apps/kubectl/kubectl.talon new file mode 100644 index 0000000..3f6bc88 --- /dev/null +++ b/community/apps/kubectl/kubectl.talon @@ -0,0 +1,66 @@ +tag: terminal +and tag: user.kubectl +- +cube [control]: "kubectl " + +cube create: "kubectl create " +cube expose: "kubectl expose " +cube run: "kubectl run " +cube set: "kubectl set " +cube run container: "kubectl run-container " + +cube explain: "kubectl explain " +cube get: "kubectl get " +cube edit: "kubectl edit " +cube delete: "kubectl delete " + +cube rollout: "kubectl rollout " +cube rolling update: "kubectl rolling-update " +cube scale: "kubectl scale " +cube auto scale: "kubectl autoscale " + +cube certificate: "kubectl certificate " +cube top: "kubectl top " +cube drain: "kubectl drain " +cube taint: "kubectl taint " +cube (cord | cordon): "kubectl cordon " +cube (uncord | uncordon): "kubectl uncordon " +cube cluster (info | information): "kubectl cluster-info " + +cube describe: "kubectl describe " +cube logs: "kubectl logs " +cube attach: "kubectl attach " +cube exec: "kubectl exec " +cube port forward: "kubectl port-forward " +cube proxy: "kubectl proxy " +cube copy: "kubectl cp " +cube auth: "kubectl auth " + +cube diff: "kubectl diff " +cube apply: "kubectl apply " +cube patch: "kubectl patch " +cube replace: "kubectl replace " +cube wait: "kubectl wait " +cube convert: "kubectl convert " +cube customize: "kubectl kustomize " + +cube label: "kubectl label " +cube annotate: "kubectl annotate " +cube completion: "kubectl completion " + +cube (interface | API): "kubectl api " +cube interface resources: "kubectl api-resources " +cube interface versions: "kubectl api-versions " +cube config: "kubectl config " +cube help: "kubectl help " +cube plugin: "kubectl plugin " +cube version: "kubectl version " + +cube {user.kubectl_action} [{user.kubectl_object}]: + insert("kubectl {kubectl_action} ") + insert(kubectl_object or "") + +cube detach: + key("ctrl-p") + key("ctrl-q") +cube shell: user.insert_between("kubectl exec -it ", " -- /bin/bash") diff --git a/community/apps/meld/meld.py b/community/apps/meld/meld.py new file mode 100644 index 0000000..564913e --- /dev/null +++ b/community/apps/meld/meld.py @@ -0,0 +1,44 @@ +from talon import Context, Module, actions + +mod = Module() +ctx = Context() + +apps = mod.apps +apps.meld = """ +os: windows +and app.name: Visual diff and merge tool +os: windows +and app.exe: meld.exe +""" + +ctx.matches = r""" +app: meld +""" + + +@ctx.action_class("app") +class AppActions: + def tab_open(): + actions.key("ctrl-n") + + def tab_previous(): + actions.key("ctrl-alt-pageup") + + def tab_next(): + actions.key("ctrl-alt-pagedown") + + def tab_reopen(): + print("Meld does not support this action.") + + +@ctx.action_class("user") +class UserActions: + def tab_jump(number): + if number < 10: + actions.key(f"alt-{number}") + + def tab_final(): + print("Meld does not support this action.") + + def tab_duplicate(): + print("Meld does not support this action.") diff --git a/community/apps/meld/meld.talon b/community/apps/meld/meld.talon new file mode 100644 index 0000000..f237c17 --- /dev/null +++ b/community/apps/meld/meld.talon @@ -0,0 +1,6 @@ +app: meld +- +tag(): user.tabs + +change next: key(alt-down) +change (previous | last): key(alt-up) diff --git a/community/apps/mintty/mintty_win.py b/community/apps/mintty/mintty_win.py new file mode 100644 index 0000000..783a30a --- /dev/null +++ b/community/apps/mintty/mintty_win.py @@ -0,0 +1,144 @@ +import subprocess + +from talon import Context, Module, actions, settings, ui + +mod = Module() +mod.apps.mintty = """ +os: windows +and app.name: Terminal +os: windows +and app.name: mintty.exe +""" + + +ctx = Context() +ctx.matches = r""" +app: mintty +""" +ctx.tags = [ + "terminal", + "user.generic_unix_shell", + "user.file_manager", + "user.git", + "user.kubectl", +] + +directories_to_remap = {} +directories_to_exclude = {} + +mod.setting( + "cygpath", + type=str, + default="C:\\cygwin64\\bin\\cygpath.exe", + desc="Path to cygpath.exe", +) + + +def get_win_path(cyg_path): + path = "" + try: + si = subprocess.STARTUPINFO() + si.dwFlags |= subprocess.STARTF_USESHOWWINDOW + path = ( + subprocess.check_output( + [settings.get("user.cygpath"), "-w", cyg_path], startupinfo=si + ) + .strip(b"\n") + .decode() + ) + except: + path = "" + return path + + +@ctx.action_class("edit") +class EditActions: + def paste(): + actions.key("shift-insert") + + def copy(): + actions.key("ctrl-insert") + + def delete_line(): + actions.key("ctrl-u") + + +@ctx.action_class("user") +class UserActions: + def file_manager_open_parent(): + actions.insert("cd ..") + actions.key("enter") + + def file_manager_current_path(): + path = ui.active_window().title + path = get_win_path(path) + + if path in directories_to_remap: + path = directories_to_remap[title] + + if path in directories_to_exclude: + path = "" + return path + + def file_manager_show_properties(): + """Shows the properties for the file""" + + def file_manager_open_directory(path: str): + """opens the directory that's already visible in the view""" + actions.insert("cd ") + path = f'"{path}"' + actions.insert(path) + actions.key("enter") + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(path) + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + name = f'"{name}"' + + actions.insert("mkdir " + name) + + def file_manager_open_file(path: str): + """opens the file""" + actions.insert(path) + actions.key("enter") + + def file_manager_select_file(path: str): + """selects the file""" + actions.insert(path) + + def file_manager_open_volume(volume: str): + """file_manager_open_volume""" + actions.user.file_manager_open_directory(volume) + + def terminal_list_directories(): + actions.insert("ls") + actions.key("enter") + + def terminal_list_all_directories(): + actions.insert("ls -a") + actions.key("enter") + + def terminal_change_directory(path: str): + actions.insert(f"cd {path}") + if path: + actions.key("enter") + + def terminal_change_directory_root(): + """Root of current drive""" + actions.insert("cd /") + actions.key("enter") + + def terminal_clear_screen(): + """Clear screen""" + actions.key("ctrl-l") + + def terminal_run_last(): + actions.key("up enter") + + def terminal_kill_all(): + actions.key("ctrl-c") + actions.insert("y") + actions.key("enter") diff --git a/community/apps/nautilus/nautilus.py b/community/apps/nautilus/nautilus.py new file mode 100644 index 0000000..9a75c84 --- /dev/null +++ b/community/apps/nautilus/nautilus.py @@ -0,0 +1,66 @@ +from talon import Context, Module, actions, clip, ui + +# App definition +mod = Module() +mod.apps.nautilus = """ +os: linux +and app.exe: nautilus +os: linux +and app.name: Org.gnome.Nautilus +os: linux +and app.name: Caja +""" + +# Context matching +ctx = Context() +ctx.matches = r""" +app: nautilus +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_next(): + actions.key("ctrl-pagedown") + + def tab_previous(): + actions.key("ctrl-pageup") + + +@ctx.action_class("user") +class UserActions: + # user.tabs + def tab_jump(number: int): + actions.key(f"alt-{number}") + + # user.navigation + def go_back(): + actions.key("alt-left") + + def go_forward(): + actions.key("alt-right") + + # user.file_manager + def file_manager_open_parent(): + actions.key("alt-up") + + def file_manager_show_properties(): + actions.key("ctrl-i") + + def file_manager_open_directory(path: str): + actions.key("ctrl-l") + actions.insert(path) + actions.key("enter") + + def file_manager_new_folder(name: str = None): + actions.key("ctrl-shift-n") + if name: + actions.insert(name) + + def file_manager_terminal_here(): + actions.key("ctrl-l") + with clip.capture() as path: + actions.edit.copy() + ui.launch(path="gnome-terminal", args=[f"--working-directory={path.get()}"]) diff --git a/community/apps/nautilus/nautilus.talon b/community/apps/nautilus/nautilus.talon new file mode 100644 index 0000000..4d0c06c --- /dev/null +++ b/community/apps/nautilus/nautilus.talon @@ -0,0 +1,5 @@ +app: nautilus +- +# Set tags +tag(): user.tabs +tag(): user.file_manager diff --git a/community/apps/nitro_reader/nitro_reader_5.py b/community/apps/nitro_reader/nitro_reader_5.py new file mode 100644 index 0000000..25bff2d --- /dev/null +++ b/community/apps/nitro_reader/nitro_reader_5.py @@ -0,0 +1,43 @@ +from talon import Context, Module, actions + +# --- App definition --- +mod = Module() +mod.apps.nitro_reader_five = r""" +os: windows +and app.name: Nitro Reader 5 +os: windows +and app.exe: /^nitropdfreader\.exe$/i +""" + +# Context matching +ctx = Context() +ctx.matches = """ +app: nitro_reader_five +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class app_actions: + # app.tabs + def tab_open(): + actions.key("ctrl-shift-o") + + +@ctx.action_class("user") +class UserActions: + # user.pages + def page_next(): + actions.key("right") + + def page_previous(): + actions.key("left") + + def page_jump(number: int): + actions.key("ctrl-g") + actions.edit.select_line() + actions.insert(str(number)) + actions.key("enter alt:2") + + def page_final(): + actions.key("end") diff --git a/community/apps/nitro_reader/nitro_reader_5.talon b/community/apps/nitro_reader/nitro_reader_5.talon new file mode 100644 index 0000000..a68d75b --- /dev/null +++ b/community/apps/nitro_reader/nitro_reader_5.talon @@ -0,0 +1,5 @@ +app: nitro_reader_five +- +# Set tags +tag(): user.pages +tag(): user.tabs diff --git a/community/apps/notepad++/notepad++_win.py b/community/apps/notepad++/notepad++_win.py new file mode 100644 index 0000000..751c05f --- /dev/null +++ b/community/apps/notepad++/notepad++_win.py @@ -0,0 +1,148 @@ +from talon import Context, Module, actions + +mod = Module() +ctx = Context() + +apps = mod.apps +apps.notepad_plus_plus = r""" +os: windows +and app.name: Notepad++ : a free (GNU) source code editor +os: windows +and app.name: Notepad++ : a free (GPL) source code editor +os: windows +and app.exe: /^notepad\+\+\.exe$/i +""" + +ctx.matches = r""" +app: notepad_plus_plus +""" + +ctx.tags = ["user.find_and_replace", "user.line_commands", "user.tabs"] + + +@ctx.action_class("app") +class AppActions: + def tab_open(): + actions.key("ctrl-n") + + def tab_previous(): + actions.key("ctrl-pageup") + + def tab_next(): + actions.key("ctrl-pagedown") + + +@ctx.action_class("code") +class CodeActions: + def toggle_comment(): + actions.key("ctrl-q") + + +@ctx.action_class("edit") +class EditActions: + def line_clone(): + actions.key("ctrl-d") + + def line_swap_up(): + actions.key("ctrl-shift-up") + + def line_swap_down(): + actions.key("ctrl-shift-down") + + def indent_more(): + actions.key("tab") + + def indent_less(): + actions.key("shift-tab") + + def jump_line(n: int): + actions.key("ctrl-g") + actions.insert(str(n)) + actions.key("enter") + + def find_next(): + actions.key("enter") + + def find_previous(): + actions.key("shift-enter") + + +@ctx.action_class("win") +class win_actions: + def filename(): + title = actions.win.title() + result = title.split(" - ")[0] + if "." in result: + # print(result.split("\\")[-1]) + return result.split("\\")[-1] + return "" + + +@ctx.action_class("user") +class UserActions: + def select_next_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("enter esc") + actions.sleep("100ms") + + def select_previous_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("shift-enter esc") + actions.sleep("100ms") + + def tab_jump(number: int): + if number < 10: + actions.key(f"ctrl-keypad_{number}") + + def tab_final(): + """Jumps to the final tab""" + print("Notepad doesn't support this...") + # actions.key("ctrl-numpad_0") + + # find_and_replace.py support begin + + def find_everywhere(text: str): + """Triggers find across project""" + + actions.key("ctrl-shift-f") + + if text: + actions.insert(text) + + def find_toggle_match_by_case(): + """Toggles find match by case sensitivity""" + actions.key("alt-c") + + def find_toggle_match_by_word(): + """Toggles find match by whole words""" + actions.key("alt-w") + + def find_toggle_match_by_regex(): + """Toggles find match by regex""" + actions.key("alt-g") + + def replace(text: str): + """Search and replaces in the active editor""" + actions.key("esc ctrl-h") + + if text: + actions.insert(text) + + def replace_everywhere(text: str): + """Search and replaces in the entire project""" + actions.key("esc ctrl-shift-f") + + if text: + actions.insert(text) + + def replace_confirm(): + """Confirm replace at current position""" + actions.key("alt-r") + + def replace_confirm_all(): + """Confirm replace all""" + actions.key("alt-a") + + # find_and_replace.py support end diff --git a/community/apps/notepad/notepad.py b/community/apps/notepad/notepad.py new file mode 100644 index 0000000..9770892 --- /dev/null +++ b/community/apps/notepad/notepad.py @@ -0,0 +1,22 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.apps.notepad = r""" +os: windows +and app.exe: notepad.exe +""" + +ctx.matches = r""" +app: notepad +""" + + +@ctx.action_class("win") +class win_actions: + def filename(): + filename = actions.win.title().split(" - ")[0] + if "." in filename: + return filename + return "" diff --git a/community/apps/notepad/notepad.talon b/community/apps/notepad/notepad.talon new file mode 100644 index 0000000..6593544 --- /dev/null +++ b/community/apps/notepad/notepad.talon @@ -0,0 +1,5 @@ +app: notepad +- + +tag(): user.tabs +tag(): user.find_and_replace diff --git a/community/apps/obsidian/obsidian.py b/community/apps/obsidian/obsidian.py new file mode 100644 index 0000000..bff0d02 --- /dev/null +++ b/community/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/community/apps/obsidian/obsidian.talon b/community/apps/obsidian/obsidian.talon new file mode 100644 index 0000000..bb33103 --- /dev/null +++ b/community/apps/obsidian/obsidian.talon @@ -0,0 +1,3 @@ +app: obsidian +- +tag(): user.tabs diff --git a/community/apps/okular/okular.py b/community/apps/okular/okular.py new file mode 100644 index 0000000..d4d21c8 --- /dev/null +++ b/community/apps/okular/okular.py @@ -0,0 +1,49 @@ +from talon import Context, Module, actions + +# --- App definition --- +mod = Module() +mod.apps.okular = r""" +os: windows +and app.name: okular.exe +os: windows +and app.exe: /^okular\.exe$/i +""" +mod.apps.okular = """ +os: linux +and app.name: okular +""" +# TODO: mac context and implementation + +# Context matching +ctx = Context() +ctx.matches = """ +os: windows +os: linux +app: okular +""" + + +# --- Implement actions --- +@ctx.action_class("user") +class UserActions: + # user.pages + def page_current(): + actions.key("ctrl-g") + page = actions.edit.selected_text() + actions.key("escape") + return int(page) + + def page_next(): + actions.key("l") + + def page_previous(): + actions.key("h") + + def page_jump(number: int): + actions.key("ctrl-g") + actions.sleep("100ms") + actions.insert(str(number)) + actions.key("enter") + + def page_final(): + actions.key("ctrl-end") diff --git a/community/apps/okular/okular.talon b/community/apps/okular/okular.talon new file mode 100644 index 0000000..8cfc62a --- /dev/null +++ b/community/apps/okular/okular.talon @@ -0,0 +1,4 @@ +app: okular +- +# Set tags +tag(): user.pages diff --git a/community/apps/opera/opera.py b/community/apps/opera/opera.py new file mode 100644 index 0000000..860d421 --- /dev/null +++ b/community/apps/opera/opera.py @@ -0,0 +1,18 @@ +from talon import Module + +mod = Module() +apps = mod.apps +apps.opera = "app.name: Opera" +apps.opera = "app.name: Opera Internet Browser" +apps.opera = """ +os: mac +and app.bundle: com.operasoftware.Opera +""" +apps.opera = r""" +os: windows +and app.exe: /^opera\.exe$/i +""" +apps.opera = """ +os: linux +and app.exe: opera +""" diff --git a/community/apps/opera/opera.talon b/community/apps/opera/opera.talon new file mode 100644 index 0000000..86e6438 --- /dev/null +++ b/community/apps/opera/opera.talon @@ -0,0 +1,4 @@ +app: opera +- +tag(): browser +tag(): user.tabs diff --git a/community/apps/opera/opera_mac.py b/community/apps/opera/opera_mac.py new file mode 100644 index 0000000..8f74e23 --- /dev/null +++ b/community/apps/opera/opera_mac.py @@ -0,0 +1,73 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: mac +app: opera +""" + + +@ctx.action_class("user") +class UserActions: + def tab_duplicate(): + actions.browser.focus_address() + actions.sleep("180ms") + possibly_edited_url = actions.edit.selected_text() + actions.key("esc:2") + actions.browser.focus_address() + actions.sleep("180ms") + url_address = actions.edit.selected_text() + actions.user.paste(possibly_edited_url) + actions.app.tab_open() + actions.user.paste(url_address) + actions.key("enter") + + def tab_final(): + raise NotImplementedError( + "Opera doesn't have a default shortcut for this functionality but it can be configured" + ) + + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + +@ctx.action_class("app") +class AppActions: + def tab_next(): + actions.key("cmd-alt-right") + + def tab_previous(): + actions.key("cmd-alt-left") + + +@ctx.action_class("browser") +class BrowserActions: + def bookmark_tabs(): + raise NotImplementedError("Opera doesn't support this functionality") + + def go_home(): + raise NotImplementedError("Opera doesn't support this functionality") + + def go_back(): + actions.browser.focus_page() + actions.next() + + def go_forward(): + actions.browser.focus_page() + actions.next() + + def show_downloads(): + actions.key("cmd-j") + + def show_extensions(): + actions.key("cmd-shift-e") + + def show_history(): + actions.key("cmd-shift-h") + + def focus_page(): + actions.key("cmd-alt-l") + + def reload_hard(): + actions.key("shift-5") diff --git a/community/apps/opera/opera_win_linux.py b/community/apps/opera/opera_win_linux.py new file mode 100644 index 0000000..6043c95 --- /dev/null +++ b/community/apps/opera/opera_win_linux.py @@ -0,0 +1,86 @@ +from talon import Context, actions, app + +ctx = Context() +ctx.matches = r""" +os: windows +os: linux +app: opera +""" + + +@ctx.action_class("user") +class UserActions: + def tab_duplicate(): + actions.browser.focus_address() + actions.sleep("180ms") + possibly_edited_url = actions.edit.selected_text() + actions.key("esc:2") + actions.browser.focus_address() + actions.sleep("180ms") + url_address = actions.edit.selected_text() + actions.user.paste(possibly_edited_url) + actions.app.tab_open() + actions.user.paste(url_address) + actions.key("enter") + + def tab_jump(number: int): + if number < 9: + actions.key(f"ctrl-{number}") + + def tab_final(): + raise NotImplementedError( + "Opera doesn't have a default shortcut for this functionality but it can be configured" + ) + + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + +@ctx.action_class("app") +class AppActions: + def preferences(): + actions.key("alt-p") + + def tab_next(): + actions.key("ctrl-pagedown") + + def tab_previous(): + actions.key("ctrl-pageup") + + +@ctx.action_class("browser") +class BrowserActions: + def bookmarks_bar(): + raise NotImplementedError( + "Opera doesn't have a default shortcut for this functionality but it can be configured" + ) + + def bookmark_tabs(): + raise NotImplementedError("Opera doesn't support this functionality") + + def go_home(): + raise NotImplementedError("Opera doesn't support this functionality") + + def go_back(): + actions.browser.focus_page() + actions.next() + + def go_forward(): + actions.browser.focus_page() + actions.next() + + def bookmarks(): + actions.key("ctrl-shift-b") + + def show_downloads(): + actions.key("ctrl-j") + + def show_extensions(): + actions.key("ctrl-shift-e") + + def focus_page(): + actions.key("f9") + + def reload_hard(): + actions.key("shift-5") diff --git a/community/apps/orion/orion.py b/community/apps/orion/orion.py new file mode 100644 index 0000000..c7db542 --- /dev/null +++ b/community/apps/orion/orion.py @@ -0,0 +1,45 @@ +from talon import Context, Module, actions, ui +from talon.mac import applescript + +ctx = Context() +mod = Module() +apps = mod.apps +mod.apps.orion = """ +os: mac +app.bundle: com.kagi.kagimacOS +""" + +ctx.matches = r""" +app: orion +""" + + +@ctx.action_class("user") +class UserActions: + def browser_open_address_in_new_tab(): + actions.key("cmd-enter") + + +@ctx.action_class("browser") +class BrowserActions: + def bookmark_tabs(): + raise NotImplementedError("Orion doesn't have a default shortcut for this") + + def show_clear_cache(): + actions.key("cmd-alt-e") + + def reload_hard(): + actions.key("cmd-alt-r") + + def show_downloads(): + actions.key("cmd-alt-l") + + def show_extensions(): + actions.key("cmd-shift-x") + + +@mod.action_class +class Actions: + def overview_tabs(): + "Toggle tab overview in Orion" + actions.key("cmd-shift-\\") diff --git a/community/apps/orion/orion.talon b/community/apps/orion/orion.talon new file mode 100644 index 0000000..e845d3e --- /dev/null +++ b/community/apps/orion/orion.talon @@ -0,0 +1,6 @@ +app: orion +- +tag(): browser +tag(): user.tabs + +tab overview [open | close]: user.overview_tabs() diff --git a/community/apps/outlook/outlook_web.talon b/community/apps/outlook/outlook_web.talon new file mode 100644 index 0000000..b8d9ce3 --- /dev/null +++ b/community/apps/outlook/outlook_web.talon @@ -0,0 +1,64 @@ +# https://support.office.com/en-us/article/keyboard-shortcuts-for-outlook-3cdeb221-7ae5-4c1d-8c1d-9e63216c1efd#PickTab=Web +# the shortcuts below our based half of the bill in short cut menu, but the +# link above has significantly more that could so be added +os: linux +tag: browser +win.title: /Outlook/ +- + +# write email +new message: key(n) +send [this] message: key(alt-s) +reply [to] [this] message: key(r) +reply all [to] [this] message: key(ctrl-shift-r) +forward [this] message: key(ctrl-shift-f) +save [draft]: key(ctrl-s) +discard [draft]: key(esc) +insert [a] [hyper] link: key(ctrl-k) + +# email list +(select | unselect) [this] message: key(ctrl-space) +select all [messages]: key(ctrl-a) +clear all [messages]: key(esc) +select first [message]: key(home) +select last [message]: key(and) + +# read email +open [this] message: key(o) +open [this] message [in] [a] new window: key(shift-enter) +close [this] message: key(esc) +[open] [the] next (item | message): key(ctrl-.) +[open] [the] (prev | previous) item: key(ctrl-,) +next reading [pane] (item | message): key(.) +(prev | previous) [pane] (item | message): key(,) +(expand | collapse) [conversation]: key(x) + +# go to +go [to] mail: key(ctrl-shift-1) +go [to] calendar: key(ctrl-shift-2) +go [to] people: key(ctrl-shift-3) +go [to] to do: key(ctrl-shift-4) +go [to] inbox: + key(g) + key(i) +go to drafts: + key(g) + key(d) +go to sent: + key(g) + key(s) +search [email]: key(alt-q) +show help: key(?) + +# email actions +undo [last] [action]: key(ctrl-z) +delete [this] [message]: key(delete) +(perm | permanently) delete [this] [message]: key(shift+delete) +new folder: key(shift-e) +mark [this] [(item | message)] as read: key(q) +mark [this] [(item | message)] as unread: key(u) +flag [this] [(item | message)]: key(insert) +archive: key(e) +mark [this] [message] [as] junk: key(j) +moved to [a] folder: key(v) +categorize [this] message: key(c) diff --git a/community/apps/outlook/outlook_win.talon b/community/apps/outlook/outlook_win.talon new file mode 100644 index 0000000..295ebac --- /dev/null +++ b/community/apps/outlook/outlook_win.talon @@ -0,0 +1,11 @@ +os: windows +and app: Outlook +- +archive: key(alt h o 1) +new e-mail: key(ctrl-n) +calendar: key(ctrl-2) +inbox: key(ctrl-1) +Reply: key(ctrl-r) +Reply all: key(ctrl-shift-r) +Forward: key(ctrl-f) +accept: key(shift-f10 c c enter) diff --git a/community/apps/powershell/README.md b/community/apps/powershell/README.md new file mode 100644 index 0000000..5baaa70 --- /dev/null +++ b/community/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/community/apps/powershell/powershell_win.py b/community/apps/powershell/powershell_win.py new file mode 100644 index 0000000..81f4fbe --- /dev/null +++ b/community/apps/powershell/powershell_win.py @@ -0,0 +1,79 @@ +from talon import Context, Module, actions, settings, ui + +ctx = Context() +mod = Module() +ctx.matches = r""" +app: windows_power_shell +app: windows_terminal +and win.title: /PowerShell/ +""" + +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: + def delete_line(): + actions.key("esc") + + +@ctx.action_class("user") +class UserActions: + def file_manager_refresh_title(): + actions.insert( + "$Host.UI.RawUI.WindowTitle = 'Windows PowerShell: ' + $(get-location)" + ) + actions.key("enter") + + def file_manager_open_parent(): + actions.insert("cd ..") + actions.key("enter") + if settings.get("user.powershell_always_refresh_title"): + actions.user.file_manager_refresh_title() + + def file_manager_current_path(): + path = ui.active_window().title + path = path.replace("Administrator: ", "").replace("Windows PowerShell: ", "") + + if path in directories_to_remap: + path = directories_to_remap[path] + + if path in directories_to_exclude: + path = "" + return path + + 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") + if settings.get("user.powershell_always_refresh_title"): + actions.user.file_manager_refresh_title() + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(f'"{path}"') + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + actions.insert(f'mkdir "{name}"') + + def file_manager_open_file(path: str): + """opens the file""" + actions.insert(f'./"{path}"') + actions.key("enter") + + def file_manager_select_file(path: str): + """selects the file""" + actions.insert(path) + + def file_manager_open_volume(volume: str): + """file_manager_open_volume""" + actions.user.file_manager_open_directory(volume) diff --git a/community/apps/powershell/powershell_win.talon b/community/apps/powershell/powershell_win.talon new file mode 100644 index 0000000..b0eabe0 --- /dev/null +++ b/community/apps/powershell/powershell_win.talon @@ -0,0 +1,21 @@ +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 +tag(): terminal + +# activates the implementation of the commands/functions in terminal.talon +tag(): user.generic_windows_shell + +# makes commands for certain applications available +# you can deactivate them if you do not use the application +tag(): user.git +tag(): user.anaconda +# tag(): user.kubectl + +tag(): user.file_manager diff --git a/community/apps/protonmail/protonmail.talon b/community/apps/protonmail/protonmail.talon new file mode 100644 index 0000000..358a0ee --- /dev/null +++ b/community/apps/protonmail/protonmail.talon @@ -0,0 +1,81 @@ +tag: browser +win.title: /ProtonMail/ +- +# General +## Application +open help: key(?) +[focus] search: key(/) +confirm active: key(enter) +close active: key(escape) +open command [palette]: key(shift-space) + +## Composer +new message: key(c) +send message: key(ctrl-enter) +save message: key(ctrl-s) + +# Mail +## Jumping +(go | jump) [to] inbox: + key(g) + key(i) +(go | jump) [to] draft: + key(g) + key(d) +(go | jump) [to] sent: + key(g) + key(s) +(go | jump) [to] starred: + key(g) + key(.) +(go | jump) [to] archive: + key(g) + key(a) +(go | jump) [to] spam: + key(g) + key(x) +(go | jump) [to] trash: + key(g) + key(t) + +## Navigation +(prev | previous) message: key(up) +next message: key(down) +exit message: key(left) +enter message: key(right) +(show | display) newer [message]: key(k) +(show | display) older [message]: key(j) +open message: key(enter) +go back: key(escape) + +## Threadlist +select all: + key(*) + key(a) +(deselect | unselect) all: + key(*) + key(n) +select [the] (message | conversation): key(x) +mark [as] read: key(r) +mark [as] unread: key(u) +star (message | conversation): key(.) +move to inbox: key(i) +move to trash: key(t) +move to archive: key(a) +move to spam: key(s) + +## Actions +reply to (message | conversation): key(shift-r) +reply all [to] (message | conversation): key(shift-a) +forward (message | conversation): key(shift-f) + +# Contacts +## Contact List +(prev | previous) contact: key(up) +next contact: key(down) +enter contact: key(right) +delete contact: key(t) + +## Contact Details +exit contact: key(left) +save contact: key(ctrl-s) diff --git a/community/apps/rstudio/rstudio_mac.talon b/community/apps/rstudio/rstudio_mac.talon new file mode 100644 index 0000000..50d5803 --- /dev/null +++ b/community/apps/rstudio/rstudio_mac.talon @@ -0,0 +1,127 @@ +os: mac +app: RStudio +- + +run that: key("cmd-enter") +run document: key("cmd-alt-r") +run from top: key("cmd-alt-b") +run to end: key("cmd-alt-e") +run (function | funk): key("cmd-alt-f") +run section: key("cmd-alt-t") +run previous chunks: key("cmd-alt-p") +run chunk: key("cmd-alt-c") +run next chunk: key("cmd-alt-n") +run all: key("cmd-shift-s") +run knitter: key("cmd-shift-k") +run profiler: key("cmd-shift-alt-p") + +# Moving around and formatting +jump back: key("cmd-f9") +jump forward: key("cmd-f10") +close all tabs: key("cmd-shift-w") +indent lines: key("cmd-i") +toggle comment: key("cmd-shift-c") +reformat comment: key("cmd-shift-/") +reformat R code: key("cmd-shift-a") +line up: key("alt-up") +line down: key("alt-down") +duplicate line up: key("cmd-alt-up") +duplicate line [down]: key("cmd-alt-down") +select to paren: key("ctrl-shift-e") +select to matching paren: key("ctrl-shift-alt-e") +jump to matching: key("ctrl-p") +expand selection: key("shift-alt-cmd-up") +reduce selection: key("shift-alt-cmd-down") +add cursor up: key("ctrl-alt-up") +add cursor down: key("ctrl-alt-down") +move active cursor up: key("ctrl-alt-shift-up") +move active cursor down: key("ctrl-alt-shift-down") +delete line: key("cmd-d") +delete word left: key("alt-backspace") +delete word right: key("alt-delete") +assign that: key("alt--") +pipe that: key("cmd-shift-m") +insert knitter chunk: key("cmd-alt-i") + +# Folding +fold that: key("cmd-alt-l") +unfold that: key("cmd-shift-alt-l") +fold all: key("cmd-alt-o") +unfold all: key("cmd-shift-alt-o") + +# Find and replace +find and replace: key("cmd-f") +find next: key("cmd-g") +find previous: key("cmd-shift-g") +find with selection: key("cmd-e") +find in files: key("cmd-shift-f") +run replace: key("cmd-shift-j") +run spell check: key("f7") + +# Navigation and panels +go to source: key("ctrl-1") +go to console: key("ctrl-2") +go to help: key("ctrl-3") +go to history: key("ctrl-4") +go to files: key("ctrl-5") +go to (plots | plot): key("ctrl-6") +go to packages: key("ctrl-7") +go to environment: key("ctrl-8") +go to git: key("ctrl-9") +go to build: key("ctrl-0") +go to terminal: key("alt-shift-t") +go to omni: key("ctrl-.") +go to line: key("cmd-shift-alt-g") +go to section: key("cmd-shift-alt-j") +go to tab: key("ctrl-shift-.") +go to previous tab: key("ctrl-f11") +go to next tab: key("ctrl-f12") +go to first tab: key("ctrl-shift-f11") +go to last tab: key("ctrl-shift-f12") + +zoom source: key("ctrl-shift-1") +(zoom | show) all: key("ctrl-shift-0") + +help that: key("f1") +define that: key("f2") +previous plot: key("cmd-alt-f11") +next plot: key("cmd-alt-f12") + +# devtools, package development, and session management +restart R session: key("cmd-shift-f10") +dev tools build: key("cmd-shift-b") +dev tools load all: key("cmd-shift-l") +dev tools test: key("cmd-shift-t") +dev tools check: key("cmd-shift-e") +dev tools document: key("cmd-shift-d") + +# Debugging +toggle breakpoint: key("shift-f9") +debug next: key("f10") +debug step into (function | funk): key("shift-f4") +debug finish (function | funk): key("shift-f6") +debug continue: key("shift-f5") +debug stop: key("shift-f8") + +# Git/SVN +run git diff: key("ctrl-alt-d") +run git commit: key("ctrl-alt-m") + +# Other shortcuts that could be enabled +# run line and stay: key("alt-enter") +# run and echo all: key("cmd-shift-enter") +# extract (function|funk): key("cmd-alt-x") +# extract variable: key("cmd-alt-v") +# new terminal: key("shift-alt-t") +# rename current terminal: key("shift-alt-r") +# clear current terminal: key("ctrl-shift-l") +# previous terminal: key("ctrl-alt-f11") +# next terminal: key("ctrl-alt-f12") +# clear console: key("ctrl-l") +# popup history: key("cmd-up") +# change working directory: key("ctrl-shift-h") +# new document: key("cmd-shift-n") +# new document (chrome only): key("cmd-shift-alt-n") +# insert code section: key("cmd-shift-r") +# scroll diff view: key("ctrl-up/down") +# sync editor & pdf preview: key("cmd-f8") diff --git a/community/apps/safari/safari.py b/community/apps/safari/safari.py new file mode 100644 index 0000000..ea60f81 --- /dev/null +++ b/community/apps/safari/safari.py @@ -0,0 +1,66 @@ +from talon import Context, Module, actions, ui +from talon.mac import applescript + +ctx = Context() +mod = Module() +apps = mod.apps +mod.apps.safari = """ +os: mac +app.bundle: com.apple.Safari +app.bundle: com.apple.SafariTechnologyPreview +""" + +ctx.matches = r""" +app: safari +""" + + +@ctx.action_class("user") +class UserActions: + def browser_open_address_in_new_tab(): + actions.key("cmd-enter") + + +@ctx.action_class("browser") +class BrowserActions: + def address() -> str: + try: + window = ui.active_app().windows()[0] + except IndexError: + return "" + try: + toolbar = window.children.find_one(AXRole="AXToolbar", max_depth=0) + address_field = toolbar.children.find_one( + AXRole="AXTextField", + AXIdentifier="WEB_BROWSER_ADDRESS_AND_SEARCH_FIELD", + ) + address = address_field.AXValue + except (ui.UIErr, AttributeError): + address = applescript.run( + f""" + tell application id "{actions.app.bundle()}" + with timeout of 0.1 seconds + if not (exists (window 1)) then return "" + return window 1's current tab's URL + end timeout + end tell + """ + ) + return address + + def bookmark_tabs(): + raise NotImplementedError( + "Safari doesn't have a default shortcut for this functionality but it can be configured" + ) + + def show_clear_cache(): + raise NotImplementedError("Safari doesn't support this functionality") + + def reload_hard(): + actions.key("cmd-alt-r") + + def show_downloads(): + actions.key("cmd-alt-l") + + def show_extensions(): + actions.key("cmd-, tab:8 space") diff --git a/community/apps/safari/safari.talon b/community/apps/safari/safari.talon new file mode 100644 index 0000000..303d31d --- /dev/null +++ b/community/apps/safari/safari.talon @@ -0,0 +1,4 @@ +app: safari +- +tag(): browser +tag(): user.tabs diff --git a/community/apps/signal/signal_linux.talon b/community/apps/signal/signal_linux.talon new file mode 100644 index 0000000..0c6d1f3 --- /dev/null +++ b/community/apps/signal/signal_linux.talon @@ -0,0 +1,41 @@ +app: signal +- +show shortcuts: key("ctrl-/") + +# Note that the order below matches Keyboard Shortcuts listings + +# Navigation +(next | nav | navigate) [by] (sec | section): key("ctrl-t") +(prev | previous) (chat | conversation): key("alt-down") +next (chat | conversation): key("alt-up") +(prev | previous) unread: key("alt-shift-down") +next unread: key("alt-shift-up") +[open] (pref | preferences): key("ctrl-,") +open conversation menu: key("ctrl-shift-l") +search: key("ctrl-f") +search chat: key("ctrl-shift-f") +focus (chat | composer): key("ctrl-shift-t") +open media: key("ctrl-shift-m") +open emoji: key("ctrl-shift-j") +open sticker: key("ctrl-shift-s") +record [voice] message: key("ctrl-shift-v") +archive chat: key("ctrl-shift-a") +unarchive chat: key("ctrl-shift-u") +(first | top) message: key("ctrl-up") +(last | bottom) message: key("ctrl-down") +close chat: key("ctrl-shift-c") + +# Messages +send it: key("enter") +message details: key("ctrl-d") +reply [message]: key("ctrl-shift-r") +react [message]: key("ctrl-shift-e") +save attachment: key("ctrl-s") +delete [message]: key("ctrl-shift-d") + +# Composer +send message: key("ctrl-enter") +expand chat: key("ctrl-shift-x") +attach [file]: key("ctrl-u") +remove [link] preview: key("ctrl-p") +remove [link] attachment: key("ctrl-shift-p") diff --git a/community/apps/slack/slack.py b/community/apps/slack/slack.py new file mode 100644 index 0000000..1b7c17c --- /dev/null +++ b/community/apps/slack/slack.py @@ -0,0 +1,119 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() +apps = mod.apps +apps.slack = "app.name: Slack" +mod.apps.slack = r""" +os: windows +and app.name: Slack +os: windows +and app.exe: /^slack\.exe$/i +""" +apps.slack = """ +os: mac +and app.bundle: com.tinyspeck.slackmacgap +""" +apps.slack = """ +tag: browser +browser.host: app.slack.com +""" +ctx.matches = r""" +app: slack +""" + + +@ctx.action_class("edit") +class EditActions: + def line_insert_down(): + actions.edit.line_end() + actions.key("shift-enter") + + +@mod.action_class +class Actions: + def slack_open_workspace(number: int): + """Opens the specified Slack workspace""" + + def slack_show_channel_info(): + """Shows the current channel's information""" + + def slack_section_next(): + """Selects the next Slack section""" + actions.key("f6") + + def slack_section_previous(): + """Selects the previous Slack section""" + actions.key("shift-f6") + + def slack_open_direct_messages(): + """Opens direct messages in Slack""" + + def slack_open_threads(): + """Opens threads in Slack""" + + def slack_go_back(): + """Navigates back in Slack""" + + def slack_go_forward(): + """Navigates forward in Slack""" + + def slack_open_activity(): + """Opens Activity in Slack""" + + def slack_open_directory(): + """Opens Directory in Slack""" + + def slack_open_unread_messages(): + """Opens Unread Messages in Slack""" + + def slack_open_starred_items(): + """Opens Starred Items in Slack""" + + def slack_toggle_full_screen(): + """Toggles full screen mode in Slack""" + + def slack_add_reaction(): + """Adds a reaction to the current message in Slack""" + + def slack_insert_command(): + """Inserts a command in Slack""" + + def slack_insert_link(): + """Inserts a link in Slack""" + + def slack_insert_code(): + """Inserts a code block in Slack""" + + def slack_start_bulleted_list(): + """Starts a bulleted list in Slack""" + + def slack_start_numbered_list(): + """Starts a numbered list in Slack""" + + def slack_insert_quotation(): + """Inserts a quotation in Slack""" + + def slack_toggle_bold(): + """Toggles bold formatting in Slack""" + + def slack_toggle_italic(): + """Toggles italic formatting in Slack""" + + def slack_toggle_strikethrough(): + """Toggles strikethrough formatting in Slack""" + + def slack_create_snippet(): + """Opens the menu for creating a snippet in Slack""" + + def slack_huddle(): + """Starts a huddle in Slack""" + + def slack_open_keyboard_shortcuts(): + """Opens the keyboard shortcuts menu in Slack""" + + def slack_toggle_left_sidebar(): + """Toggles the visibility of the left sidebar in Slack""" + + def slack_toggle_right_sidebar(): + """Toggles the visibility of the right sidebar in Slack""" diff --git a/community/apps/slack/slack.talon b/community/apps/slack/slack.talon new file mode 100644 index 0000000..6cbd705 --- /dev/null +++ b/community/apps/slack/slack.talon @@ -0,0 +1,72 @@ +app: slack +- +tag(): user.messaging +tag(): user.emoji + +# Workspace +workspace : user.slack_open_workspace(number) +# Channel +(slack | lack) [channel] info: user.slack_show_channel_info() +focus (move | next): key(ctrl-`) +(section | zone) [next]: user.slack_section_next() +(section | zone) (previous | last): user.slack_section_previous() +(slack | lack) (starred [items] | stars): user.slack_open_starred_items() +(slack | lack) [direct] messages: user.slack_open_direct_messages() +(slack | lack) threads: user.slack_open_threads() +(slack | lack) (history [next] | back | backward): user.slack_go_back() +(slack | lack) forward: user.slack_go_forward() + +# Messaging +grab left: key(shift-up) +grab right: key(shift-down) +add line: key(shift-enter) + +(slack | lack) (slap | slaw | slapper): edit.line_insert_down() +(element | bit) [next]: key(tab) +(element | bit) (previous | last): key(shift-tab) + +(slack | lack) (my stuff | activity): user.slack_open_activity() +(slack | lack) directory: user.slack_open_directory() + +(slack | lack) unread [messages]: user.slack_open_unread_messages() + +(go | undo | toggle) full: user.slack_toggle_full_screen() +(slack | lack) (react | reaction): user.slack_add_reaction() +(insert command | commandify): user.slack_insert_command() +insert link: user.slack_insert_link() +insert code: user.slack_insert_code() +(slack | lack) (bull | bullet | bulleted) [list]: user.slack_start_bulleted_list() +(slack | lack) (number | numbered) [list]: user.slack_start_numbered_list() +(slack | lack) (quotes | quotation): user.slack_insert_quotation() +bold: user.slack_toggle_bold() +(italic | italicize): user.slack_toggle_italic() +(strike | strikethrough): user.slack_toggle_strikethrough() +(slack | lack) snippet: user.slack_create_snippet() +# Calls +(slack | lack) huddle: user.slack_huddle() +([toggle] mute | unmute): key(m) +(slack | lack) ([toggle] video): key(v) +(slack | lack) invite: key(a) + +# Miscellaneous +emote : ":{text}:" +(slack | lack) shortcuts: user.slack_open_keyboard_shortcuts() +toggle left sidebar: user.slack_toggle_left_sidebar() +toggle right sidebar: user.slack_toggle_right_sidebar() + +# DEPRECATED +(move | next) focus: + app.notify("please use the voice command 'focus next' instead of 'next focus'") + key(ctrl-`) +[next] (section | zone): + app.notify("please use the voice command 'section next' instead of 'next section'") + key(f6) +(previous | last) (section | zone): + app.notify("please use the voice command 'section last' instead of 'last section'") + key(shift-f6) +[next] (element | bit): + app.notify("please use the voice command 'element next' instead of 'next element'") + key(tab) +(previous | last) (element | bit): + app.notify("please use the voice command 'element last' instead of 'last element'") + key(shift-tab) diff --git a/community/apps/slack/slack_mac.py b/community/apps/slack/slack_mac.py new file mode 100644 index 0000000..e6425c2 --- /dev/null +++ b/community/apps/slack/slack_mac.py @@ -0,0 +1,125 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: mac +app: slack +""" + + +@ctx.action_class("user") +class UserActions: + def messaging_workspace_previous(): + actions.key("cmd-shift-[") + + def messaging_workspace_next(): + actions.key("cmd-shift-]") + + def messaging_open_channel_picker(): + actions.key("cmd-k") + + def messaging_channel_previous(): + actions.key("alt-up") + + def messaging_channel_next(): + actions.key("alt-down") + + def messaging_unread_previous(): + actions.key("alt-shift-up") + + def slack_open_starred_items(): + actions.key("cmd-shift-s") + + def messaging_unread_next(): + actions.key("alt-shift-down") + + def messaging_open_search(): + actions.key("cmd-f") + + def messaging_mark_workspace_read(): + actions.key("shift-esc") + + def messaging_mark_channel_read(): + actions.key("esc") + + # Files and Snippets + def messaging_upload_file(): + actions.key("cmd-u") + + def slack_open_workspace(number: int): + actions.key(f"cmd-{number}") + + def slack_show_channel_info(): + actions.key("cmd-shift-i") + + def slack_open_direct_messages(): + actions.key("cmd-shift-k") + + def slack_open_threads(): + actions.key("cmd-shift-t") + + def slack_go_back(): + actions.key("cmd-[") + + def slack_go_forward(): + actions.key("cmd-]") + + def slack_open_activity(): + actions.key("cmd-shift-m") + + def slack_open_directory(): + actions.key("cmd-shift-e") + + def slack_open_unread_messages(): + actions.key("cmd-shift-a") + + def slack_toggle_full_screen(): + actions.key("ctrl-cmd-f") + + def slack_add_reaction(): + actions.key("cmd-shift-\\") + + def slack_insert_command(): + actions.key("cmd-shift-c") + + def slack_insert_link(): + actions.key("cmd-shift-u") + + def slack_insert_code(): + actions.key("cmd-shift-alt-c") + + def slack_start_bulleted_list(): + actions.key("cmd-shift-8") + + def slack_start_numbered_list(): + actions.key("cmd-shift-7") + + def slack_insert_quotation(): + actions.key("cmd-shift->") + + def slack_toggle_bold(): + actions.key("cmd-b") + + def slack_toggle_italic(): + actions.key("cmd-i") + + def slack_toggle_strikethrough(): + actions.key("cmd-shift-x") + + def slack_create_snippet(): + actions.key("cmd-shift-enter") + + def slack_huddle(): + actions.key("cmd-shift-h") + + def slack_open_keyboard_shortcuts(): + """Opens the keyboard shortcuts menu in Slack""" + actions.key("cmd-/") + + def slack_toggle_left_sidebar(): + """Toggles the visibility of the left sidebar in Slack""" + actions.key("cmd-shift-d") + + def slack_toggle_right_sidebar(): + """Toggles the visibility of the right sidebar in Slack""" + actions.key("cmd-.") diff --git a/community/apps/slack/slack_win.py b/community/apps/slack/slack_win.py new file mode 100644 index 0000000..fbe1b40 --- /dev/null +++ b/community/apps/slack/slack_win.py @@ -0,0 +1,127 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: windows +os: linux +app: slack +""" + + +@ctx.action_class("user") +class UserActions: + def messaging_workspace_previous(): + actions.key("ctrl-shift-tab") + + def messaging_workspace_next(): + actions.key("ctrl-tab") + + def messaging_open_channel_picker(): + actions.key("ctrl-k") + + def messaging_channel_previous(): + actions.key("alt-up") + + def messaging_channel_next(): + actions.key("alt-down") + + def messaging_unread_previous(): + actions.key("alt-shift-up") + + def messaging_unread_next(): + actions.key("alt-shift-down") + + # (go | undo | toggle) full: key(ctrl-cmd-f) + def messaging_open_search(): + actions.key("ctrl-f") + + def messaging_mark_workspace_read(): + actions.key("shift-esc") + + def messaging_mark_channel_read(): + actions.key("esc") + + # Files and Snippets + def messaging_upload_file(): + actions.key("ctrl-u") + + def slack_open_workspace(number: int): + actions.key(f"ctrl-{number}") + + def slack_show_channel_info(): + actions.key("ctrl-shift-i") + + def slack_open_direct_messages(): + actions.key("ctrl-shift-k") + + def slack_open_threads(): + actions.key("ctrl-shift-t") + + def slack_go_back(): + actions.key("alt-left") + + def slack_go_forward(): + actions.key("alt-right") + + def slack_open_activity(): + actions.key("ctrl-shift-m") + + def slack_open_directory(): + actions.key("ctrl-shift-e") + + def slack_open_unread_messages(): + actions.key("ctrl-shift-a") + + def slack_open_starred_items(): + actions.key("ctrl-shift-s") + + def slack_toggle_full_screen(): + actions.key("ctrl-ctrl-f") + + def slack_add_reaction(): + actions.key("ctrl-shift-\\") + + def slack_insert_command(): + actions.key("ctrl-shift-c") + + def slack_insert_link(): + actions.key("ctrl-shift-u") + + def slack_insert_code(): + actions.insert("```") + + def slack_start_bulleted_list(): + actions.key("ctrl-shift-8") + + def slack_start_numbered_list(): + actions.key("ctrl-shift-7") + + def slack_insert_quotation(): + actions.key("ctrl-shift-9") + + def slack_toggle_bold(): + actions.key("ctrl-b") + + def slack_toggle_italic(): + actions.key("ctrl-i") + + def slack_toggle_strikethrough(): + actions.key("ctrl-shift-x") + + def slack_create_snippet(): + actions.key("ctrl-shift-enter") + + def slack_huddle(): + actions.key("ctrl-shift-h") + + def slack_open_keyboard_shortcuts(): + """Opens the keyboard shortcuts menu in Slack""" + actions.key("ctrl-/") + + def slack_toggle_left_sidebar(): + """Toggles the visibility of the left sidebar in Slack""" + actions.key("ctrl-shift-d") + + def slack_toggle_right_sidebar(): + """Toggles the visibility of the right sidebar in Slack""" + actions.key("ctrl-.") diff --git a/community/apps/stata/stata.py b/community/apps/stata/stata.py new file mode 100644 index 0000000..9223987 --- /dev/null +++ b/community/apps/stata/stata.py @@ -0,0 +1,21 @@ +from talon import Context, Module + +mod = Module() +ctx = Context() + +mod.apps.stata = r""" +os: windows +and app.name: Stata +os: windows +and app.exe: /^statase\-64\.exe$/i +""" + +ctx.matches = r""" +app: stata +""" + + +@ctx.action_class("code") +class CodeActions: + def language(): + return "stata" diff --git a/community/apps/stata/stata_do_file_editor.talon b/community/apps/stata/stata_do_file_editor.talon new file mode 100644 index 0000000..0362b7b --- /dev/null +++ b/community/apps/stata/stata_do_file_editor.talon @@ -0,0 +1,25 @@ +# Commands for the Stata Do-File Editor +os: windows +app: stata +win.title: /^Do-file Editor/ +- +do this: key(ctrl-d) + +do line: + edit.select_line() + key(ctrl-d) + +do (all | file): + edit.select_all() + edit.copy() + key(ctrl-d) + +do way up: + edit.extend_file_start() + edit.copy() + key(ctrl-d) + +do way down: + edit.extend_file_end() + edit.copy() + key(ctrl-d) diff --git a/community/apps/sumatrapdf/sumatrapdf.py b/community/apps/sumatrapdf/sumatrapdf.py new file mode 100644 index 0000000..699f207 --- /dev/null +++ b/community/apps/sumatrapdf/sumatrapdf.py @@ -0,0 +1,71 @@ +from talon import Context, Module, actions + +# --- App definition --- +mod = Module() +mod.apps.sumatrapdf = r""" +os: windows +and app.name: SumatraPDF +os: windows +and app.exe: /^sumatrapdf\.exe$/i +""" + +# Context matching +ctx = Context() +ctx.matches = """ +app: sumatrapdf +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class app_actions: + # app.tabs + def tab_open(): + actions.key("ctrl-o") + + +@ctx.action_class("edit") +class EditActions: + def zoom_in(): + actions.key("+") + + def zoom_out(): + actions.key("-") + + +@ctx.action_class("user") +class UserActions: + # user.pages + def page_current(): + actions.key("ctrl-g") + page = actions.edit.selected_text() + actions.key("escape") + return int(page) + + def page_next(): + actions.key("n") + + def page_previous(): + actions.key("p") + + def page_jump(number: int): + actions.key("ctrl-g") + actions.insert(str(number)) + actions.key("enter") + + 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: + actions.key(f"alt-{number}") + + def tab_final(): + actions.key("alt-9") diff --git a/community/apps/sumatrapdf/sumatrapdf.talon b/community/apps/sumatrapdf/sumatrapdf.talon new file mode 100644 index 0000000..127e57c --- /dev/null +++ b/community/apps/sumatrapdf/sumatrapdf.talon @@ -0,0 +1,5 @@ +app: sumatrapdf +- +# Set tags +tag(): user.pages +tag(): user.tabs diff --git a/community/apps/talon/talon_debug_window/talon_debug_window.py b/community/apps/talon/talon_debug_window/talon_debug_window.py new file mode 100644 index 0000000..664bdf0 --- /dev/null +++ b/community/apps/talon/talon_debug_window/talon_debug_window.py @@ -0,0 +1,16 @@ +# this functionality is only available in the talon beta +from talon import Module + +mod = Module() +mod.apps.talon_debug_window = """ +os: mac +and app.bundle: com.talonvoice.Talon +win.title: Talon Debug +""" +mod.apps.talon_debug_window = """ +os: windows +and app.name: Talon +os: windows +and app.exe: talon.exe +win.title: Talon Debug +""" diff --git a/community/apps/talon/talon_debug_window/talon_debug_window.talon b/community/apps/talon/talon_debug_window/talon_debug_window.talon new file mode 100644 index 0000000..0c44a6e --- /dev/null +++ b/community/apps/talon/talon_debug_window/talon_debug_window.talon @@ -0,0 +1,24 @@ +# this functionality is only available in the talon beta +# note: these commands are only useful when the search box is focused +app: talon_debug_window +- +# uncomment user.talon_populate_lists tag to activate talon-specific lists of actions, scopes, modes etcetera. +# Do not enable this tag with dragon, as it will be unusable. +# with conformer, the latency increase may also be unacceptable depending on your cpu +# see https://github.com/talonhub/community/issues/600 +# tag(): user.talon_populate_lists + +tag {user.talon_tags}: "{talon_tags}" + +#commands for dictating key combos +key over: "{keys}" +key over: "{modifiers}" + +action {user.talon_actions}: "{talon_actions}" +# requires user.talon_populate_lists tag. do not use with dragon +list {user.talon_lists}: "{talon_lists}" + +# requires user.talon_populate_lists tag. do not use with dragon +capture {user.talon_captures}: "{talon_captures}" +set {user.talon_settings}: "{talon_settings}" +application {user.talon_apps}: "{talon_apps}" diff --git a/community/apps/talon/talon_repl/talon_repl.py b/community/apps/talon/talon_repl/talon_repl.py new file mode 100644 index 0000000..ca7a968 --- /dev/null +++ b/community/apps/talon/talon_repl/talon_repl.py @@ -0,0 +1,19 @@ +from talon import Context, Module + +mod = Module() +mod.apps.talon_repl = r""" +win.title: /Talon - REPL/ +win.title: /.talon\/bin\/repl/ +""" + +ctx = Context() +ctx.matches = r""" +app: talon_repl +not tag: user.code_language_forced +""" + + +@ctx.action_class("code") +class CodeActions: + def language(): + return "python" diff --git a/community/apps/talon/talon_repl/talon_repl.talon b/community/apps/talon/talon_repl/talon_repl.talon new file mode 100644 index 0000000..4f0fd51 --- /dev/null +++ b/community/apps/talon/talon_repl/talon_repl.talon @@ -0,0 +1,56 @@ +app: talon_repl +- +tag(): user.talon_python +tag(): user.readline + +# uncomment user.talon_populate_lists tag to activate talon-specific lists of actions, scopes, modes etcetera. +# Do not enable this tag with dragon, as it will be unusable. +# with conformer, the latency increase may also be unacceptable depending on your cpu +# see https://github.com/talonhub/community/issues/600 +# tag(): user.talon_populate_lists + +^test last$: + phrase = user.history_get(1) + command = "sim('{phrase}')" + insert(command) + key(enter) +^test $: + insert("sim('{phrase}')") + key(enter) +^test numb $: + phrase = user.history_get(number_small) + command = "sim('{phrase}')" + #to do: shouldn't this work? + #user.paste("sim({phrase})") + insert(command) + key(enter) +# requires user.talon_populate_lists tag. do not use with dragon +^debug action {user.talon_actions}$: + insert("actions.find('{user.talon_actions}')") + key(enter) +# requires user.talon_populate_lists tag. do not use with dragon +^debug list {user.talon_lists}$: + insert("actions.user.talon_pretty_print(registry.lists['{talon_lists}'])") + key(enter) +^debug tags$: + insert("actions.user.talon_pretty_print(registry.tags)") + key(enter) +^debug settings$: + insert("actions.user.talon_pretty_print(registry.settings)") + key(enter) +^debug modes$: + insert("actions.user.talon_pretty_print(scope.get('mode'))") + key(enter) +# requires user.talon_populate_lists tag. do not use with dragon +^debug scope {user.talon_scopes}$: + insert("actions.user.talon_pretty_print(scope.get('{talon_scopes}'))") + key(enter) +^debug running apps$: + insert("actions.user.talon_pretty_print(ui.apps(background=False))") + key(enter) +^debug all windows$: + insert("actions.user.talon_pretty_print(ui.windows())") + key(enter) +^debug {user.running} windows$: + insert("actions.user.talon_debug_app_windows('{running}')") + key(enter) diff --git a/community/apps/taskwarrior/taskwarrior_linux.talon b/community/apps/taskwarrior/taskwarrior_linux.talon new file mode 100644 index 0000000..0a6ca9e --- /dev/null +++ b/community/apps/taskwarrior/taskwarrior_linux.talon @@ -0,0 +1,30 @@ +os: linux +tag: terminal +and tag: user.taskwarrior +- +# general +task version: "task --version\n" +task commands: "task commands\n" +task help: "task help\n" + +# task list +task list: "task list\n" +task list orphans: "task project: list\n" +task list untagged: "task tags.none: list\n" +task list : "task list {text}\n" +task list project: "task list project: " +task list project : "task list project:{text}\n" + +# task add +task add: "task add " +task add : "task add {text}\n" +task undo: "task undo\n" + +(tasks | task next): "task next\n" + +# task editing +task edit$: "task {number} edit" + +# task completion +task done$: "task {number} done" +task delete$: "task {number} delete" diff --git a/community/apps/teams/teams.py b/community/apps/teams/teams.py new file mode 100644 index 0000000..55fcfc2 --- /dev/null +++ b/community/apps/teams/teams.py @@ -0,0 +1,10 @@ +from talon import Module + +mod = Module() +apps = mod.apps +apps.microsoft_teams = """ +os: linux +and app.name: /teams/ +os: linux +and app.name: /Teams/ +""" diff --git a/community/apps/teams/teams.talon b/community/apps/teams/teams.talon new file mode 100644 index 0000000..ea8b7c2 --- /dev/null +++ b/community/apps/teams/teams.talon @@ -0,0 +1,65 @@ +app: microsoft_teams +os: windows +os: linux +- + +# Shortcut reference +# https://support.office.com/en-us/article/keyboard-shortcuts-for-microsoft-teams-2e8e2a70-e8d8-4a19-949b-4c36dd5292d2 + +# generics +show shortcuts: key(ctrl-.) +[go] [to] search: key(ctrl-e) +show commands: key(ctrl-/) +open filter: key(ctrl-shift-f) +go to: key(ctrl-g) +open (apps | applications): key(ctrl-`) +[start] new chat: key(ctrl-n) +open settings: key(ctrl-,) +open help: key(f1) +close: key(escape) + +# navigations +open activity: key(ctrl-1) +open chat: key(ctrl-2) +open teams: key(ctrl-3) +open calendar: key(ctrl-4) +open planner: key(ctrl-5) +open calls: key(ctrl-6) +open files: key(ctrl-7) +[go] [to] (prev | previous) [list] item: key(alt-up) +[go] [to] next [list] item: key(alt-down) +move [selected] team up: key(ctrl-shift-up) +move [selected] team down: key(ctrl-shift-down) +[go] [to] (prev | previous) section: key(ctrl-shift-f6) +[go] [to] next section: key(ctrl-f6) + +# messaging +[go] [to] compose [box]: key(c) +[expand] compose [box]: key(ctrl-shift-x) +send: key(ctrl-enter) +attach file: key(ctrl-o) +[start] new line: key(shift-enter) +reply [to] [thread]: key(r) + +# Meetings, Calls and Calendar +accept video call: key(ctrl-shift-a) +accept audio call: key(ctrl-shift-s) +decline call: key(ctrl-shift-d) +start audio call: key(ctrl-shift-c) +start video call: key(ctrl-shift-u) +toggle mute: key(ctrl-shift-m) +starch screen share session: key(ctrl-shift-e) +toggle video: key(ctrl-shift-o) +[go] [to] sharing toolbar: key(ctrl-shift-space) +decline screen share: key(ctrl-shift-d) +accept screen share: key(ctrl-shift-a) +schedule [a] meeting: key(alt-shift-n) +go to current time: key(alt-.) +go to (prev | previous) (day | week): key(ctrl-alt-left) +go to next (day | week): key(ctrl-alt-right) +view day: key(ctrl-alt-1) +view work week: key(ctrl-alt-2) +view week: key(ctrl-alt-3) +(safe | send) meeting request: key(ctrl-s) +join [from] meeting [details]: key(alt-shift-j) +go to suggested time: key(alt-shift-s) diff --git a/community/apps/teams/teams_linux.py b/community/apps/teams/teams_linux.py new file mode 100644 index 0000000..a2647a5 --- /dev/null +++ b/community/apps/teams/teams_linux.py @@ -0,0 +1,22 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: linux +app: microsoft_teams +""" + + +@ctx.action_class("edit") +class EditActions: + # zoom in: key(ctrl-=) + # zoom out: key(ctrl--) + # reset zoom: key(ctrl-0) + def zoom_in(): + actions.key("ctrl-=") + + def zoom_out(): + actions.key("ctrl--") + + def zoom_reset(): + actions.key("ctrl-0") diff --git a/community/apps/teams/teams_mac.talon b/community/apps/teams/teams_mac.talon new file mode 100644 index 0000000..6b3f4b6 --- /dev/null +++ b/community/apps/teams/teams_mac.talon @@ -0,0 +1,74 @@ +os: mac +app: com.microsoft.teams +- + +open history: key(super-shift-h) +view shortcuts: key(super-.) +show shortcuts: key(super-.) +[go] [to] search: key(super-e) +show commands: key(super-/) +open filter: key(super-shift-f) +go to: key(super-g) +open (apps | applications): key(ctrl-`) +[start] new chat: key(super-n) +open settings: key(super-,) +open help: key(f1) +close: key(escape) +reset: + key(escape) + key(escape) + key(escape) + key(escape) +zoom reset: key(super-0) + +# navigations +open (act | activity): key(super-1) +open chat: key(super-2) +open teams: key(super-3) +open calendar: key(super-4) +open calls: key(super-5) +open files: key(super-6) +[go] [to] (prev | previous) [list] item: key(alt-up) +[go] [to] next [list] item: key(alt-down) +move [selected] team up: key(super-shift-up) +move [selected] team down: key(super-shift-down) +[go] [to] (prev | previous) section: key(super-shift-f6) +[go] [to] next section: key(super-f6) + +# messaging +[go] [to] compose [box]: key(shift-alt-c) +[expand] compose [box]: key(super-shift-x) +send message: key(super-enter) +attach file: key(shift-alt-o) +attach local file: + key(shift-alt-o) + sleep(100ms) + key(down) + key(space) +[start] new line: key(shift-enter) +reply [to] [thread]: key(shift-alt-r) + +# Meetings, Calls and Calendar +accept video call: key(super-shift-a) +accept audio call: key(super-shift-s) +decline call: key(super-shift-d) +start audio call: key(super-shift-c) +start video call: key(super-shift-u) +toggle mute: key(super-shift-m) +starch screen share session: key(super-shift-e) +toggle video: key(super-shift-o) +[go] [to] sharing toolbar: key(super-shift-space) +decline screen share: key(super-shift-d) +accept screen share: key(super-shift-a) +schedule [a] meeting: key(alt-shift-n) +go to current time: key(alt-.) +go to (prev | previous) (day | week): key(super-alt-left) +go to next (day | week): key(super-alt-right) +view day: key(super-alt-1) +view work week: key(super-alt-2) +view week: key(super-alt-3) +(save | send) meeting request: key(super-s) +join [from] meeting [details]: key(alt-shift-j) +go to suggested time: key(alt-shift-s) +(raise | lower) hand: key(super-shift-k) +leave team meeting: key(super-shift-h) diff --git a/community/apps/terminator/terminator_linux.py b/community/apps/terminator/terminator_linux.py new file mode 100644 index 0000000..1b258db --- /dev/null +++ b/community/apps/terminator/terminator_linux.py @@ -0,0 +1,119 @@ +from talon import Context, Module, actions + +# App definition +mod = Module() +mod.apps.terminator = """ +os: linux +and app.exe: terminator +os: linux +and app.name: Terminator +""" + +# Context matching +ctx = Context() +ctx.matches = r""" +app: terminator +""" +ctx.tags = [ + "terminal", + "user.tabs", + "user.splits", + "user.generic_unix_shell", + "user.git", + "user.kubectl", +] + + +# --- Implement actions --- +@ctx.action_class("user") +class user_actions: + # user.splits + def split_window_right(): + actions.key("alt-right") + + def split_window_left(): + actions.key("alt-left") + + def split_window_down(): + actions.key("alt-down") + + def split_window_up(): + actions.key("alt-up") + + def split_window_vertically(): + actions.key("shift-ctrl-e") + + def split_window_horizontally(): + actions.key("shift-ctrl-o") + + def split_flip(): + actions.key("super-r") + + def split_maximize(): + actions.key("shift-ctrl-x") + + def split_reset(): + actions.key("shift-ctrl-x") + + def split_window(): + actions.key("shift-ctrl-o") + + def split_clear(): + actions.key("shift-ctrl-r") + + def split_clear_all(): + actions.key("shift-ctrl-g") + + def split_next(): + actions.key("shift-ctrl-n") + + def split_last(): + actions.key("shift-ctrl-p") + + +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_open(): + actions.key("ctrl-shift-t") + + def tab_previous(): + actions.key("ctrl-pageup") + + def tab_next(): + actions.key("ctrl-pagedown") + + def tab_close(): + actions.key("ctrl-shift-w") + + # global (overwrite linux/app.py) + def window_open(): + actions.key("ctrl-shift-i") + + def window_close(): + actions.key("ctrl-shift-q") + + +# global (overwrite linux/edit.py) +@ctx.action_class("edit") +class EditActions: + def page_down(): + actions.key("shift-pagedown") + + def page_up(): + actions.key("shift-pageup") + + def paste(): + actions.key("ctrl-shift-v") + + def copy(): + actions.key("ctrl-shift-c") + + def find(text: str = None): + actions.key("ctrl-shift-f") + if text: + actions.insert(text) + + def delete_line(): + actions.edit.line_start() + actions.key("ctrl-k") diff --git a/community/apps/termite/termite.talon b/community/apps/termite/termite.talon new file mode 100644 index 0000000..134302d --- /dev/null +++ b/community/apps/termite/termite.talon @@ -0,0 +1,17 @@ +app: termite +and not win.title: /VIM/ +- +#comment or remove tags for command sets you don't want +#termite doesn't support the file_manager stuff yet +#tag(): user.file_manager +tag(): user.kubectl +tag(): user.git +tag(): user.taskwarrior +tag(): terminal + +# Selection mode +shell yank: key("y") +shell select: key("ctrl-shift-space") +shell insert: key("escape") +visual line: key("v") +visual line mode: key("V") diff --git a/community/apps/terraform/terraform.py b/community/apps/terraform/terraform.py new file mode 100644 index 0000000..30b956b --- /dev/null +++ b/community/apps/terraform/terraform.py @@ -0,0 +1,4 @@ +from talon import Module + +mod = Module() +mod.tag("terraform_client", desc="tag for enabling terraform commands in your terminal") diff --git a/community/apps/terraform/terraform.talon b/community/apps/terraform/terraform.talon new file mode 100644 index 0000000..1b63123 --- /dev/null +++ b/community/apps/terraform/terraform.talon @@ -0,0 +1,15 @@ +tag: terminal +and tag: user.terraform_client +- +terraform: "terraform " + +terraform apply: "terraform apply " +terraform destroy: "terraform destroy " +terraform format recursive: "terraform fmt -recursive\n" +terraform format: "terraform fmt\n" +terraform help: "terraform -help" +terraform init upgrade: "terraform init -upgrade\n" +terraform init: "terraform init\n" +terraform plan: "terraform plan\n" +terraform state move: "terraform state mv " +terraform validate: "terraform validate\n" diff --git a/community/apps/thunderbird/thunderbird.py b/community/apps/thunderbird/thunderbird.py new file mode 100644 index 0000000..289b728 --- /dev/null +++ b/community/apps/thunderbird/thunderbird.py @@ -0,0 +1,97 @@ +from talon import Context, Module + +# --- App definitions --- +# Main app TODO: mac context +mod = Module() +mod.apps.thunderbird = r""" +os: windows +and app.name: Thunderbird +os: windows +and app.exe: /^thunderbird\.exe$/i +""" +mod.apps.thunderbird = """ +os: linux +and app.name: Thunderbird +""" + +# Inbox tab TODO: also matches emails opened in new tab +mod.apps.thunderbird_inbox = """ +app: thunderbird +title: /@/ +""" + +# Calendar tab (lightning) +months = [ + "January", # English + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + "Januar", # German + "Februar", + "März", + "Mai", + "Juni", + "Juli", + "Oktober", + "Dezember", +] +mod.apps.thunderbird_calendar = f""" +app: thunderbird +title: Calendar - Mozilla Thunderbird +title: Kalender - Mozilla Thunderbird +title: /({"|".join(map(lambda m: m + " ", months))})/ +""" + +# Tasks tab +mod.apps.thunderbird_tasks = """ +app: thunderbird +title: Tasks - Mozilla Thunderbird +title: Aufgaben - Mozilla Thunderbird +""" + +# Mail composer window +mod.apps.thunderbird_composer = """ +app: thunderbird +title: /Write: / +title: /Verfassen: / +""" + +# Address book popup window +mod.apps.thunderbird_contacts = """ +app: thunderbird +title: Address Book +title: Adressbuch +""" + +# Context matching +ctx = Context() +ctx.matches = r""" +app: thunderbird +""" + + +# --- Define actions --- +@mod.action_class +class UserActions: + def thunderbird_mod(keys: str): + """Press keys with modifier ctrl or cmd""" + + def thunderbird_calendar_view(number: int): + """Select between calendar view tabs""" + + +# --- Implement actions --- +@ctx.action_class("app") +class AppActions: + # app.tabs + # not possible in thunderbird + def tab_open(): + pass diff --git a/community/apps/thunderbird/thunderbird.talon b/community/apps/thunderbird/thunderbird.talon new file mode 100644 index 0000000..4586eff --- /dev/null +++ b/community/apps/thunderbird/thunderbird.talon @@ -0,0 +1,14 @@ +app: thunderbird +and not app: thunderbird_contacts +and not app: thunderbird_composer +- +# Set tags +tag(): user.tabs + +# navigate tabs +go (mails | messages | inbox): user.tab_jump(1) +go (calendar | lightning): user.thunderbird_mod("shift-c") +go tasks: user.thunderbird_mod("shift-d") +# open windows +(open address [book] | address book | open contacts): user.thunderbird_mod("shift-b") +dev tools: user.thunderbird_mod("shift-i") diff --git a/community/apps/thunderbird/thunderbird_calendar.talon b/community/apps/thunderbird/thunderbird_calendar.talon new file mode 100644 index 0000000..0f66aef --- /dev/null +++ b/community/apps/thunderbird/thunderbird_calendar.talon @@ -0,0 +1,13 @@ +app: thunderbird_calendar +- +# event/task +event new: user.thunderbird_mod("i") +task new: user.thunderbird_mod("d") +(task | event) delete: key(delete) +# layout +toggle today: key(f11) +view : user.thunderbird_calendar_view(number_small) +view day: user.thunderbird_calendar_view(1) +view week: user.thunderbird_calendar_view(2) +view multi [week]: user.thunderbird_calendar_view(3) +view month: user.thunderbird_calendar_view(4) diff --git a/community/apps/thunderbird/thunderbird_composer.talon b/community/apps/thunderbird/thunderbird_composer.talon new file mode 100644 index 0000000..3863373 --- /dev/null +++ b/community/apps/thunderbird/thunderbird_composer.talon @@ -0,0 +1,15 @@ +app: thunderbird_composer +- +# mail +(draft | mail | message) save: user.thunderbird_mod("s") +(draft | mail | message) print: user.thunderbird_mod("p") +(draft | mail | message) send: user.thunderbird_mod("enter") +# layout +toggle contacts: key(f9) +# navigation +go (inbox | thunderbird | main): user.thunderbird_mod("1") +# edit +cite paste: user.thunderbird_mod("shift-o") +(unformatted | raw) paste: user.thunderbird_mod("shift-v") +link new: user.thunderbird_mod("k") +link delete: user.thunderbird_mod("shift-k") diff --git a/community/apps/thunderbird/thunderbird_contacts.talon b/community/apps/thunderbird/thunderbird_contacts.talon new file mode 100644 index 0000000..74e2600 --- /dev/null +++ b/community/apps/thunderbird/thunderbird_contacts.talon @@ -0,0 +1,9 @@ +app: thunderbird_contacts +- +contact new: user.thunderbird_mod("n") +contact edit: user.thunderbird_mod("i") +contact delete: key(delete) +contact print: user.thunderbird_mod("p") +contact message: user.thunderbird_mod("m") +contact up: key(up) +contact down: key(down) diff --git a/community/apps/thunderbird/thunderbird_inbox.talon b/community/apps/thunderbird/thunderbird_inbox.talon new file mode 100644 index 0000000..3f5c932 --- /dev/null +++ b/community/apps/thunderbird/thunderbird_inbox.talon @@ -0,0 +1,29 @@ +app: thunderbird_inbox +- +# navigate +(mail | message) open: key(enter) +(mail | message) (up | last): key(b) +(mail | message) (down | next): key(f) +unread [mail | message] (up | last): key(p) +unread [mail | message] (down | next): key(n) +go home: key(alt-home) +toggle (mail | message) [pane]: key(f8) +# mark +(mail | message) (favorite | unfavorite): key(s) +(mail | message) (read | unread): key(m) +(mail | message) (watch | unwatch): key(w) +(mail | message) (ignore | unignore): key(k) +(mail | message) suspend: key(c) +(mail | message) spam: key(j) +# send +(mail | message) new: user.thunderbird_mod("n") +(mail | message) edit: user.thunderbird_mod("e") +(mail | message) reply sender: user.thunderbird_mod("r") +(mail | message) reply all: user.thunderbird_mod("shift-r") +(mail | message) reply list: user.thunderbird_mod("shift-l") +(mail | message) forward: user.thunderbird_mod("l") +# organize +(mail | message) delete: key(delete) +(mail | message) archive: key(a) +(mail | message) save: user.thunderbird_mod("s") +(mail | message) print: user.thunderbird_mod("p") diff --git a/community/apps/thunderbird/thunderbird_linux.py b/community/apps/thunderbird/thunderbird_linux.py new file mode 100644 index 0000000..a892081 --- /dev/null +++ b/community/apps/thunderbird/thunderbird_linux.py @@ -0,0 +1,34 @@ +from talon import Context, actions + +# Context matching +ctx = Context() +ctx.matches = r""" +os: linux +app: thunderbird +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_reopen(): + actions.key("ctrl-shift-t") # only works from inbox tab + + +@ctx.action_class("user") +class UserActions: + # user.tabs + def tab_jump(number: int): + if number <= 9: + actions.key(f"alt-{number}") + + def tab_final(): + actions.key("alt-9") + + # custom actions + def thunderbird_mod(keys: str): + actions.key(f"ctrl-{keys}") + + def thunderbird_calendar_view(number: int): + actions.key(f"ctrl-{number}") diff --git a/community/apps/thunderbird/thunderbird_mac.py b/community/apps/thunderbird/thunderbird_mac.py new file mode 100644 index 0000000..095d28f --- /dev/null +++ b/community/apps/thunderbird/thunderbird_mac.py @@ -0,0 +1,34 @@ +from talon import Context, actions + +# Context matching +ctx = Context() +ctx.matches = r""" +os: mac +app: thunderbird +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_reopen(): + actions.key("cmd-shift-t") # only works from inbox tab + + +@ctx.action_class("user") +class UserActions: + # user.tabs + def tab_jump(number: int): + if number <= 9: + actions.key(f"cmd-{number}") + + def tab_final(): + actions.key("cmd-9") + + # custom actions + def thunderbird_mod(keys: str): + actions.key(f"cmd-{keys}") + + def thunderbird_calendar_view(number: int): + actions.key(f"alt-{number}") diff --git a/community/apps/thunderbird/thunderbird_tasks.talon b/community/apps/thunderbird/thunderbird_tasks.talon new file mode 100644 index 0000000..ff442ab --- /dev/null +++ b/community/apps/thunderbird/thunderbird_tasks.talon @@ -0,0 +1,8 @@ +app: thunderbird_tasks +- +# event/task +event new: user.thunderbird_mod("i") +task new: user.thunderbird_mod("d") +(task | event) delete: key(delete) +# layout +toggle today: key(f11) diff --git a/community/apps/thunderbird/thunderbird_win.py b/community/apps/thunderbird/thunderbird_win.py new file mode 100644 index 0000000..5f4880d --- /dev/null +++ b/community/apps/thunderbird/thunderbird_win.py @@ -0,0 +1,34 @@ +from talon import Context, actions + +# Context matching +ctx = Context() +ctx.matches = r""" +os: windows +app: thunderbird +""" + + +# --- Implement actions --- +@ctx.action_class("app") +class AppActions: + # app.tabs + def tab_reopen(): + actions.key("ctrl-shift-t") # only works from inbox tab + + +@ctx.action_class("user") +class UserActions: + # user.tabs + def tab_jump(number: int): + if number <= 9: + actions.key(f"ctrl-{number}") + + def tab_final(): + actions.key("ctrl-9") + + # custom actions + def thunderbird_mod(keys: str): + actions.key(f"ctrl-{keys}") + + def thunderbird_calendar_view(number: int): + actions.key(f"alt-{number}") diff --git a/community/apps/tmux/tmux.py b/community/apps/tmux/tmux.py new file mode 100644 index 0000000..1f2114a --- /dev/null +++ b/community/apps/tmux/tmux.py @@ -0,0 +1,123 @@ +from talon import Context, Module, actions, settings + +mod = Module() + +mod.apps.tmux = """ +tag: terminal +and tag: user.tmux +""" + +mod.setting( + "tmux_prefix_key", + type=str, + default="ctrl-b", + desc="The key used to prefix all tmux commands", +) + + +@mod.action_class +class TmuxActions: + def tmux_prefix(): + """press control and the configured tmux prefix key""" + actions.key(settings.get("user.tmux_prefix_key")) + + def tmux_keybind(key: str): + """press tmux prefix followed by a key bind""" + actions.user.tmux_prefix() + actions.key(key) + + def tmux_enter_command(command: str = ""): + """Enter tmux command mode and optionally insert a command without executing it.""" + actions.user.tmux_keybind(":") + actions.insert(command) + + def tmux_execute_command(command: str): + """execute tmux command""" + actions.user.tmux_enter_command(command) + actions.key("enter") + actions.sleep("100ms") + + def tmux_execute_command_with_confirmation(command: str, confirmation_prompt: str): + """execute tmux command with confirm-before""" + actions.user.tmux_execute_command( + f'confirm-before -p "{confirmation_prompt} (y/n)" {command}' + ) + actions.key("\n") + + +ctx = Context() +ctx.matches = "app: tmux" + + +@ctx.action_class("app") +class AppActions: + def tab_open(): + actions.user.tmux_execute_command("new-window") + + def tab_next(): + actions.user.tmux_execute_command("select-window -n") + + def tab_previous(): + actions.user.tmux_execute_command("select-window -p") + + +@ctx.action_class("user") +class UserActions: + def tab_jump(number: int): + if number < 10: + actions.user.tmux_keybind(f"{number}") + else: + actions.user.tmux_execute_command(f"select-window -t {number}") + + def tab_close_wrapper(): + actions.user.tmux_execute_command_with_confirmation( + "kill-window", "kill-window #W?" + ) + + def split_window_right(): + actions.user.split_window_horizontally() + actions.user.tmux_execute_command("swap-pane -U -s #P") + + def split_window_left(): + actions.user.split_window_horizontally() + + def split_window_down(): + actions.user.split_window_vertically() + actions.user.tmux_execute_command("swap-pane -U -s #P") + + def split_window_up(): + actions.user.split_window_vertically() + + def split_flip(): + actions.user.tmux_execute_command("next-layout") + + def split_window_vertically(): + actions.user.tmux_execute_command("split-pane") + + def split_window_horizontally(): + actions.user.tmux_execute_command("split-pane -h") + + def split_maximize(): + # toggle the maximization because zooming when already zoomed is pointless + actions.user.tmux_execute_command("resize-pane -Z") + + def split_reset(): + actions.user.tmux_execute_command("resize-pane -Z") + + def split_window(): + actions.user.split_window_horizontally() + + def split_clear(): + actions.user.tmux_execute_command_with_confirmation( + "kill-pane", "kill-pane #P?" + ) + + def split_next(): + # select-pane doesn't seem to support the prefix-o behavior + actions.user.tmux_keybind("o") + + def split_last(): + actions.user.tmux_execute_command("select-pane -l") + + def split_number(index: int): + actions.user.tmux_execute_command(f"select-pane -t {index}") diff --git a/community/apps/tmux/tmux.talon b/community/apps/tmux/tmux.talon new file mode 100644 index 0000000..5424f87 --- /dev/null +++ b/community/apps/tmux/tmux.talon @@ -0,0 +1,20 @@ +app: tmux +- +tag(): user.splits +tag(): user.tabs + +# Note that you will need to add something to match the tmux app in your configuration +# This is not active by default +# Adding a file with a matcher for detecting tmux active in your terminal and activating +# the tmux tag is required +# Something like: +# +# title: /^tmux/ +# - +# tag(): user.tmux + +# pane management - these commands use the word split to match with the splits +# tag defined in tags/splits/splits.talon +go split : user.tmux_keybind(arrow_key) +#Say a number after this command to switch to pane +go split: user.tmux_execute_command("display-panes -d 0") diff --git a/community/apps/tmux/tmux_linux.talon b/community/apps/tmux/tmux_linux.talon new file mode 100644 index 0000000..98cdd0b --- /dev/null +++ b/community/apps/tmux/tmux_linux.talon @@ -0,0 +1,53 @@ +os: linux +tag: user.tmux +- +mux: "tmux " + +#session management +mux new session: insert("tmux new ") +mux sessions: + key(ctrl-b) + key(s) +mux name session: + key(ctrl-b) + key($) +mux kill session: insert("tmux kill-session -t ") +#window management +mux new window: + key(ctrl-b) + key(c) +mux window : + key(ctrl-b) + key('{number}') +mux previous window: + key(ctrl-b) + key(p) +mux next window: + key(ctrl-b) + key(n) +mux rename window: + key(ctrl-b) + key(,) +mux close window: + key(ctrl-b) + key(&) +#pane management +mux split horizontal: + key(ctrl-b) + key(%) +mux split vertical: + key(ctrl-b) + key(") +mux next pane: + key(ctrl-b) + key(o) +mux move : + key(ctrl-b) + key(arrow_key) +mux close pane: + key(ctrl-b) + key(x) +#Say a number right after this command, to switch to pane +mux pane numbers: + key(ctrl-b) + key(q) diff --git a/community/apps/twitter/twitter.talon b/community/apps/twitter/twitter.talon new file mode 100644 index 0000000..4e7feb6 --- /dev/null +++ b/community/apps/twitter/twitter.talon @@ -0,0 +1,37 @@ +tag: browser +browser.host: twitter.com +#win.title: /Twitter/ +- + +# navigation +(show shortcuts | shortcuts help): key(?) +next tweet: key(j) +previous tweet: key(k) +page down: key(space) +load new tweet: key(.) +go home: insert("gh") +go explore: insert("ge") +go notifications: insert("gn") +go mentions: insert("gr") +go profile: insert("gp") +go likes: insert("gl") +go lists: insert("gi") +go direct messages: insert("gm") +go settings: insert("gs") +go book marks: insert("gb") +go to user: insert("gu") +display settings: insert("gd") +# actions +new tweet: key(n) +send tweet: key(ctrl-enter) +new direct message: key(m) +search: key(/) +like message: key(l) +reply message: key(r) +re tweet [message]: key(t) +share tweet: key(s) +bookmark: key(b) +mute account: key(urge) +block account: key(x) +open details: key(enter) +expand photo: key(o) diff --git a/community/apps/visualstudio/visual_studio.py b/community/apps/visualstudio/visual_studio.py new file mode 100644 index 0000000..d587870 --- /dev/null +++ b/community/apps/visualstudio/visual_studio.py @@ -0,0 +1,229 @@ +# vs title tracking requires an extension +# https://marketplace.visualstudio.com/items?itemName=mayerwin.RenameVisualStudioWindowTitle +# https://github.com/mayerwin/vs-customize-window-title (VS 2022 support in releases) +# I currently configure the extension as below +# Document (no solution) open: [documentName] - [ideName] +# No document or solution open: [idleName] +# Solution in break mode: [documentName] - [parentPath]\[solutionName] (Debugging) - [ideName] +# Solution in design mode: [documentName] - [parentPath]\[solutionName] - [ideName] +# Solution in running mode: [documentName] - [parentPath]\[solutionName] (Running) - [ideName] + +from talon import Context, Module, actions + +mod = Module() +ctx = Context() + +mod.apps.visual_studio = r""" +os: windows +and app.name: Microsoft Visual Studio 2022 +os: windows +and app.name: Microsoft Visual Studio 2019 +os: windows +and app.name: devenv.exe +""" + +ctx.matches = r""" +app: visual_studio +""" + + +@ctx.action_class("app") +class AppActions: + def tab_close(): + actions.key("ctrl-f4") + + def tab_next(): + actions.key("ctrl-tab") + + def tab_previous(): + actions.key("ctrl-shift-tab") + + def tab_reopen(): + actions.key("ctrl-1 ctrl-r enter") + + +@ctx.action_class("code") +class CodeActions: + def toggle_comment(): + actions.key("ctrl-k ctrl-/") + + +@ctx.action_class("edit") +class EditActions: + def indent_more(): + actions.key("tab") + + def indent_less(): + actions.key("shift-tab") + + def save_all(): + actions.key("ctrl-shift-s") + + def find_next(): + actions.key("enter") + + def find_previous(): + actions.key("shift-enter") + + def line_swap_up(): + actions.key("alt-up") + + def line_swap_down(): + actions.key("alt-down") + + def line_clone(): + actions.key("ctrl-d") + + def jump_line(n: int): + actions.key("ctrl-g") + actions.sleep("100ms") + actions.insert(str(n)) + actions.key("enter") + + +@ctx.action_class("win") +class WinActions: + def filename(): + title = actions.win.title() + result = title.split("-")[0].rstrip() + if "." in result: + return result + return "" + + +@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) + + # def select_next_occurrence(verbs: str, text: str): + # actions.edit.find(text) + # actions.sleep("100ms") + + # actions.key("esc") + # if verbs is not None: + # actions.user.perform_selection_action(verbs) + + # def select_previous_occurrence(verbs: str, text: str): + # actions.edit.find(text) + # actions.key("shift-enter") + # actions.sleep("100ms") + # actions.key("esc") + # if verbs is not None: + # actions.user.perform_selection_action(verbs) + + # def go_to_line(verb: str, line: int): + # actions.key("ctrl-g") + # actions.insert(str(line)) + # actions.key("enter") + + # if verb is not None: + # actions.user.perform_movement_action(verb) + + # def tab_jump(number: int): + # if number < 10: + # if is_mac: + # actions.key("ctrl-{}".format(number)) + # else: + # actions.key("alt-{}".format(number)) + + # def tab_final(): + # if is_mac: + # actions.key("ctrl-0") + # 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("cmd-{}".format(index)) + # else: + # actions.key("ctrl-{}".format(index)) + + # splits.py support end + + # find_and_replace.py support begin + + def find_everywhere(text: str): + """Triggers find across project""" + actions.key("ctrl-shift-f") + + if text: + actions.insert(text) + + def find_toggle_match_by_case(): + """Toggles find match by case sensitivity""" + actions.key("alt-c") + + def find_toggle_match_by_word(): + """Toggles find match by whole words""" + actions.key("alt-w") + + def find_toggle_match_by_regex(): + """Toggles find match by regex""" + actions.key("alt-r") + + def replace(text: str): + """Search and replaces in the active editor""" + actions.key("ctrl-h") + + if text: + actions.insert(text) + + def replace_everywhere(text: str): + """Search and replaces in the entire project""" + actions.key("ctrl-shift-h") + + if text: + actions.insert(text) + + def replace_confirm(): + """Confirm replace at current position""" + actions.key("alt-r") + + def replace_confirm_all(): + """Confirm replace all""" + actions.key("alt-a") + + def select_previous_occurrence(text: str): + actions.edit.find(text) + actions.key("shift-enter") + actions.sleep("100ms") + actions.key("esc") + + def select_next_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("esc") + + # find_and_replace.py support end + + # multiple_cursor.py support begin + # note: visual studio has no explicit mode for multiple cursors; requires https://marketplace.visualstudio.com/items?itemName=VaclavNadrasky.MultiCaretBooster + def multi_cursor_add_above(): + actions.key("shift-alt-up") + + def multi_cursor_add_below(): + actions.key("shift-alt-down") + + # action(user.multi_cursor_add_to_line_ends): does not exist :( + def multi_cursor_disable(): + actions.key("escape") + + def multi_cursor_enable(): + actions.skip() + + def multi_cursor_select_all_occurrences(): + actions.key("shift-alt-;") + + def multi_cursor_select_fewer_occurrences(): + actions.key("shift-alt-k") + + def multi_cursor_select_more_occurrences(): + actions.key("shift-alt->") diff --git a/community/apps/visualstudio/visual_studio.talon b/community/apps/visualstudio/visual_studio.talon new file mode 100644 index 0000000..b37007f --- /dev/null +++ b/community/apps/visualstudio/visual_studio.talon @@ -0,0 +1,93 @@ +app: visual_studio +- +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) +panel properties: key(f4) +panel output: key(ctrl-alt-o) +panel class: key(ctrl-shift-c) +panel errors: key(ctrl-\ ctrl-e) +panel design: key(shift-f7) +panel marks: key(ctrl-k ctrl-w) +panel breakpoints: key(ctrl-alt-b) + +# Settings +show settings: key(alt-t o) +#show shortcuts: +#show snippets: + +# Display +fullscreen switch: key(shift-alt-enter) +wrap switch: key(ctrl-e ctrl-w) + +# File Commands +file hunt []: + key(ctrl-shift-t) + insert(text or "") +file create: key(ctrl-n) +#file open folder: +file rename: key(ctrl-[ s f2) +file reveal: key(ctrl-[ s) + +# Language Features +hint show: key(ctrl-shift-space) +definition show: key(f12) +definition peek: key(alt-f12) +references find: key(shift-f12) +format that: key(ctrl-k ctrl-d) +format selection: key(ctrl-k ctrl-f) +imports fix: key(ctrl-r ctrl-g) + +# problem next: +# problem last: +# problem fix: +refactor field: key(ctrl-r ctrl-e) +refactor interface: key(ctrl-r ctrl-i) +refactor method: key(ctrl-r ctrl-m) +refactor reorder parameters: key(ctrl-r ctrl-o) +refactor remove parameters: key(ctrl-r ctrl-v) +refactor that: key(ctrl-r ctrl-r) + +#code navigation +(go declaration | follow): key(ctrl-f12) +go back: key(ctrl--) +go forward: key(ctrl-shift--) +go implementation: key(f12) +go recent []: + key(ctrl-1 ctrl-r) + sleep(100ms) + insert(text or "") +go type []: + key(ctrl-1 ctrl-t) + sleep(100ms) + insert(text or "") +go member []: + key(alt-\) + sleep(100ms) + insert(text or "") +go usage: key(shift-f12) + +# Bookmarks. +go marks: key(ctrl-k ctrl-w) +toggle mark: key(ctrl-k ctrl-k) +go next mark: key(ctrl-k ctrl-n) +go last mark: key(ctrl-k ctrl-p) + +# Folding +fold toggle: key(ctrl-m ctrl-m) +fold toggle all: key(ctrl-m ctrl-l) +fold definitions: key(ctrl-m ctrl-o) + +#Debugging +break point: key(f9) +step over: key(f10) +debug step into: key(f11) +debug step out [of]: key(f10) +debug start: key(f5) +debug stopper: key(shift-f5) +debug continue: key(f5) diff --git a/community/apps/visualstudio/visual_studio_win.py b/community/apps/visualstudio/visual_studio_win.py new file mode 100644 index 0000000..c89a17c --- /dev/null +++ b/community/apps/visualstudio/visual_studio_win.py @@ -0,0 +1,68 @@ +from talon import Context, actions + +ctx = Context() + +ctx.matches = r""" +os: windows +app: visual_studio +""" + + +@ctx.action_class("app") +class AppActions: + def tab_close(): + actions.key("ctrl-f4") + + def tab_next(): + actions.key("ctrl-tab") + + def tab_previous(): + actions.key("ctrl-shift-tab") + + def tab_reopen(): + actions.key("ctrl-1 ctrl-r enter") + + +@ctx.action_class("code") +class CodeActions: + def toggle_comment(): + actions.key("ctrl-k ctrl-/") + + +@ctx.action_class("edit") +class EditActions: + def indent_more(): + actions.key("tab") + + def indent_less(): + actions.key("shift-tab") + + def save_all(): + actions.key("ctrl-shift-s") + + +@ctx.action_class("user") +class UserActions: + # multiple_cursor.py support begin + # note: visual studio has no explicit mode for multiple cursors; requires https://marketplace.visualstudio.com/items?itemName=VaclavNadrasky.MultiCaretBooster + def multi_cursor_add_above(): + actions.key("shift-alt-up") + + def multi_cursor_add_below(): + actions.key("shift-alt-down") + + # action(user.multi_cursor_add_to_line_ends): does not exist :( + def multi_cursor_disable(): + actions.key("escape") + + def multi_cursor_enable(): + actions.skip() + + def multi_cursor_select_all_occurrences(): + actions.key("shift-alt-;") + + def multi_cursor_select_fewer_occurrences(): + actions.key("shift-alt-k") + + def multi_cursor_select_more_occurrences(): + actions.key("shift-alt->") diff --git a/community/apps/vivaldi/vivaldi.py b/community/apps/vivaldi/vivaldi.py new file mode 100644 index 0000000..61b9400 --- /dev/null +++ b/community/apps/vivaldi/vivaldi.py @@ -0,0 +1,85 @@ +from talon import Context, Module, actions, app + +ctx = Context() +mod = Module() + +mod.apps.vivaldi = "app.name: Vivaldi" +mod.apps.vivaldi = "app.name: Vivaldi-stable" +mod.apps.vivaldi = r""" +os: windows +and app.exe: /^vivaldi\.exe$/i +os: linux +and app.exe: vivaldi-bin +os: mac +and app.bundle: com.vivaldi.Vivaldi +""" +ctx.matches = r""" +app: vivaldi +""" + + +@mod.action_class +class Actions: + def vivaldi_history_panel(): + """Toggles the Vivaldi history panel""" + actions.key("ctrl-shift-h") + + def vivaldi_bookmarks_panel(): + """Toggles the Vivaldi bookmarks panel""" + actions.user.command_search("Bookmarks Panel") + actions.key("enter") + + def vivaldi_downloads_panel(): + """Toggles the Vivaldi downloads panel""" + actions.key("ctrl-shift-d") + + def vivaldi_notes_panel(): + """Toggles the Vivaldi notes panel""" + actions.key("ctrl-shift-o") + + +@ctx.action_class("user") +class UserActions: + def tab_close_wrapper(): + actions.sleep("180ms") + actions.app.tab_close() + + def tab_jump(number: int): + actions.key(f"ctrl-{number}") + + def command_search(command: str = ""): + actions.key("ctrl-e") + if command != "": + actions.sleep("180ms") + actions.insert(command) + + +@ctx.action_class("browser") +class BrowserActions: + def show_extensions(): + actions.key("ctrl-shift-e") + + def focus_address(): + actions.key("ctrl-l") + + def focus_page(): + actions.key("f9") + + def bookmarks(): + actions.key("ctrl-b") + + def bookmark_tabs(): + raise NotImplementedError("Vivaldi doesn't support this functionality") + + def show_downloads(): + # There is no default shortcut for showing the downloads page. You can + # configure one. + actions.app.tab_open() + actions.sleep("180ms") + actions.browser.go("vivaldi://downloads") + + def go(url: str): + actions.browser.focus_address() + actions.sleep("150ms") + actions.insert(url) + actions.key("enter") diff --git a/community/apps/vivaldi/vivaldi.talon b/community/apps/vivaldi/vivaldi.talon new file mode 100644 index 0000000..aad2916 --- /dev/null +++ b/community/apps/vivaldi/vivaldi.talon @@ -0,0 +1,10 @@ +app: vivaldi +- +tag(): browser +tag(): user.tabs +tag(): user.command_search + +(sidebar | panel) history: user.vivaldi_history_panel() +(sidebar | panel) downloads: user.vivaldi_downloads_panel() +(sidebar | panel) bookmarks: user.vivaldi_bookmarks_panel() +(sidebar | panel) notes: user.vivaldi_notes_panel() diff --git a/community/apps/vivaldi/vivaldi_mac.py b/community/apps/vivaldi/vivaldi_mac.py new file mode 100644 index 0000000..0f0791a --- /dev/null +++ b/community/apps/vivaldi/vivaldi_mac.py @@ -0,0 +1,49 @@ +from talon import Context, actions, app + +ctx = Context() + +ctx.matches = r""" +os: mac +app: vivaldi +""" + + +@ctx.action_class("user") +class UserActions: + def vivaldi_history_panel(): + actions.key("cmd-alt-y") + + def vivaldi_downloads_panel(): + actions.key("cmd-alt-l") + + def vivaldi_notes_panel(): + # This shortcut didn't work for me. You might need to change it to a + # different one. + actions.key("cmd-alt-n") + + def vivaldi_toggle_quick_commands(): + actions.key("cmd-e") + + def tab_jump(number: int): + actions.key(f"cmd-{number}") + + +@ctx.action_class("app") +class AppActions: + def tab_next(): + actions.key("cmd-shift-]") + + def tab_previous(): + actions.key("cmd-shift-[") + + +@ctx.action_class("browser") +class BrowserActions: + def show_extensions(): + actions.key("ctrl-cmd-e") + + def bookmarks(): + actions.key("cmd-ctrl-b") + + def focus_address(): + actions.key("cmd-l") diff --git a/community/apps/vscode/README.md b/community/apps/vscode/README.md new file mode 100644 index 0000000..ffdb668 --- /dev/null +++ b/community/apps/vscode/README.md @@ -0,0 +1,70 @@ +# VS Code support + +Installing several Talon community-developed VS Code extensions will improve your experience. + +The [VS Code Talon extension pack](https://marketplace.visualstudio.com/items?itemName=pokey.talon) enables a couple advanced commands and improves the speed/robustness of Talon issuing VS Code commands. + +The [Andreas Talon](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon) extension (dependent on the command server in the extension pack) adds additional commands and useful features for editing your Talon configuration in VS Code. + +## Cursorless + +If you'd like to use Cursorless, [follow the instructions on the Cursorless site](https://www.cursorless.org/docs/user/installation/). + +## Terminal + +By default, Talon cannot recognize that you have the VS Code integrated terminal focused, so the `terminal` tag is never active in VS Code. Your goal is for the VS Code window title to reflect whether a terminal is focused, then to match the title in your Talon configuration. You can do so in two ways. + +Note that the full window title may not be displayed at the top of VS Code windows. To be sure you are seeing the whole title, say _help scope_ and watch Misc > `win.title` in the scope window that appears. + +### Option 1: Add focused view to window title + +Change the [`window.title`](vscode://settings/window.title) setting to: + +``` +${activeEditorShort}${separator}${rootName}${separator}${profileName}${separator}focus:[${focusedView}] +``` + +This causes VS Code to include `focus:[Terminal]` in the window title whenever the terminal is focused (e.g. by saying _panel terminal_). [Community's VS Code support looks for this string in the window title](vscode_terminal.talon#L5) and activates the terminal tag. + +If you have existing customizations to your window title you want to keep, ensure that `focus:[${focusedView}]` appears somewhere within your custom `window.title`. + +To enable terminal commands, create a file in your Talon user directory that matches the terminal tag in VS Code, and activates any tags for commands you have installed/want to use, for example: + +```talon +app: vscode +tag: terminal +- +tag(): user.generic_unix_shell +tag(): user.git +tag(): user.kubectl +tag(): user.readline +``` + +### Option 2: Open VS Code integrated terminals as editors + +This option lets you enable different voice commands based on _what_ is running in the terminal — for example, if you use both PowerShell and WSL in VS Code integrated terminals. + +Change the [`terminal.integrated.defaultLocation`](vscode://settings/terminal.integrated.defaultLocation) setting to `editor`. Then, create a terminal with the voice command _terminal new_. + +In an otherwise-default VS Code setup, the first part of the window title as displayed in _help scope_ is the currently-running process, e.g. `zsh` or `powershell`; this is also displayed in the tab title. You can customize the terminal tab title/part of the window title with the [`terminal.integrated.tabs.title`](vscode://settings/terminal.integrated.tabs.title) setting. + +To enable terminal commands, create one or more files in your Talon user directory that match the first portion of the window title, and activates **both** the `terminal` tag and any tags for commands you have installed/want to use. For example: + +```talon +app: vscode +win.title: /^zsh / +- +tag(): terminal +tag(): user.generic_unix_shell +tag(): user.git +tag(): user.readline +``` + +```talon +app: vscode +win.title: /^powershell / +- +tag(): terminal +tag(): user.generic_windows_shell +tag(): user.git +``` diff --git a/community/apps/vscode/vscode.py b/community/apps/vscode/vscode.py new file mode 100644 index 0000000..6cd4bad --- /dev/null +++ b/community/apps/vscode/vscode.py @@ -0,0 +1,438 @@ +from talon import Context, Module, actions, app + +is_mac = app.platform == "mac" + +ctx = Context() +ctx_editor = 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 +os: mac +and app.bundle: com.microsoft.VSCodeInsiders +os: mac +and app.bundle: com.vscodium +os: mac +and app.bundle: co.posit.positron +os: mac +and app.bundle: com.visualstudio.code.oss +os: mac +and app.bundle: com.todesktop.230313mzl4w4u92 +os: mac +and app.bundle: com.exafunction.windsurf +""" +mod.apps.vscode = """ +os: linux +and app.name: Code +os: linux +and app.name: code-oss +os: linux +and app.name: code-insiders +os: linux +and app.name: VSCodium +os: linux +and app.name: Codium +os: linux +and app.name: Cursor +os: linux +and app.name: Positron +""" +mod.apps.vscode = r""" +os: windows +and app.name: Visual Studio Code +os: windows +and app.name: Visual Studio Code Insiders +os: windows +and app.name: Visual Studio Code - Insiders +os: windows +and app.exe: /^code\.exe$/i +os: windows +and app.exe: /^code-insiders\.exe$/i +os: windows +and app.name: VSCodium +os: windows +and app.exe: /^vscodium\.exe$/i +os: windows +and app.name: Azure Data Studio +os: windows +and app.exe: /^azuredatastudio\.exe$/i +os: windows +and app.exe: positron.exe +os: windows +and app.exe: /^cursor\.exe$/i +os: windows +and app.exe: /^positron\.exe$/i +""" + +ctx.matches = r""" +app: vscode +""" +ctx_editor.matches = r""" +app: vscode +and win.title: /focus:\[Text Editor\]/ +""" +mac_ctx.matches = r""" +os: mac +app: vscode +""" + + +@ctx.action_class("app") +class AppActions: + # talon app actions + def tab_open(): + actions.user.vscode("workbench.action.files.newUntitledFile") + + def tab_close(): + actions.user.vscode("workbench.action.closeActiveEditor") + + def tab_next(): + actions.user.vscode("workbench.action.nextEditorInGroup") + + def tab_previous(): + actions.user.vscode("workbench.action.previousEditorInGroup") + + def tab_reopen(): + actions.user.vscode("workbench.action.reopenClosedEditor") + + def window_close(): + actions.user.vscode("workbench.action.closeWindow") + + def window_open(): + actions.user.vscode("workbench.action.newWindow") + + +@ctx.action_class("code") +class CodeActions: + # talon code actions + def toggle_comment(): + actions.user.vscode("editor.action.commentLine") + + +# In the editor, use RPC commands to avoid conflicting with the editor's keybindings. +# Only do this for editor, so that e.g. modal windows can still be pasted into with +# ctrl-v. +@ctx_editor.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") + + def find(text: str = None): + if text: + actions.user.run_rpc_command( + "editor.actions.findWithArgs", {"searchString": text} + ) + else: + actions.user.vscode("actions.find") + + +@ctx.action_class("edit") +class EditActions: + # talon edit actions + def indent_more(): + actions.user.vscode("editor.action.indentLines") + + def indent_less(): + actions.user.vscode("editor.action.outdentLines") + + 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") + + def find_previous(): + actions.user.vscode("editor.action.previousMatchFindAction") + + def line_swap_up(): + actions.key("alt-up") + + def line_swap_down(): + actions.key("alt-down") + + def line_clone(): + actions.key("shift-alt-down") + + def line_insert_down(): + actions.user.vscode("editor.action.insertLineAfter") + + def line_insert_up(): + actions.user.vscode("editor.action.insertLineBefore") + + def jump_line(n: int): + actions.user.vscode("workbench.action.gotoLine") + actions.insert(str(n)) + actions.key("enter") + actions.edit.line_start() + + def zoom_reset(): + actions.user.vscode("workbench.action.zoomReset") + + +@ctx.action_class("win") +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 + + if is_mac: + result = title.split(" — ")[0] + else: + result = title.split(" - ")[0] + + if "." in result: + return result + + return "" + + +@mod.action_class +class Actions: + def vscode_terminal(number: int): + """Activate a terminal by number""" + actions.user.vscode(f"workbench.action.terminal.focusAtIndex{number}") + + def command_palette(): + """Show command palette""" + actions.key("ctrl-shift-p") + + +@mac_ctx.action_class("edit") +class MacEditActions: + def find(text: str = None): + actions.key("cmd-f") + if text: + actions.insert(text) + + +@mac_ctx.action_class("user") +class MacUserActions: + def command_palette(): + actions.key("cmd-shift-p") + + +@ctx.action_class("user") +class UserActions: + # splits.py support begin + def split_clear_all(): + actions.user.vscode("workbench.action.editorLayoutSingle") + + def split_clear(): + actions.user.vscode("workbench.action.joinTwoGroups") + + def split_flip(): + actions.user.vscode("workbench.action.toggleEditorGroupLayout") + + def split_maximize(): + actions.user.vscode("workbench.action.toggleMaximizeEditorGroup") + + def split_reset(): + actions.user.vscode("workbench.action.evenEditorWidths") + + def split_last(): + actions.user.vscode("workbench.action.focusLeftGroup") + + def split_next(): + actions.user.vscode("workbench.action.focusRightGroup") + + def split_window_down(): + actions.user.vscode("workbench.action.moveEditorToBelowGroup") + + def split_window_horizontally(): + actions.user.vscode("workbench.action.splitEditorOrthogonal") + + def split_window_left(): + actions.user.vscode("workbench.action.moveEditorToLeftGroup") + + def split_window_right(): + actions.user.vscode("workbench.action.moveEditorToRightGroup") + + def split_window_up(): + actions.user.vscode("workbench.action.moveEditorToAboveGroup") + + def split_window_vertically(): + actions.user.vscode("workbench.action.splitEditor") + + 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 + # note: vscode has no explicit mode for multiple cursors + def multi_cursor_add_above(): + actions.user.vscode("editor.action.insertCursorAbove") + + def multi_cursor_add_below(): + actions.user.vscode("editor.action.insertCursorBelow") + + def multi_cursor_add_to_line_ends(): + actions.user.vscode("editor.action.insertCursorAtEndOfEachLineSelected") + + def multi_cursor_disable(): + actions.key("escape") + + def multi_cursor_enable(): + actions.skip() + + def multi_cursor_select_all_occurrences(): + actions.user.vscode("editor.action.selectHighlights") + + def multi_cursor_select_fewer_occurrences(): + actions.user.vscode("cursorUndo") + + def multi_cursor_select_more_occurrences(): + actions.user.vscode("editor.action.addSelectionToNextFindMatch") + + 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: + actions.user.vscode_with_plugin( + f"workbench.action.openEditorAtIndex{number}" + ) + else: + actions.key(f"alt-{number}") + else: + actions.user.vscode_with_plugin( + "workbench.action.openEditorAtIndex", number + ) + + def tab_final(): + if is_mac: + actions.user.vscode("workbench.action.lastEditorInGroup") + else: + actions.key("alt-0") + + 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() + + # tabs.py support end + + # find_and_replace.py support begin + + def find_everywhere(text: str): + """Triggers find across project""" + if is_mac: + actions.key("cmd-shift-f") + else: + actions.key("ctrl-shift-f") + + if text: + actions.insert(text) + + def find_toggle_match_by_case(): + """Toggles find match by case sensitivity""" + if is_mac: + actions.key("alt-cmd-c") + else: + actions.key("alt-c") + + def find_toggle_match_by_word(): + """Toggles find match by whole words""" + if is_mac: + actions.key("cmd-alt-w") + else: + actions.key("alt-w") + + def find_toggle_match_by_regex(): + """Toggles find match by regex""" + if is_mac: + actions.key("cmd-alt-r") + else: + actions.key("alt-r") + + def replace(text: str): + """Search and replaces in the active editor""" + if is_mac: + actions.key("alt-cmd-f") + else: + actions.key("ctrl-h") + + if text: + actions.insert(text) + + def replace_everywhere(text: str): + """Search and replaces in the entire project""" + if is_mac: + actions.key("cmd-shift-h") + else: + actions.key("ctrl-shift-h") + + if text: + actions.insert(text) + + def replace_confirm(): + """Confirm replace at current position""" + if is_mac: + actions.key("shift-cmd-1") + else: + actions.key("ctrl-shift-1") + + def replace_confirm_all(): + """Confirm replace all""" + if is_mac: + actions.key("cmd-enter") + else: + actions.key("ctrl-alt-enter") + + def select_previous_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("shift-enter esc") + + def select_next_occurrence(text: str): + actions.edit.find(text) + actions.sleep("100ms") + actions.key("esc") + + def insert_snippet(body: str): + actions.user.run_rpc_command("editor.action.insertSnippet", {"snippet": body}) + + def move_cursor_to_next_snippet_stop(): + actions.user.vscode("jumpToNextSnippetPlaceholder") diff --git a/community/apps/vscode/vscode.talon b/community/apps/vscode/vscode.talon new file mode 100644 index 0000000..09215e0 --- /dev/null +++ b/community/apps/vscode/vscode.talon @@ -0,0 +1,289 @@ +#custom vscode commands go here +app: vscode +- +tag(): user.find_and_replace +tag(): user.line_commands +tag(): user.multiple_cursors +tag(): user.splits +tag(): user.tabs +tag(): user.command_search + +window reload: user.vscode("workbench.action.reloadWindow") +window close: user.vscode("workbench.action.closeWindow") +#multiple_cursor.py support end + +go view []: + user.vscode("workbench.action.openView") + insert(user.text or "") + +# Sidebar +bar explore: user.vscode("workbench.view.explorer") +bar extensions: user.vscode("workbench.view.extensions") +bar outline: user.vscode("outline.focus") +bar run: user.vscode("workbench.view.debug") +bar search: user.vscode("workbench.view.search") +bar source: user.vscode("workbench.view.scm") +bar test: user.vscode("workbench.view.testing.focus") +bar switch: user.vscode("workbench.action.toggleSidebarVisibility") + +# Symbol search +symbol hunt []: + user.vscode("workbench.action.gotoSymbol") + sleep(50ms) + insert(text or "") + +symbol hunt all []: + user.vscode("workbench.action.showAllSymbols") + sleep(50ms) + insert(text or "") + +# Panels +panel control: user.vscode("workbench.panel.repl.view.focus") +panel output: user.vscode("workbench.panel.output.focus") +panel problems: user.vscode("workbench.panel.markers.view.focus") +panel switch: user.vscode("workbench.action.togglePanel") +panel terminal: user.vscode("workbench.action.terminal.focus") +focus editor: user.vscode("workbench.action.focusActiveEditorGroup") + +# Settings +show settings: user.vscode("workbench.action.openGlobalSettings") +show settings json: user.vscode("workbench.action.openSettingsJson") +show settings folder: user.vscode("workbench.action.openFolderSettings") +show settings folder json: user.vscode("workbench.action.openFolderSettingsFile") +show settings workspace: user.vscode("workbench.action.openWorkspaceSettings") +show settings workspace json: user.vscode("workbench.action.openWorkspaceSettingsFile") +show shortcuts: user.vscode("workbench.action.openGlobalKeybindings") +show shortcuts json: user.vscode("workbench.action.openGlobalKeybindingsFile") +show snippets: user.vscode("workbench.action.openSnippets") + +# VSCode Snippets +snip (last | previous): user.vscode("jumpToPrevSnippetPlaceholder") + +# Display +centered switch: user.vscode("workbench.action.toggleCenteredLayout") +fullscreen switch: user.vscode("workbench.action.toggleFullScreen") +theme switch: user.vscode("workbench.action.selectTheme") +wrap switch: user.vscode("editor.action.toggleWordWrap") +zen switch: user.vscode("workbench.action.toggleZenMode") + +# File Commands +file hunt []: + user.vscode("workbench.action.quickOpen") + sleep(50ms) + insert(text or "") +file hunt (pace | paste): + user.vscode("workbench.action.quickOpen") + sleep(50ms) + edit.paste() +file copy name: user.vscode("fileutils.copyFileName") +file copy path: user.vscode("copyFilePath") +file copy local [path]: user.vscode("copyRelativeFilePath") +file create sibling: user.vscode_and_wait("explorer.newFile") +file create: user.vscode("workbench.action.files.newUntitledFile") +file create relative: user.vscode("fileutils.newFile") +file create root: user.vscode("fileutils.newFileAtRoot") +file rename: + user.vscode("fileutils.renameFile") + sleep(150ms) +file move: + user.vscode("fileutils.moveFile") + sleep(150ms) +file clone: + user.vscode("fileutils.duplicateFile") + sleep(150ms) +file delete: + user.vscode("fileutils.removeFile") + sleep(150ms) +file open folder: user.vscode("revealFileInOS") +file reveal: user.vscode("workbench.files.action.showActiveFileInExplorer") +save ugly: user.vscode("workbench.action.files.saveWithoutFormatting") + +# Language Features +suggest show: user.vscode("editor.action.triggerSuggest") +hint show: user.vscode("editor.action.triggerParameterHints") +definition show: user.vscode("editor.action.revealDefinition") +definition peek: user.vscode("editor.action.peekDefinition") +definition side: user.vscode("editor.action.revealDefinitionAside") +references show: user.vscode("editor.action.goToReferences") +hierarchy peek: user.vscode("editor.showCallHierarchy") +references find: user.vscode("references-view.find") +format that: user.vscode("editor.action.formatDocument") +format selection: user.vscode("editor.action.formatSelection") +imports fix: user.vscode("editor.action.organizeImports") +problem next: user.vscode("editor.action.marker.nextInFiles") +problem last: user.vscode("editor.action.marker.prevInFiles") +problem fix: user.vscode("problems.action.showQuickFixes") +rename that: user.vscode("editor.action.rename") +refactor that: user.vscode("editor.action.refactor") +whitespace trim: user.vscode("editor.action.trimTrailingWhitespace") +language switch: user.vscode("workbench.action.editor.changeLanguageMode") +refactor rename: user.vscode("editor.action.rename") +refactor this: user.vscode("editor.action.refactor") + +#code navigation +(go declaration | follow): user.vscode("editor.action.revealDefinition") +go back: user.vscode("workbench.action.navigateBack") +go forward: user.vscode("workbench.action.navigateForward") +go implementation: user.vscode("editor.action.goToImplementation") +go type: user.vscode("editor.action.goToTypeDefinition") +go usage: user.vscode("references-view.find") +go recent []: + user.vscode("workbench.action.openRecent") + sleep(50ms) + insert(text or "") + sleep(250ms) +go edit: user.vscode("workbench.action.navigateToLastEditLocation") + +# Bookmarks. Requires Bookmarks plugin +bar marks: user.vscode("workbench.view.extension.bookmarks") +go marks: + user.deprecate_command("2023-06-06", "go marks", "bar marks") + user.vscode("workbench.view.extension.bookmarks") +toggle mark: user.vscode("bookmarks.toggle") +go next mark: user.vscode("bookmarks.jumpToNext") +go last mark: user.vscode("bookmarks.jumpToPrevious") + +close other tabs: user.vscode("workbench.action.closeOtherEditors") +close all tabs: user.vscode("workbench.action.closeAllEditors") +close tabs way right: user.vscode("workbench.action.closeEditorsToTheRight") +close tabs way left: user.vscode("workbench.action.closeEditorsToTheLeft") + +# Folding +fold that: user.vscode("editor.fold") +unfold that: user.vscode("editor.unfold") +fold those: user.vscode("editor.foldAllMarkerRegions") +unfold those: user.vscode("editor.unfoldRecursively") +fold all: user.vscode("editor.foldAll") +unfold all: user.vscode("editor.unfoldAll") +fold comments: user.vscode("editor.foldAllBlockComments") +fold one: user.vscode("editor.foldLevel1") +fold two: user.vscode("editor.foldLevel2") +fold three: user.vscode("editor.foldLevel3") +fold four: user.vscode("editor.foldLevel4") +fold five: user.vscode("editor.foldLevel5") +fold six: user.vscode("editor.foldLevel6") +fold seven: user.vscode("editor.foldLevel7") + +# Git / Github (not using verb-noun-adjective pattern, mirroring terminal commands.) +git branch: user.vscode("git.branchFrom") +git branch this: user.vscode("git.branch") +git checkout []: + user.vscode("git.checkout") + sleep(50ms) + insert(text or "") +git commit []: + user.vscode("git.commitStaged") + sleep(100ms) + user.insert_formatted(text or "", "CAPITALIZE_FIRST_WORD") +git commit undo: user.vscode("git.undoCommit") +git commit amend: user.vscode("git.commitStagedAmend") +git diff: user.vscode("git.openChange") +git fetch: user.vscode("git.fetch") +git fetch all: user.vscode("git.fetchAll") +git ignore: user.vscode("git.ignore") +git merge: user.vscode("git.merge") +git output: user.vscode("git.showOutput") +git pull: user.vscode("git.pullRebase") +git push: user.vscode("git.push") +git push force: user.vscode("git.pushForce") +git rebase abort: user.vscode("git.rebaseAbort") +git reveal: user.vscode("git.revealInExplorer") +git revert: user.vscode("git.revertChange") +git stash: user.vscode("git.stash") +git stash pop: user.vscode("git.stashPop") +git status: user.vscode("workbench.scm.focus") +git stage: user.vscode("git.stage") +git stage all: user.vscode("git.stageAll") +git sync: user.vscode("git.sync") +git unstage: user.vscode("git.unstage") +git unstage all: user.vscode("git.unstageAll") +pull request: user.vscode("pr.create") +# Use keyboard shortcuts because VSCode relies on when clause contexts to choose the appropriate +# action: https://code.visualstudio.com/api/references/when-clause-contexts +change next: key(alt-f5) +change last: key(shift-alt-f5) + +# Testing +test run: user.vscode("testing.runAtCursor") +test run file: user.vscode("testing.runCurrentFile") +test run all: user.vscode("testing.runAll") +test run failed: user.vscode("testing.reRunFailTests") +test run last: user.vscode("testing.reRunLastRun") + +test debug: user.vscode("testing.debugAtCursor") +test debug file: user.vscode("testing.debugCurrentFile") +test debug all: user.vscode("testing.debugAll") +test debug failed: user.vscode("testing.debugFailTests") +test debug last: user.vscode("testing.debugLastRun") + +test cancel: user.vscode("testing.cancelRun") + +# Debugging +break point: user.vscode("editor.debug.action.toggleBreakpoint") +step over: user.vscode("workbench.action.debug.stepOver") +debug step into: user.vscode("workbench.action.debug.stepInto") +debug step out [of]: user.vscode("workbench.action.debug.stepOut") +debug start: user.vscode("workbench.action.debug.start") +debug pause: user.vscode("workbench.action.debug.pause") +debug stopper: user.vscode("workbench.action.debug.stop") +debug continue: user.vscode("workbench.action.debug.continue") +debug restart: user.vscode("workbench.action.debug.restart") +debug console: user.vscode("workbench.debug.action.toggleRepl") +debug clean: user.vscode("workbench.debug.panel.action.clearReplAction") + +# Terminal +terminal external: user.vscode("workbench.action.terminal.openNativeConsole") +terminal new: user.vscode("workbench.action.terminal.new") +terminal next: user.vscode("workbench.action.terminal.focusNext") +terminal last: user.vscode("workbench.action.terminal.focusPrevious") +terminal split: user.vscode("workbench.action.terminal.split") +terminal zoom: user.vscode("workbench.action.toggleMaximizedPanel") +terminal trash: user.vscode("workbench.action.terminal.kill") +terminal toggle: user.vscode_and_wait("workbench.action.terminal.toggleTerminal") +terminal scroll up: user.vscode("workbench.action.terminal.scrollUp") +terminal scroll down: user.vscode("workbench.action.terminal.scrollDown") +terminal : user.vscode_terminal(number_small) + +task run []: + user.vscode("workbench.action.tasks.runTask") + insert(user.text or "") +#TODO: should this be added to linecommands? +copy line down: user.vscode("editor.action.copyLinesDownAction") +copy line up: user.vscode("editor.action.copyLinesUpAction") + +#Expand/Shrink AST Selection +select less: user.vscode("editor.action.smartSelect.shrink") +select (more | this): user.vscode("editor.action.smartSelect.expand") + +minimap: user.vscode("editor.action.toggleMinimap") +maximize: user.vscode("workbench.action.minimizeOtherEditors") +restore: user.vscode("workbench.action.evenEditorWidths") + +#breadcrumb +select breadcrumb: user.vscode("breadcrumbs.focusAndSelect") +# Use `alt-left` and `alt-right` to navigate the bread crumb + +replace here: + user.replace("") + key(cmd-alt-l) + +hover show: user.vscode("editor.action.showHover") + +join lines: user.vscode("editor.action.joinLines") + +full screen: user.vscode("workbench.action.toggleFullScreen") + +curse undo: user.vscode("cursorUndo") +curse redo: user.vscode("cursorRedo") + +select word: user.vscode("editor.action.addSelectionToNextFindMatch") +skip word: user.vscode("editor.action.moveSelectionToNextFindMatch") + +# jupyter +cell next: user.vscode("notebook.focusNextEditor") +cell last: user.vscode("notebook.focusPreviousEditor") +cell run above: user.vscode("notebook.cell.executeCellsAbove") +cell run: user.vscode("notebook.cell.execute") + +install local: user.vscode("workbench.extensions.action.installVSIX") +preview markdown: user.vscode("markdown.showPreview") diff --git a/community/apps/vscode/vscode_command_client.py b/community/apps/vscode/vscode_command_client.py new file mode 100644 index 0000000..1765e12 --- /dev/null +++ b/community/apps/vscode/vscode_command_client.py @@ -0,0 +1,107 @@ +from typing import Any + +from talon import Context, Module, actions + +from ...core.command_client.command_client import NotSet, run_command +from ...core.command_client.rpc_client.types import NoFileServerException + +mod = Module() + +ctx = Context() +linux_ctx = Context() + +ctx.matches = r""" +app: vscode +""" +linux_ctx.matches = r""" +os: linux +app: vscode +""" + +ctx.tags = ["user.command_client"] + + +def command_server_or_client_fallback(command_id: str, wait: bool): + """Execute command via command server, falling back to command palette if directory not present.""" + try: + run_command(command_id, wait_for_finish=wait) + except NoFileServerException: + actions.user.command_palette() + actions.user.paste(command_id) + actions.key("enter") + print( + "Command server directory not found; falling back to command palette. For better performance, install the VSCode extension for Talon: https://marketplace.visualstudio.com/items?itemName=pokey.talon" + ) + + +@ctx.action_class("user") +class VsCodeAction: + def command_server_directory() -> str: + return "vscode-command-server" + + +@mod.action_class +class Actions: + def vscode(command_id: str): + """Execute command via vscode command server, if available, or fallback + to command palette.""" + command_server_or_client_fallback(command_id, False) + + def vscode_and_wait(command_id: str): + """Execute command via vscode command server, if available, and wait + for command to finish. If command server not available, uses command + palette and doesn't guarantee that it will wait for command to + finish.""" + command_server_or_client_fallback(command_id, True) + + # These commands are shims, to provide backwards compatibility, they may be removed in the fuuture. + # Prefer the run_command... version in command_client. + def vscode_with_plugin( + command_id: str, + arg1: Any = NotSet, + arg2: Any = NotSet, + arg3: Any = NotSet, + arg4: Any = NotSet, + arg5: Any = NotSet, + ): + """Execute command via vscode command server.""" + actions.user.run_rpc_command( + command_id, + arg1, + arg2, + arg3, + arg4, + arg5, + ) + + def vscode_with_plugin_and_wait( + command_id: str, + arg1: Any = NotSet, + arg2: Any = NotSet, + arg3: Any = NotSet, + arg4: Any = NotSet, + arg5: Any = NotSet, + ): + """Execute command via vscode command server and wait for command to finish.""" + actions.user.run_rpc_command_and_wait(command_id, arg1, arg2, arg3, arg4, arg5) + + def vscode_get( + command_id: str, + arg1: Any = NotSet, + arg2: Any = NotSet, + arg3: Any = NotSet, + arg4: Any = NotSet, + arg5: Any = NotSet, + ) -> Any: + """Execute command via vscode command server and return command output.""" + return actions.user.run_rpc_command_get( + command_id, arg1, arg2, arg3, arg4, arg5 + ) + + +@linux_ctx.action_class("user") +class LinuxUserActions: + def trigger_command_server_command_execution(): + # Work around bug with upper f-keys in VSCode on Linux. See + # https://github.com/pokey/command-server/issues/9#issuecomment-963733930 + actions.key("ctrl-shift-alt-p") diff --git a/community/apps/vscode/vscode_terminal.talon b/community/apps/vscode/vscode_terminal.talon new file mode 100644 index 0000000..63caed5 --- /dev/null +++ b/community/apps/vscode/vscode_terminal.talon @@ -0,0 +1,7 @@ +app: vscode +# Looks for special string in window title. +# NOTE: This requires you to add a special setting to your VSCode settings.json +# See [our vscode docs](./README.md#terminal) +win.title: /focus:\[Terminal\]/ +- +tag(): terminal diff --git a/community/apps/warp/warp.py b/community/apps/warp/warp.py new file mode 100644 index 0000000..407e3d3 --- /dev/null +++ b/community/apps/warp/warp.py @@ -0,0 +1,32 @@ +from talon import Context, Module, actions + +mod = Module() + +mod.apps.warp = """ +os: mac +and app.bundle: dev.warp.Warp-Stable +""" + +ctx = Context() +ctx.matches = r""" +app: warp +""" + + +@ctx.action_class("user") +class UserActions: + def tab_jump(number: int): + if number < 9: + actions.key(f"cmd-{number}") + + def tab_final(): + actions.key("cmd-9") + + +@ctx.action_class("edit") +class EditActions: + def word_left(): + actions.key("alt-left") + + def word_right(): + actions.key("alt-right") diff --git a/community/apps/warp/warp.talon b/community/apps/warp/warp.talon new file mode 100644 index 0000000..fc5fa2a --- /dev/null +++ b/community/apps/warp/warp.talon @@ -0,0 +1,9 @@ +app: warp +- +tag(): terminal +tag(): user.generic_unix_shell +tag(): user.git +tag(): user.kubectl +tag(): user.tabs +tag(): user.file_manager +tag(): user.readline diff --git a/community/apps/windbg/windbg.py b/community/apps/windbg/windbg.py new file mode 100644 index 0000000..a1ebcbe --- /dev/null +++ b/community/apps/windbg/windbg.py @@ -0,0 +1,178 @@ +from talon import Context, Module, actions + +mod = Module() +mod.tag("windbg", "tag to enabled windbg related functionality") + +# global context for enabling and disabling user.gdb tag +ctx_global = Context() + +# user.windbg-specific context +ctx_windbg_enabled = Context() +ctx_windbg_enabled.matches = r""" +tag: user.windbg +""" + +ctx_windbg_enabled.lists["self.windows_dlls"] = { + "core": "ntdll", + "en tea": "ntdll", + "user": "user32", +} + + +@mod.capture(rule="{self.windows_dlls}") +def windows_dlls(m) -> str: + "Return an register" + return m.windows_dlls + + +@mod.action_class +class Actions: + def windbg_enable(): + """Enables the windbg tag""" + ctx_global.tags = ["user.windbg"] + + def windbg_disable(): + """Disables the windbg tag""" + ctx_global.tags = [] + + +# XXX - trigger alt-1 to hit command window for necessary commands? +# ex: user.windbg_insert_in_cmd() +# edit.left() +@ctx_windbg_enabled.action_class("user") +class UserActions: + ## + # Generic debugger actions + ## + + # Code execution + def debugger_step_into(): + actions.key("f8") + + def debugger_step_over(): + actions.key("f10") + # XXX - + + def debugger_step_line(): + actions.auto_insert("") + + def debugger_step_over_line(): + actions.auto_insert("") + + def debugger_step_out(): + actions.key("shift-f11") + + def debugger_continue(): + actions.key("f5") + + def debugger_stop(): + actions.key("shift-f5") + + def debugger_restart(): + actions.key("ctrl-shift-f5") + + def debugger_detach(): + actions.insert(".detach") + # Registers + + def debugger_show_registers(): + actions.key("r enter") + + def debugger_get_register(): + actions.insert("r @") + + def debugger_set_register(): + actions.user.insert_between("set $@", "=") + # Breakpoints + + def debugger_show_breakpoints(): + actions.insert("bl\n") + + def debugger_add_sw_breakpoint(): + actions.insert("bp ") + + def debugger_add_hw_breakpoint(): + actions.insert("ba e 1 ") + + def debugger_break_now(): + actions.key("ctrl-break") + + def debugger_clear_all_breakpoints(): + actions.insert("bc *\n") + + def debugger_clear_breakpoint(): + actions.insert("bc ") + + def debugger_enable_all_breakpoints(): + actions.insert("be *\n") + + def debugger_enable_breakpoint(): + actions.insert("be ") + + def debugger_disable_all_breakpoints(): + actions.insert("bd *\n") + + def debugger_disable_breakpoint(): + actions.insert("bd ") + # Navigation + + def debugger_goto_address(): + actions.insert("ctrl-g") + + def debugger_goto_clipboard(): + actions.insert("ctrl-g") + actions.edit.paste() + actions.key("enter") + + def debugger_goto_highlighted(): + actions.insert("ctrl-g") + actions.edit.copy() + actions.edit.paste() + actions.key("enter") + # Memory inspection + + def debugger_backtrace(): + actions.key("k enter") + + def debugger_disassemble(): + actions.key("u space") + + def debugger_disassemble_here(): + actions.key("u enter") + + def debugger_disassemble_clipboard(): + actions.key("u space") + actions.edit.paste() + actions.key("enter") + + def debugger_dump_ascii_string(): + actions.insert("da ") + + def debugger_dump_unicode_string(): + actions.insert("du ") + + def debugger_dump_pointers(): + actions.insert("dps ") + + def debugger_list_modules(): + actions.insert("lm\n") + # Registers XXX + + def debugger_inspect_type(): + actions.insert("dt ") + # Convenience + + def debugger_clear_line(): + actions.key("ctrl-a backspace") + ## + # Windbg specific functionality + ## + + def debugger_clear_breakpoint_id(number_small: int): + actions.insert(f"bc {number_small}\n") + + def debugger_disable_breakpoint_id(number_small: int): + actions.insert(f"bd {number_small}\n") + + def debugger_enable_breakpoint_id(number_small: int): + actions.insert(f"be {number_small}\n") diff --git a/community/apps/windbg/windbg.talon b/community/apps/windbg/windbg.talon new file mode 100644 index 0000000..22b14e1 --- /dev/null +++ b/community/apps/windbg/windbg.talon @@ -0,0 +1,46 @@ +# XXX - trigger alt-1 to hit command window for necessary commands? +# ex: user.windbg_insert_in_cmd() +# edit.left() +tag: user.windbg +- +tag(): user.debugger + +register : + key(@) + insert("{registers}") + +open help: insert(".hh\n") + +# xxx - add window switching + +add microsoft symbols: + insert("srv*C:\\symbols*http://msdl.microsoft.com/download/symbols;\n") +force reload symbols: insert(".reload /f\n") +reload symbols: insert(".reload\n") +loaded modules: insert("lm l\n") + +display pointers: insert("dps ") + +# XXX - should be generic +dereference pointer: user.insert_between("poi(", ")") + +show version: key(ctrl-alt-w) + +## +# Windows +## + +view command: key(alt-1) +view watch: key(alt-2) +view locals: key(alt-3) +view registers: key(alt-4) +view memory: key(alt-5) +view call stack: key(alt-6) +view disassembly: key(alt-7) +view scratch pad: key(alt-8) +view (processes | threads): key(alt-9) + +# XXX - temp +dump function params: "r @rcx,@rdx,@r8,@r9\n" + +(lib | library) : "{windows_dlls}" diff --git a/community/apps/windows_command_processor/command_processor_actions_win.py b/community/apps/windows_command_processor/command_processor_actions_win.py new file mode 100644 index 0000000..20ebaf1 --- /dev/null +++ b/community/apps/windows_command_processor/command_processor_actions_win.py @@ -0,0 +1,27 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +app: windows_command_processor +app: windows_terminal +and win.title: /Command Prompt/ +""" +ctx.tags = ["user.file_manager", "user.git", "user.kubectl", "terminal"] + + +@ctx.action_class("user") +class UserActions: + def file_manager_refresh_title(): + actions.insert("title Command Prompt: %CD%") + actions.key("enter") + + def file_manager_open_parent(): + actions.insert("cd ..") + actions.key("enter") + actions.user.file_manager_refresh_title() + + +@ctx.action_class("edit") +class EditActions: + def delete_line(): + actions.key("esc") diff --git a/community/apps/windows_command_processor/command_processor_win.py b/community/apps/windows_command_processor/command_processor_win.py new file mode 100644 index 0000000..389178c --- /dev/null +++ b/community/apps/windows_command_processor/command_processor_win.py @@ -0,0 +1,112 @@ +import os + +from talon import Context, actions, ui + +ctx = Context() +ctx.matches = r""" +app: windows_command_processor +app: windows_terminal +and win.title: /Command Prompt/ +""" + +user_path = os.path.expanduser("~") +directories_to_remap = {} +directories_to_exclude = {} + +ctx.tags = ["user.file_manager", "user.git", "user.kubectl", "terminal"] + + +@ctx.action_class("edit") +class EditActions: + def delete_line(): + actions.key("esc") + + +@ctx.action_class("user") +class UserActions: + def file_manager_refresh_title(): + actions.insert("title Command Prompt: %CD%") + actions.key("enter") + + def file_manager_open_parent(): + actions.insert("cd ..") + actions.key("enter") + actions.user.file_manager_refresh_title() + + def file_manager_current_path(): + path = ui.active_window().title + path = path.replace("Administrator: ", "").replace("Command Prompt: ", "") + if path in directories_to_remap: + path = directories_to_remap[path] + + if path in directories_to_exclude: + path = "" + return path + + # def file_manager_terminal_here(): + # actions.key("ctrl-l") + # actions.insert("cmd.exe") + # actions.key("enter") + + # def file_manager_show_properties(): + # """Shows the properties for the file""" + # actions.key("alt-enter") + + 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() + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(f'"{path}"') + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + actions.insert(f'mkdir "{name}"') + + def file_manager_open_file(path: str): + """opens the file""" + actions.insert(path) + # actions.key("enter") + + def file_manager_select_file(path: str): + """selects the file""" + actions.insert(path) + + def file_manager_open_volume(volume: str): + """file_manager_open_volume""" + actions.user.file_manager_open_directory(volume) + + def terminal_list_directories(): + """Lists directories""" + actions.insert("dir") + actions.key("enter") + + def terminal_list_all_directories(): + actions.insert("dir /a") + actions.key("enter") + + def terminal_change_directory(path: str): + actions.insert(f"cd {path}") + # if path: + # actions.key("enter") + + def terminal_change_directory_root(): + """Root of current drive""" + actions.insert("cd /") + actions.key("enter") + + def terminal_clear_screen(): + """Clear screen""" + actions.insert("cls") + actions.key("enter") + + def terminal_run_last(): + actions.key("up enter") + + def terminal_kill_all(): + actions.key("ctrl-c") + actions.insert("y") + actions.key("enter") diff --git a/community/apps/windows_explorer/windows_explorer.py b/community/apps/windows_explorer/windows_explorer.py new file mode 100644 index 0000000..1ab0d28 --- /dev/null +++ b/community/apps/windows_explorer/windows_explorer.py @@ -0,0 +1,153 @@ +import os + +from talon import Context, Module, actions, app, ui + +mod = Module() +apps = mod.apps + +apps.windows_explorer = r""" +os: windows +and app.name: Windows Explorer +os: windows +and app.name: Windows-Explorer +os: windows +and app.exe: /^explorer\.exe$/i +""" + +# many commands should work in most save/open dialog. +# note the "show options" stuff won't work +# unless the path is displayed in the title, which is rare for those +apps.windows_file_browser = """ +os: windows +and app.name: /.*/ +and title: /(Save|Open|Browse|Select)/ +""" + +ctx = Context() +ctx.matches = r""" +app: windows_explorer +app: windows_file_browser +""" + +user_path = os.path.expanduser("~") +directories_to_remap = {} +directories_to_exclude = {} + +if app.platform == "windows": + is_windows = True + import ctypes + + GetUserNameEx = ctypes.windll.secur32.GetUserNameExW + NameDisplay = 3 + + size = ctypes.pointer(ctypes.c_ulong(0)) + GetUserNameEx(NameDisplay, None, size) + + nameBuffer = ctypes.create_unicode_buffer(size.contents.value) + GetUserNameEx(NameDisplay, nameBuffer, size) + one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) + + # this is probably not the correct way to check for onedrive, quick and dirty + if os.path.isdir(os.path.expanduser(os.path.join("~", r"OneDrive\Desktop"))): + directories_to_remap = { + "Desktop": os.path.join(one_drive_path, "Desktop"), + "Documents": os.path.join(one_drive_path, "Documents"), + "Downloads": os.path.join(user_path, "Downloads"), + "Music": os.path.join(user_path, "Music"), + "OneDrive": one_drive_path, + "Pictures": os.path.join(one_drive_path, "Pictures"), + "Videos": os.path.join(user_path, "Videos"), + } + else: + # todo use expanduser for cross platform support + directories_to_remap = { + "Desktop": os.path.join(user_path, "Desktop"), + "Documents": os.path.join(user_path, "Documents"), + "Downloads": os.path.join(user_path, "Downloads"), + "Music": os.path.join(user_path, "Music"), + "OneDrive": one_drive_path, + "Pictures": os.path.join(user_path, "Pictures"), + "Videos": os.path.join(user_path, "Videos"), + } + + if nameBuffer.value: + directories_to_remap[nameBuffer.value] = user_path + + directories_to_exclude = [ + "", + "Run", + "Task Switching", + "Task View", + "This PC", + "File Explorer", + "Program Manager", + ] + + +@ctx.action_class("user") +class UserActions: + + def file_manager_open_parent(): + actions.key("alt-up") + + def file_manager_current_path(): + path = ui.active_window().title + + if path in directories_to_remap: + path = directories_to_remap[path] + + if path in directories_to_exclude: + actions.user.file_manager_hide_pickers() + path = "" + + return path + + def file_manager_terminal_here(): + actions.key("ctrl-l") + actions.insert("cmd.exe") + actions.key("enter") + + def file_manager_show_properties(): + """Shows the properties for the file""" + actions.key("alt-enter") + + def file_manager_open_directory(path: str): + """opens the directory that's already visible in the view""" + actions.key("ctrl-l") + actions.insert(path) + actions.key("enter") + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(path) + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + actions.key("home") + actions.key("ctrl-shift-n") + actions.insert(name) + + def file_manager_open_file(path: str): + """opens the file""" + actions.key("home") + actions.insert(path) + actions.key("enter") + + def file_manager_select_file(path: str): + """selects the file""" + actions.key("home") + actions.insert(path) + + 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/community/apps/windows_explorer/windows_explorer.talon b/community/apps/windows_explorer/windows_explorer.talon new file mode 100644 index 0000000..ea7276f --- /dev/null +++ b/community/apps/windows_explorer/windows_explorer.talon @@ -0,0 +1,9 @@ +app: windows_explorer +app: windows_file_browser +- +tag(): user.address +tag(): user.file_manager +tag(): user.navigation + +go app data: user.file_manager_open_directory("%AppData%") +go program files: user.file_manager_open_directory("%ProgramFiles%") diff --git a/community/apps/windows_terminal/windows_terminal.py b/community/apps/windows_terminal/windows_terminal.py new file mode 100644 index 0000000..0c2cb1e --- /dev/null +++ b/community/apps/windows_terminal/windows_terminal.py @@ -0,0 +1,177 @@ +import os + +from talon import Context, Module, actions, ui + +ctx = Context() +mod = Module() +ctx.matches = r""" +app: windows_terminal +""" + +user_path = os.path.expanduser("~") +directories_to_remap = {} +directories_to_exclude = {} + + +@ctx.action_class("app") +class AppActions: + def tab_close(): + actions.key("ctrl-shift-w") + + def tab_open(): + actions.key("ctrl-shift-t") + + +@ctx.action_class("edit") +class EditActions: + def paste(): + actions.key("ctrl-shift-v") + + def copy(): + actions.key("ctrl-shift-c") + + def find(text: str = None): + actions.key("ctrl-shift-f") + if text: + actions.insert(text) + + +@ctx.action_class("user") +class UserActions: + def file_manager_current_path(): + path = ui.active_window().title + path = ( + path.replace("Administrator: ", "") + .replace("Windows PowerShell: ", "") + .replace("Command Prompt: ", "") + ) + + if path in directories_to_remap: + path = directories_to_remap[path] + + if path in directories_to_exclude: + path = "" + return path + + # def file_manager_terminal_here(): + # actions.key("ctrl-l") + # actions.insert("cmd.exe") + # actions.key("enter") + + # def file_manager_show_properties(): + # """Shows the properties for the file""" + # actions.key("alt-enter") + + 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() + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(f'"{path}"') + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + actions.insert(f'mkdir "{name}"') + + def file_manager_open_file(path: str): + """opens the file""" + actions.insert(path) + # actions.key("enter") + + def file_manager_select_file(path: str): + """selects the file""" + actions.insert(path) + + def file_manager_open_volume(volume: str): + """file_manager_open_volume""" + actions.user.file_manager_open_directory(volume) + actions.user.file_manager_refresh_title() + + def tab_jump(number: int): + actions.key(f"ctrl-alt-{number}") + + # user.splits implementation: + + def split_window_right(): + """Move active tab to right split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split right" is not possible in windows terminal without special configuration. Use "split vertically" instead.' + ) + + def split_window_left(): + """Move active tab to left split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split left" is not possible in windows terminal without special configuration. Use "split vertically" instead.' + ) + + def split_window_down(): + """Move active tab to lower split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split down" is not possible in windows terminal without special configuration. Use "split horizontally" instead.' + ) + + def split_window_up(): + """Move active tab to upper split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split up" is not possible in windows terminal without special configuration. Use "split horizontally" instead.' + ) + + def split_window_vertically(): + """Splits window vertically""" + actions.key("shift-alt-plus") + + def split_window_horizontally(): + """Splits window horizontally""" + actions.key("shift-alt-minus") + + def split_flip(): + """Flips the orietation of the active split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split flip" is not possible in windows terminal in default configuration.' + ) + + def split_window(): + """Splits the window""" + # in this implementation an alias for split vertically + actions.key("shift-alt-plus") + + def split_clear(): + """Clears the current split""" + # also closes tab, because shortcut is the same + # and closing a split does mean something differnent that in a code editor like vs code + actions.key("ctrl-shift-w") + + def split_clear_all(): + """Clears all splits""" + # TODO: decide whether to implement it at all since it either doesn't makes sense or closes the window/whole tab + + def split_next(): + """Goes to next split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split next" is not possible in windows terminal without special configuration. Use "focus left/right/up/down" instead.' + ) + + def split_last(): + """Goes to last split""" + # TODO: decide whether this notification is good style + actions.app.notify( + '"Split last" is not possible in windows terminal without special configuration. Use "focus left/right/up/down" instead.' + ) + + def split_number(index: int): + """Navigates to a the specified split""" + actions.app.notify( + '"Split_number" is not possible in windows terminal in default configuration.' + ) + + def tab_final(): + actions.key("ctrl-alt-9") diff --git a/community/apps/windows_terminal/windows_terminal.talon b/community/apps/windows_terminal/windows_terminal.talon new file mode 100644 index 0000000..038c4ec --- /dev/null +++ b/community/apps/windows_terminal/windows_terminal.talon @@ -0,0 +1,32 @@ +app: windows_terminal +- +# makes the commands in terminal.talon available +tag(): terminal + +# activates the implementation of the commands/functions in terminal.talon +tag(): user.generic_windows_shell + +# makes commands for certain applications available +# you can deactivate them if you do not use the application +tag(): user.git +tag(): user.anaconda +# tag(): user.kubectl + +tag(): user.tabs +# TODO: file_manager +tag(): user.splits + +settings open: key(ctrl-,) +focus left: key(ctrl-alt-shift-left) +focus right: key(ctrl-alt-shift-right) +focus up: key(ctrl-alt-shift-up) +focus down: key(ctrl-alt-shift-down) +term menu: key(ctrl-shift-f1) + +find it: edit.find() + +find it $: + # handle consecutive finds by first escaping out of any previous one + key(escape) + # make it so + edit.find("{phrase}\n") diff --git a/community/apps/wsl/wsl.py b/community/apps/wsl/wsl.py new file mode 100644 index 0000000..1e714be --- /dev/null +++ b/community/apps/wsl/wsl.py @@ -0,0 +1,533 @@ +import logging +import os +import re +import subprocess +import sys + +from talon import Context, Module, actions, app, ui +from talon.debug import log_exception + +mod = Module() + +mod.tag("wsl", desc="Tag to activate WSL support in Talon") + +ctx = Context() + +# note: this context match is intentionally made more complex so that it is more specific +# than the context defined in apps/win/windows_terminal/windows_terminal.py (and thereby +# takes precedence). +ctx.matches = """ +app: windows_terminal +and tag: user.wsl +tag: user.wsl +""" + +if app.platform == "windows": + import platform + + import win32api + import win32con + import win32event + + wsl_distros = [] + + key_event = None + registry_key_handle = None + + # we expect the window title to begin with 'WSL: ' and end with ': '. + # this can be achieved by setting the window title in your .bashrc (or equivalent) + # file and making use of the WSL_DISTRO_NAME environment variable. + # + # take, for example, the default .bashrc for Ubuntu-20.04 - the window title was set + # by changing the prompt definition from this: + # + # PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\h: \w\a\]$PS1" + # + # to this: + # + # PS1="\[\e]0;${debian_chroot:+($debian_chroot)}WSL:${WSL_DISTRO_NAME} \u@\h: \w\a\]$PS1" ^^^^^^^^^^^^^^^^^^^^^^ + # + # any other regex can be used below if your title is formatted differently. just be sure the + # resulting capture groups contain the distro and the path, in that order. + wsl_title_regex = re.compile(r"^WSL:([^\s]+)\s*.*@.*:\s*(.*)$") + + # prepare flags to use for registry calls + registry_access_flags = win32con.KEY_READ + # not sure if this check is important...I know the win32con.KEY_WOW64_64KEY value is needed + # on my 64-bit windows install, but I don't know what happens on 32-bit installs...so, + # playing it safe here. + # https://stackoverflow.com/questions/2208828/detect-64bit-os-windows-in-python/12578715 + if platform.machine().endswith("64"): + registry_access_flags = registry_access_flags | win32con.KEY_WOW64_64KEY + + # close registry key, if open + def _close_key(): + global registry_key_handle + # print(f"_close_key(): {registry_key_handle}") + if registry_key_handle: + win32api.RegCloseKey(registry_key_handle) + registry_key_handle = None + + # open the registry key containing the list of installed wsl distros + def _initialize_key(): + global key_event, registry_key_handle, registry_access_flags + + try: + # make sure the registry key is not currently open + if registry_key_handle: + _close_key() + + # get an event for monitoring registry updates + key_event = win32event.CreateEvent(None, True, True, None) + # print(f"KEY_EVENT: {key_event}") + + # open the registry key + registry_key_handle = win32api.RegOpenKeyEx( + win32con.HKEY_CURRENT_USER, + r"SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss", + 0, + registry_access_flags, + ) + # print(f"registry_key_handle: {registry_key_handle}") + + # register for registry change events + win32api.RegNotifyChangeKeyValue( + registry_key_handle, + True, + win32api.REG_NOTIFY_CHANGE_LAST_SET, + key_event, + True, + ) + + # trigger reading the list for the first time + win32event.SetEvent(key_event) + except OSError: + log_exception(f"[_initialize_key()] {sys.exc_info()[1]}") + + # read the list of wsl distros from the registry + def _update_wsl_distros(): + global ctx, registry_key_handle, wsl_distros, registry_access_flags + + # make sure registry is open + if not registry_key_handle: + _initialize_key() + + distro_handle = None + try: + # check for registry changes + result = win32event.WaitForSingleObjectEx(key_event, 0, False) + # for testing + if False: + print(f"WAIT - {result=} (looking for 'win32con.WAIT_OBJECT_0')") + print(f"WAIT - {win32con.WAIT_OBJECT_0=})") + print(f"WAIT - {win32con.WAIT_ABANDONED=})") + print(f"WAIT - {win32con.WAIT_TIMEOUT=})") + if result == win32con.WAIT_OBJECT_0: + # registry has changed since we last read it, load the distros + subkeys = win32api.RegEnumKeyEx(registry_key_handle) + for subkey in subkeys: + # print(f'{subkey=}') + + distro_handle = win32api.RegOpenKeyEx( + registry_key_handle, subkey[0], 0, registry_access_flags + ) + # print(f"{distro_handle=}") + + distro_name = win32api.RegQueryValueEx( + distro_handle, "DistributionName" + )[0] + # print(f'{distro_name=}') + wsl_distros.append(distro_name) + + win32api.RegCloseKey(distro_handle) + + # reset the event, will be set by system if reg key changes + win32event.ResetEvent(key_event) + + elif result != win32con.WAIT_TIMEOUT: + # something unexpected happened + error = win32api.GetLastError() + _close_key() + raise Exception( + f"failed while checking for wsl registry updates: {result=}: {error=}" + ) + except OSError: + if distro_handle: + win32api.RegCloseKey(distro_handle) + log_exception(f"[_update_wsl_distros()] {sys.exc_info()[1]}") + + # print(f'{wsl_distros=}') + + def _parse_win_title(): + path = ui.active_window().title + + _update_wsl_distros() + distro = None + try: + (distro, path) = re.match(wsl_title_regex, path).groups() + if distro not in wsl_distros: + raise Exception(f"Unknown wsl distro: {distro}") + # log_exception(f'[_update_wsl_distros()] {sys.exc_info()[1]}') + except: + try: + # select line tail following the last colon in the window title + path = path.split(":")[-1].lstrip() + except: + path = "" + + # print(f'TITLE PARSE - distro is {distro}, path is {path}') + return (distro, path) + + +directories_to_remap = {} +directories_to_exclude = {} + +# some definitions used for error handling +termination_error = "The Windows Subsystem for Linux instance has terminated." +restart_message = 'wsl path detection is offline, you need to restart your wsl session, e.g. "wsl --terminate ; wsl"' +path_detection_disable_title = "Talon - WSL path detection disabled" +path_detection_disable_notice = "WSL path detection has been disabled because new WSL sessions cannot be started. See the log for more detail." +path_detection_disabled = False + +user_path = os.path.expanduser("~") +if app.platform == "windows": + is_windows = True + one_drive_path = os.path.expanduser(os.path.join("~", "OneDrive")) + + # this is probably not the correct way to check for onedrive, quick and dirty + if os.path.isdir(os.path.expanduser(os.path.join("~", r"OneDrive\Desktop"))): + directories_to_remap = { + "Desktop": os.path.join(one_drive_path, "Desktop"), + "Documents": os.path.join(one_drive_path, "Documents"), + "Downloads": os.path.join(user_path, "Downloads"), + "Music": os.path.join(user_path, "Music"), + "OneDrive": one_drive_path, + "Pictures": os.path.join(one_drive_path, "Pictures"), + "Videos": os.path.join(user_path, "Videos"), + } + else: + # todo use expanduser for cross platform support + directories_to_remap = { + "Desktop": os.path.join(user_path, "Desktop"), + "Documents": os.path.join(user_path, "Documents"), + "Downloads": os.path.join(user_path, "Downloads"), + "Music": os.path.join(user_path, "Music"), + "OneDrive": one_drive_path, + "Pictures": os.path.join(user_path, "Pictures"), + "Videos": os.path.join(user_path, "Videos"), + } + + +def get_win_path(wsl_path, distro=None): + # for testing + # wsl_path = 'Ubuntu-20.04' + # wsl_path = '/mnt/qube/woobee/woobee/woobit' + # print(f"WINPATH: {wsl_path}") + return run_wslpath(["-w"], wsl_path, distro) + + +def get_usr_path(distro=None): + # print(f'USRPATH: {"~"}') + return run_wslpath(["-a"], "~", distro) + + +def get_wsl_path(win_path, distro=None): + # print(f"WSLPATH: {win_path}") + return run_wslpath(["-u"], f"'{win_path}'", distro) + + +def _disable_path_detection(notify=True): + global path_detection_disabled + path_detection_disabled = True + if notify: + app.notify( + title=path_detection_disable_title, body=path_detection_disable_notice + ) + + +# this command fails every once in a while, with no indication why. +# so, when that happens we just retry. +MAX_ATTEMPTS = 2 + + +def run_wslpath(args, in_path, in_distro=None): + path = "" + + if not path_detection_disabled: + loop_num = 0 + + while loop_num < MAX_ATTEMPTS: + # print(f"_run_wslpath(): {path_detection_disabled=}.") + (distro, path, error) = run_wsl(["wslpath", *args, in_path], in_distro) + if error: + if in_path == distro and error.endswith("No such file or directory"): + # for testing + # print(f"run_wslpath(): - ignoring expected failure.") + + # this is expected. happens when running after the window is created + # but before the default title has been changed. no need to spam the + # console for this case, just let it pass. + pass + else: + logging.error( + f"run_wslpath(): failed to translate given path - attempt: {loop_num}, error: {error}" + ) + + path = "" + if error == termination_error: + # disable this code until the user resets it + _disable_path_detection() + break + elif path: + # got it, no need to loop and try again + break + + loop_num += 1 + + return path + + +# Note: seems WSL itself generates utf-16-le errors, whereas your guest os probably does not. +# - see https://github.com/microsoft/WSL/issues/4607 and related issures. Not sure how this +# behavior might differ when the system locale has been changed from the default. +# +# Anyways, these WSL errors require special handling so they are logged clearly. This is presumably +# worthwhile given the likely importance of any such messages. For example, which would you rather +# see in the log? +# +# 1. Nothing at all, even though there might be serious problems. +# +# 2. b'T\x00h\x00e\x00 \x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x00S\x00u\x00b\x00s\x00y\x00s\x00t\x00e\x00m\x00 \x00f\x00o\x00r\x00 \x00L\x00i\x00n\x00u\x00x\x00 \x00i\x00n\x00s\x00t\x00a\x00n\x00c\x00e\x00 \x00h\x00a\x00s\x00 \x00t\x00e\x00r\x00m\x00i\x00n\x00a\x00t\x00e\x00d\x00.\x00\r\x00\r\x00\n\x00' +# +# 3. The Windows Subsystem for Linux instance has terminated. +# +# The error above indicates the WSL distro is hung and this result detection mechanism is offline. When +# that happens, it takes a while for the command to return and the talon watchdog generates messages +# in the log that indicate a hang but we can provide more contextual detail. The prime thing to do here +# is to get word to the user that WSL is not responding normally. Note that, even after reaching this +# state, existing interactive wsl sessions continue to run and so the user may be unaware of the true +# source of their "talon problems". For more information, see https://github.com/microsoft/WSL/issues/5110 +# and https://github.com/microsoft/WSL/issues/5318. +# +# 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.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 +# wsl immediately (because your existing sessions are still running and you want to finish working before +# restarting wsl). +# +# So, wsl path detection is disabled when this condition is first detected. The user +# must then re-enable the feature once the underlying problem has been resolved. This can be done by +# using the 'weasel reset path detection' voice command or simply reloading this file. + + +def _decode(value: bytes) -> str: + # check to see if the given byte string looks like utf-16-le. results may not be correct for all + # possible cases, but if there's a problem this code can be replaced with chardet (once that module + # covers utf-16-le - see https://github.com/chardet/chardet/pull/109#discussion_r119149003). of + # course, by that time wsl might not have the same problem anyways. + if (len(value) % 2 == 0) and sum(value[1::2]) == 0: + # looks like utf-16-le, see https://github.com/microsoft/WSL/issues/4607 (and related issues). + decoded = value.decode("UTF-16-LE") + else: + decoded = value.decode() + # print(f"_decode(): value is {value}") + # print(f"_decode(): decoded is {decoded}.") + return decoded.strip() + + +def _run_cmd(command_line): + result = error = "" + # print(f"_run_cmd(): RUNNING - command line is {command_line}.") + try: + # for testing + # raise subprocess.CalledProcessError(-4294967295, command_line, termination_error.encode('UTF-16-LE')) + + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = subprocess.SW_HIDE + + tmp = subprocess.check_output( + command_line, stderr=subprocess.STDOUT, startupinfo=startupinfo + ) + result = _decode(tmp) + # print(f"RESULT: command: {' '.join(command_line)}, result: {result}") + except subprocess.CalledProcessError as exc: + result = "" + + # decode the error + error = _decode(exc.output) + + # log additional info for this particular case + if error == termination_error: + logging.error(f"_run_cmd(): failed to run command - error: {error}") + logging.error(f"_run_cmd(): - {restart_message}") + except: + result = "" + log_exception(f"[_run_cmd()] {sys.exc_info()[1]}") + + # return results for the last attempt + # print(f'_run_cmd(): RETURNING - result: {result}, error: {error}') + return [result, error] + + +def run_wsl(args, distro=None): + # for testing + if False: + wsl_cmd_str = "nosuchcommand" + else: + wsl_cmd_str = "wsl" + + # for testing + # distro = "Debian" + # distro = 'Ubuntu-20.04-ms-0' + + if not distro: + # fetch the (default) distro first + result = _run_cmd([wsl_cmd_str, "echo", "$WSL_DISTRO_NAME"]) + distro = result[0] + if not distro: + # if we can't fetch the distro, then the user's command is not likely to work + # either. so, we just return any error information we have to the caller. + # print(f'run_wsl(): RETURNING EARLY (no distro) - distro: {distro}, result: {result}') + return [None] + result + + # now run the caller's command + command_line = [wsl_cmd_str, "--distribution", distro] + args + result = _run_cmd(command_line) + # print(f'run_wsl(): RETURNING - distro: {distro}, result: {result}') + return [distro] + result + + +def get_distro(): + return run_wsl(["\n"])[0] + + +@ctx.action_class("user") +class UserActions: + def file_manager_refresh_title(): + actions.skip() + + def file_manager_open_parent(): + actions.insert("cd ..") + actions.key("enter") + + def file_manager_current_path(): + if path_detection_disabled: + logging.warning( + 'Skipping WSL path detection - try "weasel reset path detection"' + ) + return "" + + (distro, path) = _parse_win_title() + + if "~" in path: + # the only way I could find to correctly support the user folder: + # get absolute path of ~, and strip /mnt/x from the string + abs_usr_path = get_usr_path(distro) + abs_usr_path = abs_usr_path[abs_usr_path.find("/home") : len(abs_usr_path)] + path = path.replace("~", abs_usr_path) + + path = get_win_path(path, distro) + + if path in directories_to_remap: + path = directories_to_remap[path] + + if path in directories_to_exclude: + path = "" + + return path + + # def file_manager_terminal_here(): + # actions.key("ctrl-l") + # actions.insert("cmd.exe") + # actions.key("enter") + + # def file_manager_show_properties(): + # """Shows the properties for the file""" + # actions.key("alt-enter") + + def file_manager_open_directory(path: str): + """opens the directory that's already visible in the view""" + if ":" in str(path): + path = get_wsl_path(path) + + actions.insert(f'cd "{path}"') + actions.key("enter") + actions.user.file_manager_refresh_title() + + def file_manager_select_directory(path: str): + """selects the directory""" + actions.insert(f'"{path}"') + + def file_manager_new_folder(name: str): + """Creates a new folder in a gui filemanager or inserts the command to do so for terminals""" + actions.insert(f'mkdir "{name}"') + + def file_manager_open_file(path: str): + actions.insert(path) + # actions.key("enter") + + def file_manager_select_file(path: str): + actions.insert(path) + + def file_manager_open_volume(volume: str): + actions.user.file_manager_open_directory(volume) + + def terminal_list_directories(): + actions.insert("ls") + actions.key("enter") + + def terminal_list_all_directories(): + actions.insert("ls -a") + actions.key("enter") + + def terminal_change_directory(path: str): + actions.insert(f"cd {path}") + if path: + actions.key("enter") + + def terminal_change_directory_root(): + """Root of current drive""" + actions.insert("cd /") + actions.key("enter") + + def terminal_clear_screen(): + """Clear screen""" + actions.key("ctrl-l") + + def terminal_run_last(): + actions.key("up enter") + + def terminal_kill_all(): + actions.key("ctrl-c") + actions.insert("y") + actions.key("enter") + + +@mod.action_class +class Actions: + def wsl_reset_path_detection(): + """reset wsl path detection""" + global path_detection_disabled + path_detection_disabled = False + + def wsl_speak(): + """ask each distro to say hello (in the log)""" + results = [] + _update_wsl_distros() + for in_distro in wsl_distros: + (distro, result, error) = run_wsl( + ["echo", 'Hello, my name is "${WSL_DISTRO_NAME}".'], in_distro + ) + if error: + logging.error(f"wsl_speak(): {error=}") + else: + # print(f'{result=}') + if len(result) == 0: + result = f'Distro "{in_distro}" has nothing to say.' + results.append(result) + print("\n" + "\n".join(results)) diff --git a/community/apps/wsl/wsl.talon b/community/apps/wsl/wsl.talon new file mode 100644 index 0000000..94fd921 --- /dev/null +++ b/community/apps/wsl/wsl.talon @@ -0,0 +1,40 @@ +# NOTE: to use these commands you will need to activate the tag below in whatever contexts you +# choose. +# +# do this in a separate .talon file or via python. for example, if you use windows terminal for +# wsl then you might do this: +# +# os: windows +# app: windows_terminal +# - +# tag(): user.wsl +# +# however, if you also use windows terminal for other things (powershell), you will want something +# more specific...like this: +# +# os: windows +# app: windows_terminal +# title: /^WSL:/ +# - +# tag(): user.wsl +# +# then, you will need to find a way to set the window title accordingly. for example, to match +# the title pattern above, you can set the prompt in your .bashrc file like this: +# +# PS1="\[\e]0;${debian_chroot:+($debian_chroot)}WSL:${WSL_DISTRO_NAME} \u@\h: \w\a\]$PS1" +# +# ALSO: if you do populate your window title with your distro name, make sure the 'wsl_title_regex' +# value in wsl.py is set accordingly. +tag: user.wsl +- + +tag(): terminal +tag(): user.file_manager +tag(): user.generic_unix_shell +tag(): user.git +tag(): user.kubectl + +^go $: user.file_manager_open_volume("/mnt/{letter}") + +(wsl | weasel) reset path detection: user.wsl_reset_path_detection() +(wsl | weasel) speak: user.wsl_speak() diff --git a/community/core/README.md b/community/core/README.md new file mode 100644 index 0000000..d84aff4 --- /dev/null +++ b/community/core/README.md @@ -0,0 +1,21 @@ +# core + +This folder contains `edit_settings.talon`, which has a command to open various [settings](https://github.com/talonhub/community?tab=readme-ov-file#settings) files. As an overview of what commands the subfolders contain: + +- `abbreviate` has a command for the use of abbreviations +- `app_switcher` does not have commands but has the implementation of functions that allow for switching between applications +- `contacts` has captures for inserting contact information including name and email. +- `edit` has commands for navigating and editing text with copy, paste, etc., as well as commands for zooming in and out +- `file_extension` has a command for simpler spoken forms of file and website name extensions +- `help` has commands to open various help menus, as described in the top level [README](https://github.com/talonhub/community?tab=readme-ov-file#getting-started-with-talon) +- `homophones` has commands to replace words with their homophones +- `keys` has commands for [pressing keys](https://github.com/talonhub/community?tab=readme-ov-file#keys) +- `modes` has commands for switching between dictation, command, and sleep mode, as well as for forcing a certain [programming language](https://github.com/talonhub/community?tab=readme-ov-file#programming-languages) mode +- `mouse_grid` has commands to use a grid on the screen to click at a specific location +- `numbers` has the command for writing a number +- `screens` has a command for talon to show the index associated with each of your computer screens for the sake of moving windows to different screens +- `snippets` has commands for inserting snippets of code for various languages +- `text` has commands for inserting and reformatting text +- `vocabulary` has commands for adding new words to be recognized and for having certain words automatically by replaced by others +- `websites_and_search_engines` has commands for opening websites, following links, and making browser searches +- `windows_and_tabs` has commands for tab and [window management](https://github.com/talonhub/community?tab=readme-ov-file#window-management), launching and switching between different applications, and snapping application windows to different locations on the screen diff --git a/community/core/abbreviate/abbreviate.py b/community/core/abbreviate/abbreviate.py new file mode 100644 index 0000000..388744f --- /dev/null +++ b/community/core/abbreviate/abbreviate.py @@ -0,0 +1,478 @@ +import re + +from talon import Context, Module + +from ..user_settings import track_csv_list + +mod = Module() +ctx = Context() +mod.list("abbreviation", desc="Common abbreviation") + +abbreviations_list = {} +abbreviations = { + "J peg": "jpg", + "abbreviate": "abbr", + "abort": "abrt", + "acknowledge": "ack", + "address": "addr", + "addresses": "addrs", + "administrator": "admin", + "administrators": "admins", + "advance": "adv", + "advanced": "adv", + "alberta": "ab", + "allocate": "alloc", + "alternative": "alt", + "apple": "appl", + "application": "app", + "applications": "apps", + "argument": "arg", + "arguments": "args", + "as far as i can tell": "afaict", + "as far as i know": "afaik", + "assembly": "asm", + "asynchronous": "async", + "at the moment": "atm", + "attribute": "attr", + "attributes": "attrs", + "authenticate": "auth", + "authentication": "authn", + "authorization": "authz", + "auto group": "augroup", + "average": "avg", + "away from keyboard": "afk", + "backup": "bkp", + "be right back": "brb", + "binary": "bin", + "block": "blk", + "boolean": "bool", + "bottom": "bot", + "break point": "bp", + "break points": "bps", + "british columbia": "bc", + "buffer": "buf", + "button": "btn", + "by the way": "btw", + "calculate": "calc", + "calculator": "calc", + "camera": "cam", + "canada": "ca", + "centimeter": "cm", + "char": "chr", + "character": "char", + "check": "chk", + "child": "chld", + "china": "cn", + "class": "cls", + "client": "cli", + "column": "col", + "command": "cmd", + "commands": "cmds", + "comment": "cmt", + "communication": "comm", + "communications": "comms", + "compare": "cmp", + "condition": "cond", + "conference": "conf", + "config": "cfg", + "configuration": "config", + "configurations": "configs", + "connection": "conn", + "constant": "const", + "contribute": "contrib", + "constructor": "ctor", + "context": "ctx", + "control flow graph": "cfg", + "control": "ctrl", + "coordinate": "coord", + "coordinates": "coords", + "copy": "cpy", + "count": "cnt", + "counter": "ctr", + "credential": "cred", + "credentials": "creds", + "cross reference": "xref", + "cross references": "xrefs", + "cuddle": "ctl", + "current": "cur", + "cute": "qt", + "database": "db", + "date format": "yyyy-mm-dd", + "debian": "deb", + "debug": "dbg", + "decimal": "dec", + "declaration": "decl", + "declare": "decl", + "decode": "dec", + "decrement": "dec", + "define": "def", + "definition": "def", + "degree": "deg", + "delete": "del", + "depend": "dep", + "depends": "deps", + "description": "desc", + "dest": "dst", + "destination": "dest", + "develop": "dev", + "development": "dev", + "device": "dev", + "diagnostic": "diag", + "dictation": "dict", + "dictionary": "dict", + "direction": "dir", + "directories": "dirs", + "directory": "dir", + "display": "disp", + "distance": "dist", + "distribution": "dist", + "document": "doc", + "documents": "docs", + "doing": "ing", # some way to add 'ing' to verbs + "double ended queue": "deque", + "double": "dbl", + "dupe": "dup", + "duplicate": "dup", + "dynamic": "dyn", + "elastic": "elast", + "element": "elem", + "elements": "elems", + "encode": "enc", + "end of day": "eod", + "end of month": "eom", + "end of quarter": "eoq", + "end of week": "eow", + "end of year": "eoy", + "entry": "ent", + "enumerate": "enum", + "environment": "env", + "error": "err", + "escape": "esc", + "etcetera": "etc", + "ethernet": "eth", + "evaluate": "eval", + "example": "ex", + "exception": "exc", + "executable": "exe", + "executables": "exes", + "execute": "exec", + "experience": "exp", + "exponent": "exp", + "expression": "expr", + "expressions": "exprs", + "extend": "ext", + "extension": "ext", + "external": "extern", + "eye dent": "id", + "eye octal": "ioctl", + "eye three": "i3", + "feature": "feat", + "file system": "fs", + "fingerprint": "fp", + "for what": "fwiw", + "format": "fmt", + "fortigate": "fgt", + "framework": "fw", + "frequency": "freq", + "function": "func", + "functions": "funcs", + "funny": "lol", + "fuzzy": "fzy", + "generate": "gen", + "generic": "gen", + "hardware": "hw", + "header": "hdr", + "hello": "helo", + "history": "hist", + "hypertext": "http", + "identity": "id", + "ignore": "ign", + "image": "img", + "implement": "impl", + "import address table": "iat", + "import table": "iat", + "in real life": "irl", + "increment": "inc", + "index": "idx", + "information": "info", + "infrastructure": "infra", + "initialize": "init", + "initializer": "init", + "inode": "ino", + "insert": "ins", + "instance": "inst", + "instruction": "insn", + "integer": "int", + "interpreter": "interp", + "interrupt": "int", + "iterate": "iter", + "jason": "json", + "jason five": "json5", + "java archive": "jar", + "javascript": "js", + "jiff": "gif", + "journal cuttle": "journalctl", + "jump": "jmp", + "just in time": "jit", + "kay": "kk", + "kernel": "krnl", + "key cuttle": "keyctl", + "keyboard": "kbd", + "keyword arguments": "kwargs", + "keyword": "kw", + "kilogram": "kg", + "kilometer": "km", + "language": "lang", + "laugh out loud": "lol", + "length": "len", + "lib see": "libc", + "library": "lib", + "lisp": "lsp", + "looks good to me": "lgtm", + "mail": "smtp", + "make": "mk", + "management": "mgmt", + "manager": "mgr", + "manitoba": "mb", + "markdown": "md", + "maximum": "max", + "memory": "mem", + "message": "msg", + "meta sploit framework": "msf", + "meta sploit": "msf", + "microphone": "mic", + "middle": "mid", + "milligram": "mg", + "millisecond": "ms", + "minimum viable product": "mvp", + "minimum": "min", + "miscellaneous": "misc", + "modify": "mod", + "module": "mod", + "modules": "mods", + "monitor": "mon", + "mount": "mnt", + "multiple": "multi", + "muscle": "musl", + "mutate": "mut", + "nano second": "ns", + "neo vim": "nvim", + "new brunswick": "nb", + "nova scotia": "ns", + "number": "num", + "numbers": "nums", + "object": "obj", + "objects": "objs", + "offset": "off", + "offsets": "offs", + "okay": "ok", + "ontario": "on", + "operating system": "os", + "operation": "op", + "operations": "ops", + "option": "opt", + "options": "opts", + "original": "orig", + "out of bounds": "oob", + "package build": "pkgbuild", + "package": "pkg", + "packages": "pkgs", + "packet": "pkt", + "packets": "pkts", + "parameter": "param", + "parameters": "params", + "password": "passwd", + "performance": "perf", + "physical": "phys", + "physical address": "paddr", + "pick": "pic", + "pico second": "ps", + "pie": "py", + "ping": "png", + "pixel": "px", + "point": "pt", + "pointer": "ptr", + "pointers": "ptrs", + "pone": "pwn", + "position independent code": "pic", + "position independent executable": "pie", + "position": "pos", + "pound bag": "pwndbg", + "preference": "pref", + "preferences": "prefs", + "previous": "prev", + "private": "priv", + "process": "proc", + "processor": "cpu", + "production": "prod", + "program": "prog", + "programs": "progs", + "properties": "props", + "property": "prop", + "protocol": "proto", + "protocol buffers": "protobuf", + "public": "pub", + "python": "py", + "quebec": "qc", + "query string": "qs", + "radian": "rad", + "random": "rand", + "read right ex": "rwx", + "receipt": "rcpt", + "receive": "recv", + "record": "rec", + "recording": "rec", + "rectangle": "rect", + "ref count": "refcnt", + "reference": "ref", + "references": "refs", + "register": "reg", + "registers": "regs", + "registery": "reg", + "regular expression": "regex", + "regular expressions": "regex", + "remove": "rm", + "repel": "repl", + "repetitive strain injury": "rsi", + "repository": "repo", + "represent": "repr", + "representation": "repr", + "request": "req", + "requests": "reqs", + "resources": "rsrcs", + "response": "resp", + "result": "res", + "return": "ret", + "revision": "rev", + "round": "rnd", + "ruby": "rb", + "rust": "rs", + "samba D": "smbd", + "samba": "smb", + "saskatchewan": "sk", + "schedule": "sched", + "scheduler": "sched", + "screen": "scr", + "scuzzy": "scsi", + "see": "C", + "segment": "seg", + "select": "sel", + "semaphore": "sem", + "send": "snd", + "sequel": "sql", + "sequence": "seq", + "service pack": "sp", + "session id": "sid", + "shell": "sh", + "shellcode": "sc", + "signal": "sig", + "size": "sz", + "snipped": "[...]", + "some": "sum", + "source": "src", + "sources": "srcs", + "special": "spec", + "specific": "spec", + "specification": "spec", + "specify": "spec", + "standard error": "stderr", + "standard in": "stdin", + "standard out": "stdout", + "standard": "std", + "start of day": "sod", + "start of month": "som", + "start of quarter": "soq", + "start of week": "sow", + "start of year": "soy", + "statement": "stmt", + "statistic": "stat", + "statistics": "stats", + "string": "str", + "structure": "struct", + "structures": "structs", + "symbol": "sym", + "symbolic link": "symlink", + "symbols": "syms", + "synchronize": "sync", + "synchronous": "sync", + "sys cuttle": "sysctl", + "system call": "syscall", + "system cuddle": "systemctl", + "system": "sys", + "table of contents": "toc", + "table": "tbl", + "taiwan": "tw", + "talk": "toc", + "technology": "tech", + "temp": "tmp", + "temperature": "temp", + "temporary": "tmp", + "terminal": "term", + "text": "txt", + "time format": "hh:mm:ss", + "time of check time of use": "toctou", + "time to live": "ttl", + "token": "tok", + "transaction": "txn", + "typescript": "ts", + "ultimate": "ulti", + "unique id": "uuid", + "unknown": "unk", + "user id": "uid", + "user": "usr", + "utilities": "utils", + "utility": "util", + "value": "val", + "values": "vals", + "variable": "var", + "variables": "vars", + "vector": "vec", + "verify": "vrfy", + "version": "ver", + "versus": "vs", + "video": "vid", + "videos": "vids", + "virtual machine": "vm", + "virtual": "virt", + "virtual address": "vaddr", + "visual studio": "msvc", + "visual": "vis", + "volume": "vol", + "vulnerable": "vuln", + "wave": "wav", + "web": "www", + "what the fuck": "wtf", + "wind": "wnd", + "window": "win", + "windows kernel": "ntoskrnl", + "work in progress": "wip", +} + + +@mod.capture(rule="brief {user.abbreviation}") +def abbreviation(m) -> str: + return m.abbreviation + + +@track_csv_list( + "abbreviations.csv", headers=("Abbreviation", "Spoken Form"), default=abbreviations +) +def on_abbreviations(values): + global abbreviations_list + + # note: abbreviations_list is imported by the create_spoken_forms module + abbreviations_list = values + + # Matches letters and spaces, as currently, Talon doesn't accept other characters in spoken forms. + PATTERN = re.compile(r"^[a-zA-Z ]+$") + abbreviation_values = { + v: v for v in abbreviations_list.values() if PATTERN.match(v) is not None + } + + # Allows the abbreviated/short form to be used as spoken phrase. eg "brief app" -> app + abbreviations_list_with_values = { + **{v: v for v in abbreviation_values.values()}, + **abbreviations_list, + } + + ctx.lists["user.abbreviation"] = abbreviations_list_with_values diff --git a/community/core/app_running.py b/community/core/app_running.py new file mode 100644 index 0000000..de2b4e7 --- /dev/null +++ b/community/core/app_running.py @@ -0,0 +1,12 @@ +from talon import Module, ui + +mod = Module() + + +@mod.scope +def scope(): + return {"running": {app.name.lower() for app in ui.apps()}} + + +ui.register("app_launch", scope.update) +ui.register("app_close", scope.update) diff --git a/community/core/app_switcher/app_name_overrides.linux.csv b/community/core/app_switcher/app_name_overrides.linux.csv new file mode 100644 index 0000000..d73a8f3 --- /dev/null +++ b/community/core/app_switcher/app_name_overrides.linux.csv @@ -0,0 +1,4 @@ +Spoken form, App name (or list an app name by itself on a line to exclude it) +grip, DataGrip +py, jetbrains-pycharm-ce +terminal, Gnome-terminal diff --git a/community/core/app_switcher/app_name_overrides.mac.csv b/community/core/app_switcher/app_name_overrides.mac.csv new file mode 100644 index 0000000..1e5907a --- /dev/null +++ b/community/core/app_switcher/app_name_overrides.mac.csv @@ -0,0 +1,4 @@ +Spoken form, App name (or list an app name by itself on a line to exclude it) +grip, DataGrip +term, iTerm2 +one note, ONENOTE diff --git a/community/core/app_switcher/app_name_overrides.windows.csv b/community/core/app_switcher/app_name_overrides.windows.csv new file mode 100644 index 0000000..f33a582 --- /dev/null +++ b/community/core/app_switcher/app_name_overrides.windows.csv @@ -0,0 +1,9 @@ +Spoken form, App name/.exe (or list an app name/.exe by itself on a line to exclude it) +grip, DataGrip +term, WindowsTerminal.exe +one note, ONENOTE +lock, slack.exe +app, slack.exe +lockapp, slack.exe +pycharm, pycharm64.exe +webstorm, webstorm64.exe diff --git a/community/core/app_switcher/app_switcher.py b/community/core/app_switcher/app_switcher.py new file mode 100644 index 0000000..7c81595 --- /dev/null +++ b/community/core/app_switcher/app_switcher.py @@ -0,0 +1,461 @@ +import os +import shlex +import subprocess +import time +from pathlib import Path + +import talon +from talon import Context, Module, actions, app, fs, imgui, ui + +# Construct a list of spoken form overrides for application names (similar to how homophone list is managed) +# These overrides are used *instead* of the generated spoken forms for the given app name or .exe (on Windows) +# CSV files contain lines of the form: +# , - to add a spoken form override for the app, or +# - to exclude the app from appearing in "running list" or "focus " + +# TODO: Consider moving overrides to settings directory +overrides_directory = os.path.dirname(os.path.realpath(__file__)) +override_file_name = f"app_name_overrides.{talon.app.platform}.csv" +override_file_path = os.path.normcase( + os.path.join(overrides_directory, override_file_name) +) + +mod = Module() +mod.list("running", desc="all running applications") +mod.list("launch", desc="all launchable applications") +ctx = Context() + +# a list of the current overrides +overrides = {} + +# apps to exclude from running list +excludes = set() + +# a list of the currently running application names +running_application_dict = {} + + +words_to_exclude = [ + "zero", + "one", + "two", + "three", + "for", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "and", + "dot", + "exe", + "help", + "install", + "installer", + "microsoft", + "nine", + "readme", + "studio", + "terminal", + "visual", + "windows", +] + +# on Windows, WindowsApps are not like normal applications, so +# we use the shell:AppsFolder to populate the list of applications +# rather than via e.g. the start menu. This way, all apps, including "modern" apps are +# launchable. To easily retrieve the apps this makes available, navigate to shell:AppsFolder in Explorer +if app.platform == "windows": + import ctypes + import os + from ctypes import wintypes + + import pywintypes + from win32com.propsys import propsys, pscon + from win32com.shell import shell, shellcon + + # KNOWNFOLDERID + # https://msdn.microsoft.com/en-us/library/dd378457 + # win32com defines most of these, except the ones added in Windows 8. + FOLDERID_AppsFolder = pywintypes.IID("{1e87508d-89c2-42f0-8a7e-645a0f50ca58}") + + # win32com is missing SHGetKnownFolderIDList, so use ctypes. + + _ole32 = ctypes.OleDLL("ole32") + _shell32 = ctypes.OleDLL("shell32") + + _REFKNOWNFOLDERID = ctypes.c_char_p + _PPITEMIDLIST = ctypes.POINTER(ctypes.c_void_p) + + _ole32.CoTaskMemFree.restype = None + _ole32.CoTaskMemFree.argtypes = (wintypes.LPVOID,) + + _shell32.SHGetKnownFolderIDList.argtypes = ( + _REFKNOWNFOLDERID, # rfid + wintypes.DWORD, # dwFlags + wintypes.HANDLE, # hToken + _PPITEMIDLIST, + ) # ppidl + + def get_known_folder_id_list(folder_id, htoken=None): + if isinstance(folder_id, pywintypes.IIDType): + folder_id = bytes(folder_id) + pidl = ctypes.c_void_p() + try: + _shell32.SHGetKnownFolderIDList(folder_id, 0, htoken, ctypes.byref(pidl)) + return shell.AddressAsPIDL(pidl.value) + except OSError as e: + if e.winerror & 0x80070000 == 0x80070000: + # It's a WinAPI error, so re-raise it, letting Python + # raise a specific exception such as FileNotFoundError. + raise ctypes.WinError(e.winerror & 0x0000FFFF) + raise + finally: + if pidl: + _ole32.CoTaskMemFree(pidl) + + def enum_known_folder(folder_id, htoken=None): + id_list = get_known_folder_id_list(folder_id, htoken) + folder_shell_item = shell.SHCreateShellItem(None, None, id_list) + items_enum = folder_shell_item.BindToHandler( + None, shell.BHID_EnumItems, shell.IID_IEnumShellItems + ) + yield from items_enum + + def list_known_folder(folder_id, htoken=None): + result = [] + for item in enum_known_folder(folder_id, htoken): + result.append(item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY)) + result.sort(key=lambda x: x.upper()) + return result + + def get_apps(): + items = {} + for item in enum_known_folder(FOLDERID_AppsFolder): + try: + property_store = item.BindToHandler( + None, shell.BHID_PropertyStore, propsys.IID_IPropertyStore + ) + app_user_model_id = property_store.GetValue( + pscon.PKEY_AppUserModel_ID + ).ToString() + + except pywintypes.error: + continue + + name = item.GetDisplayName(shellcon.SIGDN_NORMALDISPLAY) + + # exclude anything with install/uninstall... + # 'cause I don't think we don't want 'em + if "install" not in name.lower(): + items[name] = app_user_model_id + + return items + +elif app.platform == "linux": + import configparser + import re + + 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]") + for base in linux_application_directories: + if os.path.isdir(base): + for entry in os.scandir(base): + if entry.name.endswith(".desktop"): + try: + config = configparser.ConfigParser(interpolation=None) + config.read(entry.path) + # only parse shortcuts that are not hidden + 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 + if exec_key[0] == '"' and exec_key[-1] == '"': + exec_key = re.sub('"', "", exec_key) + # remove field codes and add full path if necessary + if exec_key[0] == "/": + items[name_key] = re.sub(args_pattern, "", exec_key) + else: + 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 Exception: + print( + "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: + "Returns a single application name" + try: + return m.running + except AttributeError: + return m.text + + +@mod.capture(rule="{self.launch}") +def launch_applications(m) -> str: + "Returns a single application name" + return m.launch + + +def update_running_list(): + global running_application_dict + running_application_dict = {} + running = {} + foreground_apps = ui.apps(background=False) + + for cur_app in foreground_apps: + running_application_dict[cur_app.name.lower()] = cur_app.name + + if app.platform == "windows": + exe = os.path.basename(cur_app.exe) + running_application_dict[exe.lower()] = exe + + override_apps = excludes.union(overrides.values()) + + running = actions.user.create_spoken_forms_from_list( + [ + curr_app.name + for curr_app in ui.apps(background=False) + if curr_app.name.lower() not in override_apps + and curr_app.exe.lower() not in override_apps + and os.path.basename(curr_app.exe).lower() not in override_apps + ], + words_to_exclude=words_to_exclude, + generate_subsequences=True, + ) + + for running_name, full_application_name in overrides.items(): + if running_app_name := running_application_dict.get(full_application_name): + running[running_name] = running_app_name + + ctx.lists["self.running"] = running + + +def update_overrides(name, flags): + """Updates the overrides and excludes lists""" + global overrides, excludes + + if name is None or os.path.normcase(name) == override_file_path: + overrides = {} + excludes = set() + + # print("update_overrides") + with open(override_file_path) as f: + for line in f: + line = line.rstrip().lower() + line = line.split(",") + if len(line) == 2 and line[0] != "Spoken form": + overrides[line[0]] = line[1].strip() + if len(line) == 1: + excludes.add(line[0].strip()) + + update_running_list() + + +@mod.action_class +class Actions: + def get_running_app(name: str) -> ui.App: + """Get the first available running app with `name`.""" + # We should use the capture result directly if it's already in the list + # of running applications. Otherwise, name is from and we + # can be a bit fuzzier + if name.lower() not in running_application_dict: + if len(name) < 3: + raise RuntimeError( + f'Skipped getting app: "{name}" has less than 3 chars.' + ) + for running_name, full_application_name in ctx.lists[ + "self.running" + ].items(): + if running_name == name or running_name.lower().startswith( + name.lower() + ): + name = full_application_name + break + for application in ui.apps(background=False): + if application.name == name or ( + app.platform == "windows" + and os.path.basename(application.exe).lower() == name + ): + return application + raise RuntimeError(f'App not running: "{name}"') + + def switcher_focus(name: str): + """Focus a new application by name""" + app = actions.user.get_running_app(name) + + # Focus next window on same app + if app == ui.active_app(): + actions.app.window_next() + # Focus new app + else: + actions.user.switcher_focus_app(app) + + def switcher_focus_app(app: ui.App): + """Focus application and wait until switch is made""" + app.focus() + t1 = time.perf_counter() + while ui.active_app() != app: + if time.perf_counter() - t1 > 1: + raise RuntimeError(f"Can't focus app: {app.name}") + actions.sleep(0.1) + + def switcher_focus_last(): + """Focus last window/application""" + + def switcher_focus_window(window: ui.Window): + """Focus window and wait until switch is made""" + window.focus() + t1 = time.perf_counter() + while ui.active_window() != window: + if time.perf_counter() - t1 > 1: + raise RuntimeError(f"Can't focus window: {window.title}") + actions.sleep(0.1) + + def switcher_launch(path: str): + """Launch a new application by path (all OSes), or AppUserModel_ID path on Windows""" + if app.platform == "mac": + ui.launch(path=path) + elif app.platform == "linux": + # Could potentially be merged with OSX code. Done in this explicit + # way for expediency around the 0.4 release. + cmd = shlex.split(path)[0] + args = shlex.split(path)[1:] + ui.launch(path=cmd, args=args) + elif app.platform == "windows": + is_valid_path = False + try: + current_path = Path(path) + is_valid_path = current_path.is_file() + except: + is_valid_path = False + if is_valid_path: + ui.launch(path=path) + else: + cmd = f"explorer.exe shell:AppsFolder\\{path}" + subprocess.Popen(cmd, shell=False) + else: + print("Unhandled platform in switcher_launch: " + app.platform) + + 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) + + def switcher_toggle_running(): + """Shows/hides all running applications""" + if gui_running.showing: + gui_running.hide() + else: + gui_running.show() + + def switcher_hide_running(): + """Hides list of running applications""" + gui_running.hide() + + +@imgui.open() +def gui_running(gui: imgui.GUI): + gui.text("Running applications (with spoken forms)") + gui.line() + running_apps = sorted( + (v.lower(), k, v) for k, v in ctx.lists["self.running"].items() + ) + for _, running_name, full_application_name in running_apps: + gui.text(f"{full_application_name}: {running_name}") + + gui.spacer() + if gui.button("Running close"): + actions.user.switcher_hide_running() + + +def update_launch_list(): + launch = get_apps() + + # actions.user.talon_pretty_print(launch) + + ctx.lists["self.launch"] = actions.user.create_spoken_forms_from_map( + launch, words_to_exclude + ) + + +def ui_event(event, arg): + if event in ("app_launch", "app_close"): + update_running_list() + + +# Talon starts faster if you don't use the `talon.ui` module during launch + + +def on_ready(): + update_overrides(None, None) + fs.watch(overrides_directory, update_overrides) + update_launch_list() + update_running_list() + ui.register("", ui_event) + + +app.register("ready", on_ready) diff --git a/community/core/application_matches.py b/community/core/application_matches.py new file mode 100644 index 0000000..9c524b6 --- /dev/null +++ b/community/core/application_matches.py @@ -0,0 +1,76 @@ +from talon import Module + +mod = Module() + + +apps = mod.apps + +# apple specific apps +apps.datagrip = """ +os: mac +and app.name: DataGrip +""" + +apps.finder = """ +os: mac +and app.bundle: com.apple.finder +""" + +apps.rstudio = """ +os: mac +and app.name: RStudio +""" + +apps.apple_terminal = """ +os: mac +and app.bundle: com.apple.Terminal +""" + +# linux specific apps +apps.keepass = """ +os: linux +and app.name: KeePassX2 +os: linux +and app.name: KeePassXC +os: linux +and app.name: KeepassX2 +os: linux +and app.name: keepassx2 +os: linux +and app.name: keepassxc +os: linux +and app.name: Keepassxc""" + +apps.signal = """ +os: linux +and app.name: Signal + +os: linux +and app.name: signal +""" + +apps.termite = """ +os: linux +and app.name: /termite/ +""" + +apps.windows_command_processor = r""" +os: windows +and app.name: Windows Command Processor +os: windows +and app.exe: /^cmd\.exe$/i +""" + +apps.windows_terminal = r""" +os: windows +and app.exe: /^windowsterminal\.exe$/i +""" + +mod.apps.windows_power_shell = r""" +os: windows +and app.exe: /^powershell\.exe$/i +""" + +apps.vim = """ +win.title:/VIM/ +""" diff --git a/community/core/command_client/README.md b/community/core/command_client/README.md new file mode 100644 index 0000000..9d3108f --- /dev/null +++ b/community/core/command_client/README.md @@ -0,0 +1,16 @@ +# 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/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=core/command_client --annotate="[split] " -b split +git push git@github.com:cursorless-dev/talon-command-client.git split:main +``` diff --git a/community/core/command_client/command_client.py b/community/core/command_client/command_client.py new file mode 100644 index 0000000..324e900 --- /dev/null +++ b/community/core/command_client/command_client.py @@ -0,0 +1,199 @@ +from pathlib import Path +from typing import Any + +from talon import Context, Module, actions, speech_system + +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 +did_emit_pre_phrase_signal = False + +mod = Module() +ctx = Context() +mac_ctx = Context() + +ctx.matches = r""" +tag: user.command_client +""" +mac_ctx.matches = r""" +os: mac +tag: user.command_client +""" + + +class NotSet: + def __repr__(self): + return "" + + +def run_command( + command_id: str, + *args, + wait_for_finish: bool = False, + return_command_output: bool = False, +): + """Runs a command, using command server if available + + Args: + 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. + """ + # NB: This is a hack to work around the fact that talon doesn't support + # variable argument lists + args = [x for x in args if x is not NotSet] + + 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, + ) + + +@mod.action_class +class Actions: + def run_rpc_command( + command_id: str, + arg1: Any = NotSet, + arg2: Any = NotSet, + arg3: Any = NotSet, + arg4: Any = NotSet, + arg5: Any = NotSet, + ): + """Execute command via RPC.""" + run_command( + command_id, + arg1, + arg2, + arg3, + arg4, + arg5, + ) + + def run_rpc_command_and_wait( + command_id: str, + arg1: Any = NotSet, + arg2: Any = NotSet, + arg3: Any = NotSet, + arg4: Any = NotSet, + arg5: Any = NotSet, + ): + """Execute command via application command server and wait for command to finish.""" + run_command( + command_id, + arg1, + arg2, + arg3, + arg4, + arg5, + wait_for_finish=True, + ) + + def run_rpc_command_get( + command_id: str, + arg1: Any = NotSet, + arg2: Any = NotSet, + arg3: Any = NotSet, + arg4: Any = NotSet, + arg5: Any = NotSet, + ) -> Any: + """Execute command via application command server and return command output.""" + return run_command( + command_id, + arg1, + arg2, + arg3, + arg4, + arg5, + return_command_output=True, + ) + + def command_server_directory() -> str: + """Return the directory of the command server""" + + def trigger_command_server_command_execution(): + """Issue keystroke to trigger command server to execute command that + was written to the file. For internal use only""" + actions.key("ctrl-shift-f17") + + def emit_pre_phrase_signal() -> bool: + """ + If in an application supporting the command client, returns True + and touches a file to indicate that a phrase is beginning execution. + Otherwise does nothing and returns False. + """ + return False + + def did_emit_pre_phrase_signal() -> bool: + """Indicates whether the pre-phrase signal was emitted at the start of this phrase""" + # NB: This action is used by cursorless; please don't delete it :) + return did_emit_pre_phrase_signal + + +@mac_ctx.action_class("user") +class MacUserActions: + def trigger_command_server_command_execution(): + actions.key("cmd-shift-f17") + + +@ctx.action_class("user") +class UserActions: + def emit_pre_phrase_signal(): + get_signal_path("prePhrase").touch() + return True + + +class MissingCommunicationDir(Exception): + pass + + +def get_signal_path(name: str) -> Path: + """ + Get the path to a signal in the signal subdirectory. + + Args: + name (str): The name of the signal + + Returns: + Path: The signal 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() + + signal_dir = communication_dir_path / "signals" + signal_dir.mkdir(parents=True, exist_ok=True) + + return signal_dir / name + + +def pre_phrase(_: Any): + try: + global did_emit_pre_phrase_signal + + did_emit_pre_phrase_signal = actions.user.emit_pre_phrase_signal() + except MissingCommunicationDir: + pass + + +def post_phrase(_: Any): + global did_emit_pre_phrase_signal + did_emit_pre_phrase_signal = False + + +speech_system.register("pre:phrase", pre_phrase) +speech_system.register("post:phrase", post_phrase) diff --git a/community/core/command_client/command_client_tag.py b/community/core/command_client/command_client_tag.py new file mode 100644 index 0000000..28e6d53 --- /dev/null +++ b/community/core/command_client/command_client_tag.py @@ -0,0 +1,20 @@ +from talon import Module + +mod = Module() + +mod.tag( + "command_client", desc="For applications which implement file-based RPC with Talon" +) + + +@mod.action_class +class Actions: + def command_server_directory() -> str: + """ + The dirctory which contains the files required for communication between + the application and Talon. This is the only function which absolutely + must be implemented for any application using the command-client. Each + application that supports file-based RPC should use its own directory + name. Note that this action should only return a name; the parent + directory is determined by the core command client code. + """ diff --git a/community/core/command_client/rpc_client/get_communication_dir_path.py b/community/core/command_client/rpc_client/get_communication_dir_path.py new file mode 100644 index 0000000..5a52b9d --- /dev/null +++ b/community/core/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/community/core/command_client/rpc_client/read_json_with_timeout.py b/community/core/command_client/rpc_client/read_json_with_timeout.py new file mode 100644 index 0000000..4413f64 --- /dev/null +++ b/community/core/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/community/core/command_client/rpc_client/robust_unlink.py b/community/core/command_client/rpc_client/robust_unlink.py new file mode 100644 index 0000000..293ddfd --- /dev/null +++ b/community/core/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/community/core/command_client/rpc_client/rpc_client.py b/community/core/command_client/rpc_client/rpc_client.py new file mode 100644 index 0000000..31917d1 --- /dev/null +++ b/community/core/command_client/rpc_client/rpc_client.py @@ -0,0 +1,106 @@ +import logging +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 + +logger = logging.getLogger(__name__) + +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(): + logger.warning( + f"Communication directory not found at: {communication_dir_path}" + ) + if args or return_command_output: + raise Exception( + "Communication directory not found. 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/community/core/command_client/rpc_client/types.py b/community/core/command_client/rpc_client/types.py new file mode 100644 index 0000000..f84c5cc --- /dev/null +++ b/community/core/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/community/core/command_client/rpc_client/write_request.py b/community/core/command_client/rpc_client/write_request.py new file mode 100644 index 0000000..74b4645 --- /dev/null +++ b/community/core/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/community/core/contacts/README.md b/community/core/contacts/README.md new file mode 100644 index 0000000..b9e29ef --- /dev/null +++ b/community/core/contacts/README.md @@ -0,0 +1,63 @@ +# Contacts + +This directory provides a versatile `` capture that can be +used to insert names and email addresses using a suffix. This functionality is +exposed through other captures such as `` and ``, not +directly as commands. The contact list may be provided in the private directory +via the `contacts.json` file, the `contacts.csv` file, or both. + +Here is an example contacts.json: + +```json +[ + { + "email": "john.doe@example.com", + "full_name": "Jonathan Doh: Jonathan Doe", + "nicknames": ["Jon", "Jah Nee: Jonny"] + } +] +``` + +Note that for either full_name or nicknames, pronunciation can be provided via +the standard Talon list format of "[pronunciation]: [name]". Pronunciation for +the first name is automatically extracted from pronunciation for the full name, +if there are the same number of name parts in each. Pronunciation can be +overridden for the first name by adding a nickname with matching written form. + +To refer to this contact, you could say: + +- Jonathan Doh email -> john.doe@example.com +- Jonathan email -> john.doe@example.com +- Jah Nee email -> john.doe@example.com +- Jah Nee name -> Jonny +- Jonathan Doh name -> Jonathan Doe +- Jon last name -> Doe +- Jon full name -> Jonathan Doe +- Jon names -> Jon's +- Jon full names -> Jonathan Doe's + +The CSV format provides only email and full name functionality: + +```csv +Name,Email +John Doe,jon.doe@example.com +Jane Doe,jane.doe@example.com +``` + +The advantage of the CSV format is that it is easily exported. If both the CSV +and JSON are present, they will be merged based on email addresses. This makes +it easy to use an exported CSV and maintain nicknames in the JSON. For example, +to export from Gmail, go to https://contacts.google.com/, then click "Frequently +contacted", then "Export". Then run: + +```bash +cat contacts.csv | python -c "import csv; import sys; w=csv.writer(sys.stdout); [w.writerow([row['First Name'] + ' ' + row['Last Name'], row['E-mail 1 - Value']]) for row in csv.DictReader(sys.stdin)]" +``` + +In case of name conflicts (e.g. two people named John), the first instance will +be preferred, with all JSON contacts taking precedence over CSV. If you wish to +refer to both, use the pronunciation to differentiate, using a nickname to +override the first name pronunciation if desired. For example, you might add +"John S: John" and "John D: John" as nicknames to the two different Johns. This +is also an effective way to handle name homophones such as John and Jon, which +would otherwise be resolved arbitrarily by the speech engine. diff --git a/community/core/contacts/contacts.py b/community/core/contacts/contacts.py new file mode 100644 index 0000000..a852578 --- /dev/null +++ b/community/core/contacts/contacts.py @@ -0,0 +1,293 @@ +import json +import logging +from dataclasses import dataclass + +from talon import Context, Module + +from ..user_settings import track_csv_list, track_file + +mod = Module() +ctx = Context() + +mod.list("contact_names", desc="Contact first names, full names, and nicknames.") +mod.list("contact_emails", desc="Maps names to email addresses.") +mod.list("contact_full_names", desc="Maps names to full names.") + + +@dataclass +class Contact: + email: str + full_name: str + nicknames: list[str] + pronunciations: dict[str, str] + + @classmethod + def from_json(cls, contact): + email = contact.get("email") + if not email: + logging.error(f"Skipping contact missing email: {contact}") + return None + + # Handle full name with potential pronunciation + full_name_raw = contact.get("full_name", "") + pronunciations = {} + if ":" in full_name_raw: + pronunciation, full_name = [x.strip() for x in full_name_raw.split(":", 1)] + if ( + full_name in pronunciations + and pronunciations[full_name] != pronunciation + ): + logging.info( + f"Multiple pronunciations found for '{full_name}'; using '{pronunciation}'" + ) + pronunciations[full_name] = pronunciation + + # Add pronunciation for each component of the name. + pron_parts = pronunciation.split() + name_parts = full_name.split() + if len(pron_parts) == len(name_parts): + for pron, name in zip(pron_parts, name_parts): + if name in pronunciations and pronunciations[name] != pron: + logging.info( + f"Multiple different pronunciations found for '{name}' in " + f"{full_name_raw}; using '{pron}'" + ) + pronunciations[name] = pron + else: + logging.info( + f"Pronunciation parts don't match name parts for '{full_name_raw}; skipping them.'" + ) + else: + full_name = full_name_raw + + # Handle nicknames with potential pronunciations + nicknames = [] + for nickname_raw in contact.get("nicknames", []): + if ":" in nickname_raw: + pronunciation, nickname = [ + x.strip() for x in nickname_raw.split(":", 1) + ] + if ( + nickname in pronunciations + and pronunciations[nickname] != pronunciation + ): + logging.info( + f"Multiple different pronunciations found for '{nickname}' in " + f"contact {email}; using '{pronunciation}'" + ) + pronunciations[nickname] = pronunciation + nicknames.append(nickname) + else: + nicknames.append(nickname_raw) + + return Contact( + email=email, + full_name=full_name, + nicknames=nicknames, + pronunciations=pronunciations, + ) + + +csv_contacts: list[Contact] = [] +json_contacts: list[Contact] = [] + + +@track_csv_list("contacts.csv", headers=("Name", "Email"), default={}, private=True) +def on_contacts_csv(values): + global csv_contacts + csv_contacts = [] + for email, full_name in values.items(): + if not email: + logging.error(f"Skipping contact missing email: {full_name}") + continue + csv_contacts.append( + Contact(email=email, full_name=full_name, nicknames=[], pronunciations={}) + ) + reload_contacts() + + +@track_file("contacts.json", default="[]", private=True) +def on_contacts_json(f): + global json_contacts + try: + contacts = json.load(f) + except Exception: + logging.exception("Error parsing contacts.json") + return + + json_contacts = [] + for contact in contacts: + try: + parsed_contact = Contact.from_json(contact) + if parsed_contact: + json_contacts.append(parsed_contact) + except Exception: + logging.exception(f"Error parsing contact: {contact}") + reload_contacts() + + +def create_pronunciation_to_name_map(contact): + result = {} + if contact.full_name: + result[contact.pronunciations.get(contact.full_name, contact.full_name)] = ( + contact.full_name + ) + # Add pronunciation mapping for first name only + first_name = contact.full_name.split()[0] + result[contact.pronunciations.get(first_name, first_name)] = first_name + for nickname in contact.nicknames: + result[contact.pronunciations.get(nickname, nickname)] = nickname + return result + + +def reload_contacts(): + csv_by_email = {contact.email: contact for contact in csv_contacts} + json_by_email = {contact.email: contact for contact in json_contacts} + # Merge the CSV and JSON contacts. Maintain order of contacts with JSON first. + merged_contacts = [] + for email in json_by_email | csv_by_email: + csv_contact = csv_by_email.get(email) + json_contact = json_by_email.get(email) + + if csv_contact and json_contact: + # Prefer JSON data but use CSV name if JSON name is empty + full_name = json_contact.full_name or csv_contact.full_name + merged_contacts.append( + Contact( + email=email, + full_name=full_name, + nicknames=json_contact.nicknames, + pronunciations=json_contact.pronunciations, + ) + ) + else: + # Use whichever contact exists + merged_contacts.append(json_contact or csv_contact) + + contact_names = {} + contact_emails = {} + contact_full_names = {} + # Iterate in reverse so that the first contact with a name is used. + for contact in reversed(merged_contacts): + pronunciation_map = create_pronunciation_to_name_map(contact) + for pronunciation, name in pronunciation_map.items(): + contact_names[pronunciation] = name + contact_emails[pronunciation] = contact.email + if contact.full_name: + contact_full_names[pronunciation] = contact.full_name + + ctx.lists["user.contact_names"] = contact_names + ctx.lists["user.contact_emails"] = contact_emails + ctx.lists["user.contact_full_names"] = contact_full_names + + +def first_name_from_full_name(full_name: str): + return full_name.split(" ")[0] + + +def last_name_from_full_name(full_name: str): + return full_name.split(" ")[-1] + + +def username_from_email(email: str): + return email.split("@")[0] + + +def make_name_possessive(name: str): + return f"{name}'s" + + +@mod.capture( + rule="{user.contact_names} name", +) +def prose_name(m) -> str: + return m.contact_names + + +@mod.capture( + rule="{user.contact_names} names", +) +def prose_name_possessive(m) -> str: + return make_name_possessive(m.contact_names) + + +@mod.capture( + rule="{user.contact_emails} email [address]", +) +def prose_email(m) -> str: + return m.contact_emails + + +@mod.capture( + rule="{user.contact_emails} (username | L dap)", +) +def prose_username(m) -> str: + return username_from_email(m.contact_emails) + + +@mod.capture( + rule="{user.contact_full_names} full name", +) +def prose_full_name(m) -> str: + return m.contact_full_names + + +@mod.capture( + rule="{user.contact_full_names} full names", +) +def prose_full_name_possessive(m) -> str: + return make_name_possessive(m.contact_full_names) + + +@mod.capture( + rule="{user.contact_full_names} first name", +) +def prose_first_name(m) -> str: + return first_name_from_full_name(m.contact_full_names) + + +@mod.capture( + rule="{user.contact_full_names} first names", +) +def prose_first_name_possessive(m) -> str: + return make_name_possessive(first_name_from_full_name(m.contact_full_names)) + + +@mod.capture( + rule="{user.contact_full_names} last name", +) +def prose_last_name(m) -> str: + return last_name_from_full_name(m.contact_full_names) + + +@mod.capture( + rule="{user.contact_full_names} last names", +) +def prose_last_name_possessive(m) -> str: + return make_name_possessive(last_name_from_full_name(m.contact_full_names)) + + +@mod.capture( + rule="(hi | high) {user.contact_names} [name]", +) +def prose_contact_snippet(m) -> str: + return f"hi {m.contact_names}" + + +@mod.capture( + rule=( + " " + "| " + "| " + "| " + "| " + "| " + "| " + "| " + "| " + "| " + "| " + ), +) +def prose_contact(m) -> str: + return m[0] diff --git a/community/core/create_spoken_forms.py b/community/core/create_spoken_forms.py new file mode 100644 index 0000000..36fbd0e --- /dev/null +++ b/community/core/create_spoken_forms.py @@ -0,0 +1,539 @@ +import itertools +import re +from collections import defaultdict +from dataclasses import dataclass +from typing import Any, List, Mapping, Optional + +from talon import Module, actions + +from .keys.symbols import symbols_for_create_spoken_forms +from .numbers.numbers import digits_map, scales, teens, tens +from .user_settings import track_csv_list + +mod = Module() + +DEFAULT_MINIMUM_TERM_LENGTH = 2 +EXPLODE_MAX_LEN = 3 +FANCY_REGULAR_EXPRESSION = r"[A-Z]?[a-z]+|[A-Z]+(?![a-z])|[0-9]+" +SYMBOLS_REGEX = "|".join( + re.escape(symbol) for symbol in set(symbols_for_create_spoken_forms.values()) +) +FILE_EXTENSIONS_REGEX = r"^\b$" +file_extensions = {} + + +def update_regex(): + global REGEX_NO_SYMBOLS + global REGEX_WITH_SYMBOLS + REGEX_NO_SYMBOLS = re.compile( + "|".join( + [ + FANCY_REGULAR_EXPRESSION, + FILE_EXTENSIONS_REGEX, + ] + ) + ) + REGEX_WITH_SYMBOLS = re.compile( + "|".join([FANCY_REGULAR_EXPRESSION, FILE_EXTENSIONS_REGEX, SYMBOLS_REGEX]) + ) + + +update_regex() + + +@track_csv_list("file_extensions.csv", headers=("File extension", "Name")) +def on_extensions(values): + global FILE_EXTENSIONS_REGEX + global file_extensions + file_extensions = values + FILE_EXTENSIONS_REGEX = "|".join( + re.escape(file_extension.strip()) + "$" for file_extension in values.values() + ) + update_regex() + + +abbreviations_list = {} + + +@track_csv_list("abbreviations.csv", headers=("Abbreviation", "Spoken Form")) +def on_abbreviations(values): + global abbreviations_list + abbreviations_list = values + + +REVERSE_PRONUNCIATION_MAP = { + **{str(value): key for key, value in digits_map.items()}, + **{value: key for key, value in symbols_for_create_spoken_forms.items()}, +} + +# begin: create the lists etc necessary for create_spoken_word_for_number +# by convention, each entry in the list has an append space... until I clean up the function +# the algorithm's expectation is slightly different from numbers.py + +# ["", "one ", "two ",... "nine "] or equivalents +ones = [""] + [ + REVERSE_PRONUNCIATION_MAP[str(index)] for index in range(10) if index != 0 +] + +# ["","","twenty","thirty","forty",..."ninety"] +# or equivalent +twenties = ["", ""] + list(tens) +# print("twenties = " + str(twenties)) + +thousands = [""] + [val for index, val in enumerate(scales) if index != 0] +# print("thousands = " + thousands) +# end: create the lists necessary for create_spoken_word_for_number + + +def create_spoken_form_for_number(num: int): + """Creates a spoken form for an integer""" + + n3 = [] + r1 = "" + # create numeric string + ns = str(num) + for k in range(3, 33, 3): + r = ns[-k:] + q = len(ns) - k + # break if end of ns has been reached + if q < -2: + break + else: + if q >= 0: + n3.append(int(r[:3])) + elif q >= -1: + n3.append(int(r[:2])) + elif q >= -2: + n3.append(int(r[:1])) + r1 = r + + # break each group of 3 digits into + # ones, tens/twenties, hundreds + words = [] + for i, x in enumerate(n3): + b1 = x % 10 + b2 = (x % 100) // 10 + b3 = (x % 1000) // 100 + if x == 0: + continue # skip + else: + t = thousands[i] + + # print(str(b1) + ", " + str(b2) + ", " + str(b3)) + if b2 == 0: + words = [ones[b1], t] + words + elif b2 == 1: + words = [teens[b1], t] + words + elif b2 > 1: + words = [twenties[b2], ones[b1], t] + words + if b3 > 0: + words = [ones[b3], scales[0]] + words + + # filter out the empty strings and join + return " ".join(filter(None, words)) + + +def create_spoken_form_years(num: str): + """Creates spoken forms for numbers 1000 <= num <= 9999. Returns None if not supported""" + + val = int(num) + if val > 9999 or val < 1000: + return None + + centuries = val // 100 + remainder = val % 100 + + words = [] + + if centuries % 10 != 0: + words.append(create_spoken_form_for_number(centuries)) + + # 1900 -> nineteen hundred + if remainder == 0: + words.append(scales[0]) + else: + # 200X -> two thousand x + if remainder < 9: + words.append(REVERSE_PRONUNCIATION_MAP[str(centuries // 10)]) + words.append(scales[1]) + + # 20XX => twenty xx + else: + words.append(create_spoken_form_for_number(str(centuries))) + + if remainder != 0: + # 1906 => "nineteen six" + if remainder < 10: + # todo: decide if we want nineteen oh five" + # todo: decide if we want "and" + # both seem like a waste + # if centuries % 10 != 0: + # words.append("oh") + + words.append(REVERSE_PRONUNCIATION_MAP[str(remainder)]) + else: + words.append(create_spoken_form_for_number(remainder)) + + return " ".join(words) + + +# # ---------- create_spoken_form_years (uncomment to run) ---------- +# def test_year(year: str, expected: str): +# result = create_spoken_form_years(year) +# print(f"test_year: test string = {year}, result = {result}, expected = {expected}") +# assert create_spoken_form_years(year) == expected + + +# print("************* test_year tests ******************") +# test_year("1100", "eleven hundred") +# test_year("1905", "nineteen five") +# test_year("1910", "nineteen ten") +# test_year("1925", "nineteen twenty five") +# test_year("2000", "two thousand") +# test_year("2005", "two thousand five") +# test_year("2020", "twenty twenty") +# test_year("2019", "twenty nineteen") +# test_year("2085", "twenty eighty five") +# test_year("2100", "twenty one hundred") +# test_year("2105", "twenty one five") +# test_year("9999", "ninety nine ninety nine") +# print("************* test_year tests done**************") + + +def create_single_spoken_form(source: str): + """ + Returns a spoken form of a string + (1) Returns the value from REVERSE_PRONUNCIATION_MAP if it exists + (2) Splits allcaps into separate letters ("ABC" -> A B C) + (3) Otherwise, returns the lower case source. + """ + normalized_source = source.lower() + try: + mapped_source = REVERSE_PRONUNCIATION_MAP[normalized_source] + except KeyError: + # Leave completely uppercase words alone, as we can deal with them later. + # Otherwise normalized the rest to help with subsequent abbreviation lookups, + # etc. + if source.isupper(): + mapped_source = source + else: + mapped_source = source.lower() + return mapped_source + + +def create_exploded_forms(spoken_forms: List[str]): + """Exploded common packed words into separate words""" + # TODO: This could be moved somewhere else, possibly seeded from something like + # words to replace... + packed_words = {"readme": "read me"} + + new_spoken_forms = [] + for line in spoken_forms: + exploded_form = [] + # ex: "vm" or "usb" explodes into "V M" or "U S B" + + if ( + " " not in line + and line.islower() + and len(line) > 1 + and len(line) <= EXPLODE_MAX_LEN + ): + new_spoken_forms.append(line) # Keep a regular copy (ie: "nas") + new_spoken_forms.append(" ".join(line.upper())) + # ex: "readme" explodes into "read me" + else: + for word in line.split(" "): + if word in packed_words.keys(): + exploded_form.append(packed_words[word]) + else: + exploded_form.append(word) + new_spoken_forms.append(" ".join(exploded_form)) + return new_spoken_forms + + +def create_extension_forms(spoken_forms: List[str]): + """Add extension forms""" + new_spoken_forms = [] + + file_extensions_map = {v.strip(): k for k, v in file_extensions.items()} + for line in spoken_forms: + have_file_extension = False + file_extension_forms = [] + dotted_extension_form = [] + truncated_forms = [] + for substring in line.split(" "): + # NOTE: If we ever run in to file extensions in the middle of file name, the + # truncated form is going to be busted. ie: foo.md.disabled + + if substring in file_extensions_map.keys(): + file_extension_forms.append(file_extensions_map[substring]) + dotted_extension_form.append(REVERSE_PRONUNCIATION_MAP["."]) + dotted_extension_form.append(file_extensions_map[substring]) + have_file_extension = True + # purposefully down update truncated + else: + file_extension_forms.append(substring) + dotted_extension_form.append(substring) + truncated_forms.append(substring) + # print(file_extension_forms) + if have_file_extension: + new_spoken_forms.append(" ".join(file_extension_forms)) + new_spoken_forms.append(" ".join(dotted_extension_form)) + new_spoken_forms.append(" ".join(truncated_forms)) + + return set(dict.fromkeys(new_spoken_forms)) + + +def create_cased_forms(spoken_forms: List[str]): + """Add lower and upper case forms""" + new_spoken_forms = [] + + for line in spoken_forms: + lower_forms = [] + upper_forms = [] + # print(line) + for substring in line.split(" "): + if substring.isupper(): + lower_forms.append(substring.lower()) + upper_forms.append(" ".join(substring)) + else: + lower_forms.append(substring) + upper_forms.append(substring) + + new_spoken_forms.append(" ".join(lower_forms)) + new_spoken_forms.append(" ".join(upper_forms)) + + return set(dict.fromkeys(new_spoken_forms)) + + +def create_abbreviated_forms(spoken_forms: List[str]): + """Add abbreviated case forms""" + new_spoken_forms = [] + + swapped_abbreviation_map = {v: k for k, v in abbreviations_list.items()} + for line in spoken_forms: + unabbreviated_forms = [] + abbreviated_forms = [] + for substring in line.split(" "): + if substring in swapped_abbreviation_map.keys(): + abbreviated_forms.append(swapped_abbreviation_map[substring]) + else: + abbreviated_forms.append(substring) + unabbreviated_forms.append(substring) + + new_spoken_forms.append(" ".join(abbreviated_forms)) + new_spoken_forms.append(" ".join(unabbreviated_forms)) + + return set(dict.fromkeys(new_spoken_forms)) + + +def create_spoken_number_forms(source: List[str]): + """ + Create a list of spoken forms by transforming numbers in source into spoken forms. + This creates a first pass of spoken forms with numbers translated, but will go + through multiple other passes. + """ + + # list of spoken forms returned + spoken_forms = [] + + # contains the pieces for the spoken form with individual digits + full_form_digit_wise = [] + + # contains the pieces for the spoken form with the spoken version of the number + full_form_fancy_numbers = [] + + # contains the pieces for the spoken form for years like "1900" => nineteen hundred + full_form_spoken_form_years = [] + + # indicates whether or not we processed created a version with the full number (>10) translated + has_fancy_number_version = False + + # indicates whether or not we processed created a version with the year-like ("1900" => nineteen hundred) numbers + has_spoken_form_years = False + + for substring in source: + # for piece in pieces: + # substring = piece.group(0) + length = len(substring) + + # the length is currently capped at 31 digits + if length > 1 and length <= 31 and substring.isnumeric(): + has_fancy_number_version = True + val = int(substring) + spoken_form_years = create_spoken_form_years(val) + spoken_form = create_spoken_form_for_number(val) + + if spoken_form_years: + has_spoken_form_years = True + full_form_spoken_form_years.append(spoken_form_years) + else: + full_form_spoken_form_years.append(spoken_form) + + full_form_fancy_numbers.append(spoken_form) + + # build the serial digit version + for digit in substring: + full_form_digit_wise.append(create_single_spoken_form(digit)) + + else: + spoken_form = create_single_spoken_form(substring) + full_form_digit_wise.append(spoken_form) + full_form_fancy_numbers.append(spoken_form) + full_form_spoken_form_years.append(spoken_form) + + if has_fancy_number_version: + spoken_forms.append(" ".join(full_form_fancy_numbers)) + + if has_spoken_form_years: + result = " ".join(full_form_spoken_form_years) + if result not in spoken_forms: + spoken_forms.append(result) + + spoken_forms.append(" ".join(full_form_digit_wise)) + return set(dict.fromkeys(spoken_forms)) + + +def create_spoken_forms_from_regex(source: str, pattern: re.Pattern): + """ + Creates a list of spoken forms for source using the provided regex pattern. + For numeric pieces detected by the regex, generates both digit-wise and full + spoken forms for the numbers where appropriate. + """ + source_without_apostrophes = source.replace("'", "") + pieces = list(pattern.finditer(source_without_apostrophes)) + spoken_forms = list(map(lambda x: x.group(0), pieces)) + + # NOTE: Order is sometimes important + transforms = [ + create_spoken_number_forms, + create_extension_forms, + create_cased_forms, + create_exploded_forms, + create_abbreviated_forms, + create_extension_forms, + ] + + for func in transforms: + spoken_forms = func(spoken_forms) + + return list(dict.fromkeys(spoken_forms)) + + +def generate_string_subsequences( + source: str, + words_to_exclude: list[str], + minimum_term_length: int, +): + # Includes (lower-cased): + # 1. Each word in source, eg "foo bar baz" -> "foo", "bar", "baz". + # 2. Each leading subsequence of words from source, + # eg "foo bar baz" -> "foo", "foo bar", "foo bar baz" + # (but not "bar baz" - TODO: is this intentional?) + # + # Except for: + # 3. strings shorter than minimum_term_length + # 4. strings in words_to_exclude. + term_sequence = source.split(" ") + terms = { + # WARNING: This .lower() version creates unwanted duplication of broken up + # uppercase words, eg 'R E A D M E' -> 'r e a d m e'. Everything else should be + # lower case already + # term.lower().strip() + term.strip() + for term in ( + term_sequence + + list(itertools.accumulate([f"{term} " for term in term_sequence])) + ) + } + return [ + term + for term in terms + if (term not in words_to_exclude and len(term) >= minimum_term_length) + ] + + +@dataclass +class SpeakableItem: + name: str + value: Any + + +@mod.action_class +class Actions: + def create_spoken_forms( + source: str, + words_to_exclude: Optional[list[str]] = None, + minimum_term_length: int = DEFAULT_MINIMUM_TERM_LENGTH, + generate_subsequences: bool = True, + ) -> list[str]: + """Create spoken forms for a given source""" + + spoken_forms_without_symbols = create_spoken_forms_from_regex( + source, REGEX_NO_SYMBOLS + ) + + # todo: this could probably be optimized out if there's no symbols + spoken_forms_with_symbols = create_spoken_forms_from_regex( + source, REGEX_WITH_SYMBOLS + ) + + # some may be identical, so ensure the list is reduced + spoken_forms = set(spoken_forms_with_symbols + spoken_forms_without_symbols) + + # only generate the subsequences if requested + if generate_subsequences: + # todo: do we care about the subsequences that are excluded. + # the only one that seems relevant are the full spoken form for + spoken_forms.update( + generate_string_subsequences( + spoken_forms_without_symbols[-1], + words_to_exclude or [], + minimum_term_length, + ) + ) + + # Avoid empty spoken forms. + return [x for x in spoken_forms if x] + + def create_spoken_forms_from_list( + sources: list[str], + words_to_exclude: Optional[list[str]] = None, + minimum_term_length: int = DEFAULT_MINIMUM_TERM_LENGTH, + generate_subsequences: bool = True, + ) -> dict[str, str]: + """Create spoken forms for all sources in a list, doing conflict resolution""" + return actions.user.create_spoken_forms_from_map( + {source: source for source in sources}, + words_to_exclude, + minimum_term_length, + generate_subsequences, + ) + + def create_spoken_forms_from_map( + sources: Mapping[str, Any], + words_to_exclude: Optional[list[str]] = None, + minimum_term_length: int = DEFAULT_MINIMUM_TERM_LENGTH, + generate_subsequences: bool = True, + ) -> dict[str, Any]: + """Create spoken forms for all sources in a map, doing conflict resolution""" + all_spoken_forms: defaultdict[str, list[SpeakableItem]] = defaultdict(list) + + for name, value in sources.items(): + spoken_forms = actions.user.create_spoken_forms( + name, words_to_exclude, minimum_term_length, generate_subsequences + ) + for spoken_form in spoken_forms: + all_spoken_forms[spoken_form].append(SpeakableItem(name, value)) + + final_spoken_forms = {} + for spoken_form, spoken_form_sources in all_spoken_forms.items(): + if len(spoken_form_sources) > 1: + final_spoken_forms[spoken_form] = min( + spoken_form_sources, + key=lambda speakable_item: len(speakable_item.name), + ).value + else: + final_spoken_forms[spoken_form] = spoken_form_sources[0].value + + return final_spoken_forms diff --git a/community/core/delayed_speech_off.py b/community/core/delayed_speech_off.py new file mode 100644 index 0000000..11e8480 --- /dev/null +++ b/community/core/delayed_speech_off.py @@ -0,0 +1,30 @@ +from talon import Module, actions, speech_system + +delay_mod = Module() + +delayed_enabled = False + + +def do_disable(e): + speech_system.unregister("post:phrase", do_disable) + actions.speech.disable() + + +@delay_mod.action_class +class DelayedSpeechOffActions: + def delayed_speech_on(): + """Activates a "temporary speech" mode that can be disabled lazily, + so that the actual disable command happens after whatever phrase + finishes next.""" + global delayed_enabled + if not actions.speech.enabled(): + delayed_enabled = True + actions.speech.enable() + + def delayed_speech_off(): + """Disables "temporary speech" mode lazily, meaning that the next + phrase that finishes will turn speech off.""" + global delayed_enabled + if delayed_enabled: + delayed_enabled = False + speech_system.register("post:phrase", do_disable) diff --git a/community/core/deprecations.py b/community/core/deprecations.py new file mode 100644 index 0000000..4de0883 --- /dev/null +++ b/community/core/deprecations.py @@ -0,0 +1,183 @@ +""" +Helpers for deprecating voice commands, actions, and captures. Since Talon can +be an important part of people's workflows providing a warning before removing +functionality is encouraged. + +The normal deprecation process in `community` is as follows: + +1. For 6 months from deprecation a deprecated action or command should + continue working. Put an entry in the BREAKING_CHANGES.txt file in the + project root to mark the deprecation and potentially explain how users can + migrate. Use the user.deprecate_command, user.deprecate_action, or + user.deprecate_capture actions to notify users. +2. After 6 months you can delete the deprecated command, action, or capture. + Leave the note in BREAKING_CHANGES.txt so people who missed the + notifications can see what happened. + +If for some reason you can't keep the functionality working for 6 months, +just put the information in BREAKING_CHANGES.txt so people can look there to +see what happened. + +Usages: + + # myfile.talon - demonstrate voice command deprecation + ... + old legacy command: + # This will show a notification to say use 'new command' instead of + # 'old legacy command'. No removal of functionality is allowed. + user.deprecate_command("2022-11-10", "old legacy command", "new command") + # perform command + + new command: + # perform command + + # myfile.py - demonstrate action deprecation + from talon import actions + + @mod.action_class + class Actions: + def legacy_action(): + actions.user.deprecate_action("2022-10-01", "user.legacy_action") + # Perform action + + # otherfile.py - demostrate capture deprecation + @mod.capture(rule="...") + def legacy_capture(m) -> str: + actions.user.deprecate_capture("2023-09-03", "user.legacy_capture") + # implement capture + +See https://github.com/talonhub/community/issues/940 for original discussion +""" + +import datetime +import os.path +import warnings +from typing import Optional + +from talon import Module, actions, settings, speech_system + +REPO_DIR = os.path.dirname(os.path.dirname(__file__)) + +mod = Module() +mod.setting( + "deprecate_warning_interval_hours", + type=float, + desc="""How long, in hours, to wait before notifying the user again of a + deprecated action/command/capture.""", + default=24, +) + +# Tells us the last time a notification was shown so we can +# decide when to re-show it without annoying the user too +# much +notification_last_shown = {} + +# This gets reset on every phrase, so we avoid notifying more than once per +# phrase. +notified_in_phrase = set() + + +def calculate_rule_info(): + """ + Try to work out the .talon file and line of the command that is executing + """ + try: + current_command = actions.core.current_command__unstable() + start_line = current_command[0].target.start_line + filename = current_command[0].target.filename + rule = " ".join(current_command[1]._unmapped) + return f'\nTriggered from "{rule}" ({filename}:{start_line})' + except Exception as e: + return "" + + +def deprecate_notify(id: str, message: str): + """ + Notify the user about a deprecation/deactivation. id uniquely + identifies this deprecation. + """ + + maybe_last_shown = notification_last_shown.get(id) + now = datetime.datetime.now() + interval = settings.get("user.deprecate_warning_interval_hours") + threshold = now - datetime.timedelta(hours=interval) + if maybe_last_shown is not None and maybe_last_shown > threshold: + return + + actions.app.notify(message, "Deprecation warning") + notification_last_shown[id] = now + + +def post_phrase(_ignored): + global notified_in_phrase + notified_in_phrase = set() + + +speech_system.register("post:phrase", post_phrase) + + +@mod.action_class +class Actions: + def deprecate_command(time_deprecated: str, name: str, replacement: str): + """ + Notify the user that the given voice command is deprecated and should + not be used into the future; the command `replacement` should be used + instead. + """ + + if name in notified_in_phrase: + return + + # Want to tell users every time they use a deprecated command since + # they should immediately be retraining to use {replacement}. Also + # so if they repeat the command they get another chance to read + # the popup message. + notified_in_phrase.add(name) + msg = ( + f'The "{name}" command is deprecated. Instead, say: "{replacement}".' + f" See log for more." + ) + actions.app.notify(msg, "Deprecation warning") + msg = ( + f'The "{name}" command is deprecated since {time_deprecated}.' + f' Instead, say: "{replacement}".' + f' See {os.path.join(REPO_DIR, "BREAKING_CHANGES.txt")}' + ) + warnings.warn(msg, DeprecationWarning) + + def deprecate_capture(time_deprecated: str, name: str): + """ + Notify the user that the given capture is deprecated and should + not be used into the future. + """ + + id = f"capture.{name}.{time_deprecated}" + + deprecate_notify(id, f"The `{name}` capture is deprecated. See log for more.") + + msg = ( + f"The `{name}` capture is deprecated since {time_deprecated}." + f' See {os.path.join(REPO_DIR, "BREAKING_CHANGES.txt")}' + f"{calculate_rule_info()}" + ) + warnings.warn(msg, DeprecationWarning, stacklevel=3) + + 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; 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()}" + ) + warnings.warn(msg, DeprecationWarning, stacklevel=5) diff --git a/community/core/dragon_engine.py b/community/core/dragon_engine.py new file mode 100644 index 0000000..1058dc7 --- /dev/null +++ b/community/core/dragon_engine.py @@ -0,0 +1,22 @@ +from talon import Module, speech_system + +mod = Module() + + +@mod.action_class +class Actions: + def dragon_engine_sleep(): + """Sleep the dragon engine""" + speech_system.engine_mimic("go to sleep"), + + def dragon_engine_wake(): + """Wake the dragon engine""" + speech_system.engine_mimic("wake up"), + + def dragon_engine_command_mode(): + """Switch dragon to command mode. Requires Pro.""" + speech_system.engine_mimic("switch to command mode") + + def dragon_engine_normal_mode(): + """Switch dragon to normal mode. Requires Pro.""" + speech_system.engine_mimic("start normal mode") diff --git a/community/core/edit/delimiter_pair.py b/community/core/edit/delimiter_pair.py new file mode 100644 index 0000000..f960968 --- /dev/null +++ b/community/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/community/core/edit/delimiter_pair.talon-list b/community/core/edit/delimiter_pair.talon-list new file mode 100644 index 0000000..a634042 --- /dev/null +++ b/community/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/community/core/edit/edit.py b/community/core/edit/edit.py new file mode 100644 index 0000000..bf091b7 --- /dev/null +++ b/community/core/edit/edit.py @@ -0,0 +1,153 @@ +from talon import Context, Module, actions, clip, settings + +ctx = Context() +mod = Module() + +mod.setting( + "selected_text_timeout", + type=float, + default=0.25, + desc="Time in seconds to wait for the clipboard to change when trying to get selected text", +) + +END_OF_WORD_SYMBOLS = ".!?;:—_/\\|@#$%^&*()[]{}<>=+-~`" + + +@ctx.action_class("edit") +class EditActions: + def selected_text() -> str: + timeout = settings.get("user.selected_text_timeout") + with clip.capture(timeout) as s: + actions.edit.copy() + try: + return s.text() + except clip.NoChange: + return "" + + def line_insert_down(): + actions.edit.line_end() + actions.key("enter") + + def selection_clone(): + actions.edit.copy() + actions.edit.select_none() + actions.edit.paste() + + def line_clone(): + # This may not work if editor auto-indents. Is there a better way? + actions.edit.line_start() + actions.edit.extend_line_end() + actions.edit.copy() + actions.edit.right() + actions.key("enter") + actions.edit.paste() + + # # This simpler implementation of select_word mostly works, but in some apps it doesn't. + # # See https://github.com/talonhub/community/issues/1084. + # def select_word(): + # actions.edit.right() + # actions.edit.word_left() + # actions.edit.extend_word_right() + + def select_word(): + actions.edit.extend_right() + character_to_right_of_initial_caret_position = actions.edit.selected_text() + + # Occasionally apps won't let you edit.extend_right() + # and therefore won't select text if your caret is on the rightmost character + # such as in the Chrome URL bar + did_select_text = character_to_right_of_initial_caret_position != "" + + if did_select_text: + # .strip() turns newline & space characters into empty string; the empty + # string is in any other string, so this works. + if ( + character_to_right_of_initial_caret_position.strip() + in END_OF_WORD_SYMBOLS + ): + # Come out of the highlight in the initial position. + actions.edit.left() + else: + # Come out of the highlight one character + # to the right of the initial position. + actions.edit.right() + + actions.edit.word_left() + actions.edit.extend_word_right() + + +@mod.action_class +class Actions: + def paste(text: str): + """Pastes text and preserves clipboard""" + + with clip.revert(): + clip.set_text(text) + actions.edit.paste() + # sleep here so that clip.revert doesn't revert the clipboard too soon + actions.sleep("150ms") + + def delete_right(): + """Delete character to the right""" + actions.key("delete") + + def delete_all(): + """Delete all text in the current document""" + actions.edit.select_all() + actions.edit.delete() + + def words_left(n: int): + """Moves left by n words.""" + for _ in range(n): + actions.edit.word_left() + + def words_right(n: int): + """Moves right by n words.""" + for _ in range(n): + actions.edit.word_right() + + def cut_word_left(): + """Cuts the word to the left.""" + actions.edit.extend_word_left() + actions.edit.cut() + + def cut_word_right(): + """Cuts the word to the right.""" + actions.edit.extend_word_right() + actions.edit.cut() + + def copy_word_left(): + """Copies the word to the left.""" + actions.edit.extend_word_left() + actions.edit.copy() + + def copy_word_right(): + """Copies the word to the right.""" + actions.edit.extend_word_right() + actions.edit.copy() + + # ----- Start / End of line ----- + def select_line_start(): + """Select to start of current line""" + if actions.edit.selected_text(): + actions.edit.left() + actions.edit.extend_line_start() + + def select_line_end(): + """Select to end of current line""" + if actions.edit.selected_text(): + actions.edit.right() + actions.edit.extend_line_end() + + def line_middle(): + """Go to the middle of the line""" + actions.edit.select_line() + half_line_length = int(len(actions.edit.selected_text()) / 2) + actions.edit.left() + for i in range(0, half_line_length): + actions.edit.right() + + def cut_line(): + """Cut current line""" + actions.edit.select_line() + actions.edit.cut() diff --git a/community/core/edit/edit.talon b/community/core/edit/edit.talon new file mode 100644 index 0000000..dbe3e7d --- /dev/null +++ b/community/core/edit/edit.talon @@ -0,0 +1,85 @@ +# Compound of action(select, clear, copy, cut, paste, etc.) and modifier(word, +# line, etc.) commands for editing text. +# eg: "select line", "clear all" +# For overriding or creating aliases for specific actions, this function will +# also accept strings, e.g. `user.edit_command("delete", "wordLeft")`. +# See edit_command_modifiers.py to discover the correct string for the modify argument, +# and `edit_command_actions.py` `simple_action_callbacks` to find strings for the action argument. + : user.edit_command(edit_action, edit_modifier) + +# Zoom +zoom in: edit.zoom_in() +zoom out: edit.zoom_out() +zoom reset: edit.zoom_reset() + +# Searching +find it: edit.find() +next one: edit.find_next() + +# Navigation + +# The reason for these spoken forms is that "page up" and "page down" are globally defined as keys. +scroll up: edit.page_up() +scroll down: edit.page_down() + +# go left, go left left down, go 5 left 2 down +# go word left, go 2 words right +go +: user.perform_navigation_steps(navigation_step_list) + +go line start | head: edit.line_start() +go line end | tail: edit.line_end() + +go way left: + edit.line_start() + edit.line_start() +go way right: edit.line_end() +go way up: edit.file_start() +go way down: edit.file_end() + +go top: edit.file_start() +go bottom: edit.file_end() + +go page up: edit.page_up() +go page down: edit.page_down() + +# Indentation +indent [more]: edit.indent_more() +(indent less | out dent): edit.indent_less() + +# Copy +copy that: edit.copy() + +# Cut +cut that: edit.cut() + +# Paste +(pace | paste) (that | it): edit.paste() +(pace | paste) enter: + edit.paste() + key(enter) +paste match: edit.paste_match_style() + +# Duplication +clone that: edit.selection_clone() +clone line: edit.line_clone() + +# Insert new line +new line above: edit.line_insert_up() +new line below | slap: edit.line_insert_down() + +# Insert padding with optional symbols +padding: user.insert_between(" ", " ") +(pad | padding) +: + insert(" ") + user.insert_many(symbol_key_list) + insert(" ") + +# Undo/redo +undo that: edit.undo() +redo that: edit.redo() + +# Save +file save: edit.save() +file save all: edit.save_all() + +[go] line mid: user.line_middle() diff --git a/community/core/edit/edit_command.py b/community/core/edit/edit_command.py new file mode 100644 index 0000000..2790522 --- /dev/null +++ b/community/core/edit/edit_command.py @@ -0,0 +1,191 @@ +from talon import Module, actions, settings + +from .edit_command_actions import EditAction, EditSimpleAction, run_action_callback +from .edit_command_modifiers import EditModifier, run_modifier_callback + +mod = Module() + +# providing some settings for customizing the word and line selection delay +# talon can execute selections must faster than a human +# resulting in unexpected or inconsistent results in applications such as visual studio code +mod.setting( + "edit_command_word_selection_delay", + type=int, + default=75, + desc="Sleep required between word selections", +) + +mod.setting( + "edit_command_line_selection_delay", + type=int, + default=75, + desc="Sleep required between line selections", +) + + +def before_line_up(): + actions.edit.up() + actions.edit.line_start() + + +def after_line_up(): + actions.edit.up() + actions.edit.line_end() + + +def before_line_down(): + actions.edit.down() + actions.edit.line_start() + + +def after_line_down(): + actions.edit.down() + actions.edit.line_end() + + +def select_lines(action, direction, count): + if direction == "lineUp": + selection_callback = actions.edit.extend_line_up + extend_line_callback = actions.edit.extend_line_start + else: + selection_callback = actions.edit.extend_line_down + extend_line_callback = actions.edit.extend_line_end + + selection_delay = f"{settings.get('user.edit_command_line_selection_delay')}ms" + + for i in range(1, count + 1): + selection_callback() + actions.sleep(selection_delay) + + # ensure we take the start/end of the line too! + extend_line_callback() + actions.sleep(selection_delay) + run_action_callback(action) + + +def select_words(action, direction, count): + if direction == "wordLeft": + selection_callback = actions.edit.extend_word_left + else: + selection_callback = actions.edit.extend_word_right + + selection_delay = f"{settings.get('user.edit_command_word_selection_delay')}ms" + for i in range(1, count + 1): + selection_callback() + actions.sleep(selection_delay) + + run_action_callback(action) + + +def word_movement_handler(action, direction, count): + if direction == "wordLeft": + movement_callback = actions.edit.word_left + else: + movement_callback = actions.edit.word_right + + selection_delay = f"{settings.get('user.edit_command_word_selection_delay')}ms" + for i in range(1, count + 1): + movement_callback() + actions.sleep(selection_delay) + + +# in some cases, it is necessary to have some custom handling for timing reasons +custom_callbacks = { + ("goAfter", "wordLeft"): word_movement_handler, + ("goAfter", "wordRight"): word_movement_handler, + ("goBefore", "wordLeft"): word_movement_handler, + ("goBefore", "wordRight"): word_movement_handler, + # delete + ("delete", "word"): select_words, + ("delete", "wordLeft"): select_words, + ("delete", "wordRight"): select_words, + ("delete", "lineUp"): select_lines, + ("delete", "lineDown"): select_lines, + # cut + ("cutToClipboard", "word"): select_words, + ("cutToClipboard", "wordLeft"): select_words, + ("cutToClipboard", "wordRight"): select_words, + ("cutToClipboard", "lineUp"): select_lines, + ("cutToClipboard", "lineDown"): select_lines, + # copy + ("copyToClipboard", "word"): select_words, + ("copyToClipboard", "wordLeft"): select_words, + ("copyToClipboard", "wordRight"): select_words, + ("copyToClipboard", "lineUp"): select_lines, + ("copyToClipboard", "lineDown"): select_lines, + # select + ("select", "lineUp"): select_lines, + ("select", "lineDown"): select_lines, +} + +# In other cases there already is a "compound" talon action for a given action and modifier +compound_actions = { + # select + ("select", "wordLeft"): actions.edit.extend_word_left, + ("select", "wordRight"): actions.edit.extend_word_right, + ("select", "left"): actions.edit.extend_left, + ("select", "right"): actions.edit.extend_right, + ("select", "word"): actions.edit.extend_word_right, + # Go before + ("goBefore", "line"): actions.edit.line_start, + ("goBefore", "lineUp"): before_line_up, + ("goBefore", "lineDown"): before_line_down, + ("goBefore", "paragraph"): actions.edit.paragraph_start, + ("goBefore", "document"): actions.edit.file_start, + ("goBefore", "fileStart"): actions.edit.file_start, + ("goBefore", "selection"): actions.edit.left, + ("goBefore", "wordLeft"): actions.edit.word_left, + ("goBefore", "word"): actions.edit.word_left, + # Go after + ("goAfter", "line"): actions.edit.line_end, + ("goAfter", "lineUp"): after_line_up, + ("goAfter", "lineDown"): after_line_down, + ("goAfter", "paragraph"): actions.edit.paragraph_end, + ("goAfter", "document"): actions.edit.file_end, + ("goAfter", "fileEnd"): actions.edit.file_end, + ("goAfter", "selection"): actions.edit.right, + ("goAfter", "wordRight"): actions.edit.word_right, + ("goAfter", "wordLeft"): actions.edit.word_left, + ("goAfter", "word"): actions.edit.word_right, + # Delete + ("delete", "left"): actions.edit.delete, + ("delete", "right"): actions.user.delete_right, + ("delete", "line"): actions.edit.delete_line, + ("delete", "paragraph"): actions.edit.delete_paragraph, + # ("delete", "document"): actions.edit.delete_all, # Beta only + ("delete", "document"): actions.user.delete_all, + ("delete", "selection"): actions.edit.delete, + # Cut to clipboard + ("cutToClipboard", "line"): actions.user.cut_line, + ("cutToClipboard", "selection"): actions.edit.cut, + # copy + ("copyToClipboard", "selection"): actions.edit.copy, +} + + +@mod.action_class +class Actions: + def edit_command(action: EditAction | str, modifier: EditModifier | str): + """Perform edit command with associated modifier. + Action and modifier can be dataclasses (formed from utterances via + capture) or str, for use in scripts. Strings should match the action or + modifier types declared here or in edit_command_modifiers.py or + edit_command_actions.py""" + if isinstance(modifier, str): + modifier = EditModifier(modifier) + if isinstance(action, str): + action = EditSimpleAction(action) + key = (action.type, modifier.type) + count = modifier.count + + if key in custom_callbacks: + custom_callbacks[key](action, modifier.type, count) + return + + elif key in compound_actions: + for i in range(1, count + 1): + compound_actions[key]() + return + + run_modifier_callback(modifier) + run_action_callback(action) diff --git a/community/core/edit/edit_command_actions.py b/community/core/edit/edit_command_actions.py new file mode 100644 index 0000000..7701e5c --- /dev/null +++ b/community/core/edit/edit_command_actions.py @@ -0,0 +1,113 @@ +from dataclasses import dataclass +from typing import Callable, Union + +from talon import Module, actions + + +@dataclass +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: + 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: + 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) -> EditSimpleAction: + return EditSimpleAction(m.edit_action) + + +@mod.capture(rule=" wrap") +def edit_wrap_action(m) -> EditWrapAction: + return EditWrapAction(m.delimiter_pair) + + +@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] + + +simple_action_callbacks: dict[str, Callable] = { + "select": actions.skip, + "goBefore": actions.edit.left, + "goAfter": actions.edit.right, + "copyToClipboard": actions.edit.copy, + "cutToClipboard": actions.edit.cut, + "pasteFromClipboard": actions.edit.paste, + "insertLineAbove": actions.edit.line_insert_up, + "insertLineBelow": actions.edit.line_insert_down, + "insertCopyAfter": actions.edit.selection_clone, + "delete": actions.edit.delete, +} + + +def run_action_callback(action: EditAction): + action_type = action.type + + if action_type in simple_action_callbacks: + callback = simple_action_callbacks[action_type] + callback() + return + + match action_type: + case "insert": + 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) + + case _: + raise ValueError(f"Unknown edit action: {action_type}") diff --git a/community/core/edit/edit_command_actions.talon-list b/community/core/edit/edit_command_actions.talon-list new file mode 100644 index 0000000..edd8d63 --- /dev/null +++ b/community/core/edit/edit_command_actions.talon-list @@ -0,0 +1,13 @@ +list: user.edit_action +- + +select: select +go before: goBefore +go after: goAfter + +copy: copyToClipboard +cut: cutToClipboard +paste: pasteFromClipboard +paste to: pasteFromClipboard + +clear: delete diff --git a/community/core/edit/edit_command_modifiers.py b/community/core/edit/edit_command_modifiers.py new file mode 100644 index 0000000..49bd3e0 --- /dev/null +++ b/community/core/edit/edit_command_modifiers.py @@ -0,0 +1,75 @@ +from contextlib import suppress +from dataclasses import dataclass +from typing import Callable + +from talon import Module, actions + +mod = Module() +mod.list("edit_modifier", desc="Modifiers for the edit command") +mod.list( + "edit_modifier_repeatable", + desc="Modifiers for the edit command that are repeatable", +) + + +@dataclass +class EditModifier: + type: str + count: int = 1 + + +@dataclass +class EditModifierCallback: + modifier: str + callback: Callable + + +@mod.capture( + rule="({user.edit_modifier}) | ([] {user.edit_modifier_repeatable})" +) +def edit_modifier(m) -> EditModifier: + count = 1 + with suppress(AttributeError): + count = m.number_small + + with suppress(AttributeError): + type = m.edit_modifier + + with suppress(AttributeError): + type = m.edit_modifier_repeatable + + return EditModifier(type, count=count) + + +modifiers = [ + EditModifierCallback("document", actions.edit.select_all), + EditModifierCallback("paragraph", actions.edit.select_paragraph), + EditModifierCallback("word", actions.edit.extend_word_right), + EditModifierCallback("wordLeft", actions.edit.extend_word_left), + EditModifierCallback("wordRight", actions.edit.extend_word_right), + EditModifierCallback("left", actions.edit.extend_left), + EditModifierCallback("right", actions.edit.extend_right), + EditModifierCallback("lineUp", actions.edit.extend_line_up), + EditModifierCallback("lineDown", actions.edit.extend_line_down), + EditModifierCallback("line", actions.edit.select_line), + EditModifierCallback("lineEnd", actions.edit.extend_line_end), + EditModifierCallback("lineStart", actions.edit.extend_line_start), + EditModifierCallback("fileStart", actions.edit.extend_file_start), + EditModifierCallback("fileEnd", actions.edit.extend_file_end), + EditModifierCallback("selection", actions.skip), +] + +modifier_dictionary: dict[str, EditModifierCallback] = { + item.modifier: item for item in modifiers +} + + +def run_modifier_callback(modifier: EditModifier): + modifier_type = modifier.type + if modifier_type not in modifier_dictionary: + raise ValueError(f"Unknown edit modifier: {modifier_type}") + + count = modifier.count + modifier = modifier_dictionary[modifier_type] + for i in range(1, count + 1): + modifier.callback() diff --git a/community/core/edit/edit_command_modifiers.talon-list b/community/core/edit/edit_command_modifiers.talon-list new file mode 100644 index 0000000..3b6be24 --- /dev/null +++ b/community/core/edit/edit_command_modifiers.talon-list @@ -0,0 +1,13 @@ +list: user.edit_modifier +- +all: document +paragraph: paragraph +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 diff --git a/community/core/edit/edit_command_modifiers_repeatable.talon-list b/community/core/edit/edit_command_modifiers_repeatable.talon-list new file mode 100644 index 0000000..9fca912 --- /dev/null +++ b/community/core/edit/edit_command_modifiers_repeatable.talon-list @@ -0,0 +1,9 @@ +list: user.edit_modifier_repeatable +- +word: word +word left: wordLeft +word right: wordRight +up: lineUp +down: lineDown +left: left +right: right diff --git a/community/core/edit/edit_linux.py b/community/core/edit/edit_linux.py new file mode 100644 index 0000000..f2e7496 --- /dev/null +++ b/community/core/edit/edit_linux.py @@ -0,0 +1,194 @@ +# defines the default edit actions for linux + +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: linux +""" + + +@ctx.action_class("edit") +class EditActions: + def copy(): + actions.key("ctrl-c") + + def cut(): + actions.key("ctrl-x") + + def delete(): + actions.key("backspace") + + def delete_line(): + actions.edit.select_line() + actions.edit.delete() + # action(edit.delete_paragraph): + # action(edit.delete_sentence): + + def delete_word(): + actions.edit.select_word() + actions.edit.delete() + + def down(): + actions.key("down") + # action(edit.extend_again): + # action(edit.extend_column): + + def extend_down(): + actions.key("shift-down") + + def extend_file_end(): + actions.key("shift-ctrl-end") + + def extend_file_start(): + actions.key("shift-ctrl-home") + + def extend_left(): + actions.key("shift-left") + # action(edit.extend_line): + + def extend_line_down(): + actions.key("shift-down") + + def extend_line_end(): + actions.key("shift-end") + + def extend_line_start(): + actions.key("shift-home") + + def extend_line_up(): + actions.key("shift-up") + + def extend_page_down(): + actions.key("shift-pagedown") + + def extend_page_up(): + actions.key("shift-pageup") + # action(edit.extend_paragraph_end): + # action(edit.extend_paragraph_next()): + # action(edit.extend_paragraph_previous()): + # action(edit.extend_paragraph_start()): + + def extend_right(): + actions.key("shift-right") + # action(edit.extend_sentence_end): + # action(edit.extend_sentence_next): + # action(edit.extend_sentence_previous): + # action(edit.extend_sentence_start): + + def extend_up(): + actions.key("shift-up") + + def extend_word_left(): + actions.key("ctrl-shift-left") + + def extend_word_right(): + actions.key("ctrl-shift-right") + + def file_end(): + actions.key("ctrl-end") + + def file_start(): + actions.key("ctrl-home") + + def find(text: str = None): + actions.key("ctrl-f") + if text: + actions.insert(text) + + def find_previous(): + actions.key("shift-f3") + + def find_next(): + actions.key("f3") + + def indent_less(): + actions.key("home delete") + + def indent_more(): + actions.key("home tab") + # action(edit.jump_column(n: int) + # action(edit.jump_line(n: int) + + def left(): + actions.key("left") + + def line_down(): + actions.key("down home") + + def line_end(): + actions.key("end") + + def line_insert_up(): + actions.key("home enter up") + + def line_start(): + actions.key("home") + + def line_up(): + actions.key("up home") + # action(edit.move_again): + + def page_down(): + actions.key("pagedown") + + def page_up(): + actions.key("pageup") + # action(edit.paragraph_end): + # action(edit.paragraph_next): + # action(edit.paragraph_previous): + # action(edit.paragraph_start): + + def paste(): + actions.key("ctrl-v") + # action(paste_match_style): + + def print(): + actions.key("ctrl-p") + + def redo(): + actions.key("ctrl-y") + + def right(): + actions.key("right") + + def save(): + actions.key("ctrl-s") + + def save_all(): + actions.key("ctrl-shift-s") + + def select_all(): + actions.key("ctrl-a") + + def select_line(n: int = None): + if n is not None: + actions.edit.jump_line(n) + actions.key("end shift-home") + # action(edit.select_lines(a: int, b: int)): + + def select_none(): + actions.key("right") + # action(edit.select_paragraph): + # action(edit.select_sentence): + + def undo(): + actions.key("ctrl-z") + + def up(): + actions.key("up") + + def word_left(): + actions.key("ctrl-left") + + def word_right(): + actions.key("ctrl-right") + + def zoom_in(): + actions.key("ctrl-+") + + def zoom_out(): + actions.key("ctrl--") + + def zoom_reset(): + actions.key("ctrl-0") diff --git a/community/core/edit/edit_mac.py b/community/core/edit/edit_mac.py new file mode 100644 index 0000000..5466b94 --- /dev/null +++ b/community/core/edit/edit_mac.py @@ -0,0 +1,194 @@ +from talon import Context, actions, clip + +ctx = Context() +ctx.matches = r""" +os: mac +""" + + +@ctx.action_class("edit") +class EditActions: + def copy(): + actions.key("cmd-c") + + def cut(): + actions.key("cmd-x") + + def delete(): + actions.key("backspace") + + def delete_line(): + actions.edit.select_line() + actions.edit.delete() + # action(edit.delete_paragraph): + # action(edit.delete_sentence): + + def delete_word(): + actions.edit.select_word() + actions.edit.delete() + + def down(): + actions.key("down") + # action(edit.extend_again): + # action(edit.extend_column): + + def extend_down(): + actions.key("shift-down") + + def extend_file_end(): + actions.key("cmd-shift-down") + + def extend_file_start(): + actions.key("cmd-shift-up") + + def extend_left(): + actions.key("shift-left") + # action(edit.extend_line): + + def extend_line_down(): + actions.key("shift-down") + + def extend_line_end(): + actions.key("cmd-shift-right") + + def extend_line_start(): + actions.key("cmd-shift-left") + + def extend_line_up(): + actions.key("shift-up") + + def extend_page_down(): + actions.key("cmd-shift-pagedown") + + def extend_page_up(): + actions.key("cmd-shift-pageup") + # action(edit.extend_paragraph_end): + # action(edit.extend_paragraph_next()): + # action(edit.extend_paragraph_previous()): + # action(edit.extend_paragraph_start()): + + def extend_right(): + actions.key("shift-right") + # action(edit.extend_sentence_end): + # action(edit.extend_sentence_next): + # action(edit.extend_sentence_previous): + # action(edit.extend_sentence_start): + + def extend_up(): + actions.key("shift-up") + + def extend_word_left(): + actions.key("shift-alt-left") + + def extend_word_right(): + actions.key("shift-alt-right") + + def file_end(): + actions.key("cmd-down") + + def file_start(): + actions.key("cmd-up") + + def find(text: str = None): + if text is not None: + clip.set_text(text, mode="find") + actions.key("cmd-f") + + def find_next(): + actions.key("cmd-g") + + def find_previous(): + actions.key("cmd-shift-g") + + def indent_less(): + actions.key("cmd-left delete") + + def indent_more(): + actions.key("cmd-left tab") + # action(edit.jump_column(n: int) + # action(edit.jump_line(n: int) + + def left(): + actions.key("left") + + def line_down(): + actions.key("down home") + + def line_end(): + actions.key("cmd-right") + + def line_insert_up(): + actions.key("cmd-left enter up") + + def line_start(): + actions.key("cmd-left") + + def line_up(): + actions.key("up cmd-left") + # action(edit.move_again): + + def page_down(): + actions.key("pagedown") + + def page_up(): + actions.key("pageup") + # action(edit.paragraph_end): + # action(edit.paragraph_next): + # action(edit.paragraph_previous): + # action(edit.paragraph_start): + + def paste(): + actions.key("cmd-v") + + def paste_match_style(): + actions.key("cmd-alt-shift-v") + + def print(): + actions.key("cmd-p") + + def redo(): + actions.key("cmd-shift-z") + + def right(): + actions.key("right") + + def save(): + actions.key("cmd-s") + + def save_all(): + actions.key("cmd-alt-s") + + def select_all(): + actions.key("cmd-a") + + def select_line(n: int = None): + if n is not None: + actions.edit.jump_line(n) + actions.key("cmd-right cmd-shift-left") + # action(edit.select_lines(a: int, b: int)): + + def select_none(): + actions.key("right") + # action(edit.select_paragraph): + # action(edit.select_sentence): + + def undo(): + actions.key("cmd-z") + + def up(): + actions.key("up") + + def word_left(): + actions.key("alt-left") + + def word_right(): + actions.key("alt-right") + + def zoom_in(): + actions.key("cmd-=") + + def zoom_out(): + actions.key("cmd--") + + def zoom_reset(): + actions.key("cmd-0") diff --git a/community/core/edit/edit_navigation_steps.py b/community/core/edit/edit_navigation_steps.py new file mode 100644 index 0000000..96309bb --- /dev/null +++ b/community/core/edit/edit_navigation_steps.py @@ -0,0 +1,65 @@ +from contextlib import suppress +from dataclasses import dataclass +from typing import Callable, Literal + +from talon import Module, actions, settings + + +@dataclass +class NavigationStep: + modifier: Literal[ + "wordLeft", "wordRight", "word", "left", "right", "lineUp", "lineDown" + ] + count: int + + +mod = Module() + + +@mod.capture(rule="[] {user.edit_modifier_repeatable}") +def navigation_step(m) -> NavigationStep: + count = 1 + modifier = m.edit_modifier_repeatable + + with suppress(AttributeError): + count = m.number_small + + return NavigationStep( + modifier=modifier, + count=count, + ) + + +@mod.action_class +class Actions: + def perform_navigation_steps(steps: list[NavigationStep]): + """Navigate by a series of steps""" + for step in steps: + match step.modifier: + case "wordLeft": + repeat_action(actions.edit.word_left, step.count, True) + case "wordRight": + repeat_action(actions.edit.word_right, step.count, True) + case "word": + repeat_action(actions.edit.word_right, step.count, True) + case "left": + repeat_action(actions.edit.left, step.count) + case "right": + repeat_action(actions.edit.right, step.count) + case "lineUp": + repeat_action(actions.edit.up, step.count) + case "lineDown": + repeat_action(actions.edit.down, step.count) + + +def repeat_action(action: Callable, count: int, delay: bool = False): + delay_string = None + + if delay: + delay_string = f"{settings.get('user.edit_command_word_selection_delay')}ms" + + for _ in range(count): + action() + + if delay_string: + actions.sleep(delay_string) diff --git a/community/core/edit/edit_paragraph.py b/community/core/edit/edit_paragraph.py new file mode 100644 index 0000000..2fb244f --- /dev/null +++ b/community/core/edit/edit_paragraph.py @@ -0,0 +1,115 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + + +@ctx.action_class("edit") +class EditActions: + def paragraph_start(): + if extend_paragraph_start_with_success(): + actions.edit.left() + + def paragraph_end(): + if extend_paragraph_end_with_success(): + actions.edit.right() + + def select_paragraph(): + if is_line_empty(): + return + # Search for start of paragraph + actions.edit.extend_paragraph_start() + actions.edit.left() + # Extend to end of paragraph + actions.edit.extend_paragraph_end() + + def extend_paragraph_start(): + # The reason for the wrapper function is a difference in function signature. + # The Talon action has no return value and the below function returns a boolean with success state. + extend_paragraph_start_with_success() + + def extend_paragraph_end(): + extend_paragraph_end_with_success() + + def delete_paragraph(): + actions.edit.select_paragraph() + # Remove selection + actions.edit.delete() + # Remove the empty line containing the cursor + actions.edit.delete() + # Remove leading or trailing empty line + actions.edit.delete_line() + + +@mod.action_class +class Actions: + def cut_paragraph(): + """Cut paragraph under the cursor""" + actions.edit.select_paragraph() + actions.edit.cut() + + def copy_paragraph(): + """Copy paragraph under the cursor""" + actions.edit.select_paragraph() + actions.edit.copy() + + def paste_paragraph(): + """Paste to paragraph under the cursor""" + actions.edit.select_paragraph() + actions.edit.paste() + + +def is_line_empty() -> bool: + """Check if the current line is empty. Return True if empty.""" + actions.edit.extend_line_start() + text = actions.edit.selected_text().strip() + if text: + actions.edit.right() + return False + actions.edit.extend_line_end() + text = actions.edit.selected_text().strip() + if text: + actions.edit.left() + return False + return True + + +def extend_paragraph_start_with_success() -> bool: + """Extend selection to the start of the paragraph. Return True if successful.""" + actions.edit.extend_line_start() + text = actions.edit.selected_text() + length = len(text) + while True: + actions.edit.extend_up() + actions.edit.extend_line_start() + text = actions.edit.selected_text() + new_length = len(text) + if new_length == length: + break + line = text[: new_length - length].strip() + if not line: + actions.edit.extend_down() + break + length = new_length + return text.strip() != "" + + +def extend_paragraph_end_with_success() -> bool: + """Extend selection to the end of the paragraph. Return True if successful.""" + actions.edit.extend_line_end() + text = actions.edit.selected_text() + length = len(text) + while True: + actions.edit.extend_down() + actions.edit.extend_line_end() + text = actions.edit.selected_text() + new_length = len(text) + if new_length == length: + break + line = text[length:].strip() + if not line: + actions.edit.extend_line_start() + actions.edit.extend_left() + break + length = new_length + return text.strip() != "" diff --git a/community/core/edit/edit_win.py b/community/core/edit/edit_win.py new file mode 100644 index 0000000..e127165 --- /dev/null +++ b/community/core/edit/edit_win.py @@ -0,0 +1,194 @@ +# defines the default edit actions for windows + +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: windows +""" + + +@ctx.action_class("edit") +class EditActions: + def copy(): + actions.key("ctrl-c") + + def cut(): + actions.key("ctrl-x") + + def delete(): + actions.key("backspace") + + def delete_line(): + actions.edit.select_line() + actions.edit.delete() + # action(edit.delete_paragraph): + # action(edit.delete_sentence): + + def delete_word(): + actions.edit.select_word() + actions.edit.delete() + + def down(): + actions.key("down") + # action(edit.extend_again): + # action(edit.extend_column): + + def extend_down(): + actions.key("shift-down") + + def extend_file_end(): + actions.key("shift-ctrl-end") + + def extend_file_start(): + actions.key("shift-ctrl-home") + + def extend_left(): + actions.key("shift-left") + # action(edit.extend_line): + + def extend_line_down(): + actions.key("shift-down") + + def extend_line_end(): + actions.key("shift-end") + + def extend_line_start(): + actions.key("shift-home") + + def extend_line_up(): + actions.key("shift-up") + + def extend_page_down(): + actions.key("shift-pagedown") + + def extend_page_up(): + actions.key("shift-pageup") + # action(edit.extend_paragraph_end): + # action(edit.extend_paragraph_next()): + # action(edit.extend_paragraph_previous()): + # action(edit.extend_paragraph_start()): + + def extend_right(): + actions.key("shift-right") + # action(edit.extend_sentence_end): + # action(edit.extend_sentence_next): + # action(edit.extend_sentence_previous): + # action(edit.extend_sentence_start): + + def extend_up(): + actions.key("shift-up") + + def extend_word_left(): + actions.key("ctrl-shift-left") + + def extend_word_right(): + actions.key("ctrl-shift-right") + + def file_end(): + actions.key("ctrl-end") + + def file_start(): + actions.key("ctrl-home") + + def find(text: str = None): + actions.key("ctrl-f") + if text: + actions.insert(text) + + def find_previous(): + actions.key("shift-f3") + + def find_next(): + actions.key("f3") + + def indent_less(): + actions.key("home delete") + + def indent_more(): + actions.key("home tab") + # action(edit.jump_column(n: int) + # action(edit.jump_line(n: int) + + def left(): + actions.key("left") + + def line_down(): + actions.key("down home") + + def line_end(): + actions.key("end") + + def line_insert_up(): + actions.key("home enter up") + + def line_start(): + actions.key("home") + + def line_up(): + actions.key("up home") + # action(edit.move_again): + + def page_down(): + actions.key("pagedown") + + def page_up(): + actions.key("pageup") + # action(edit.paragraph_end): + # action(edit.paragraph_next): + # action(edit.paragraph_previous): + # action(edit.paragraph_start): + + def paste(): + actions.key("ctrl-v") + # action(paste_match_style): + + def print(): + actions.key("ctrl-p") + + def redo(): + actions.key("ctrl-y") + + def right(): + actions.key("right") + + def save(): + actions.key("ctrl-s") + + def save_all(): + actions.key("ctrl-shift-s") + + def select_all(): + actions.key("ctrl-a") + + def select_line(n: int = None): + if n is not None: + actions.edit.jump_line(n) + actions.key("end shift-home") + # action(edit.select_lines(a: int, b: int)): + + def select_none(): + actions.key("right") + # action(edit.select_paragraph): + # action(edit.select_sentence): + + def undo(): + actions.key("ctrl-z") + + def up(): + actions.key("up") + + def word_left(): + actions.key("ctrl-left") + + def word_right(): + actions.key("ctrl-right") + + def zoom_in(): + actions.key("ctrl-+") + + def zoom_out(): + actions.key("ctrl--") + + def zoom_reset(): + actions.key("ctrl-0") diff --git a/community/core/edit/insert_between.py b/community/core/edit/insert_between.py new file mode 100644 index 0000000..667e8f5 --- /dev/null +++ b/community/core/edit/insert_between.py @@ -0,0 +1,12 @@ +from talon import Module, actions + +mod = Module() + + +@mod.action_class +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(f"{before}{after}") + for _ in after: + actions.edit.left() diff --git a/community/core/edit_text_file/edit_text_file.py b/community/core/edit_text_file/edit_text_file.py new file mode 100644 index 0000000..ce4764f --- /dev/null +++ b/community/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/community/core/edit_text_file/edit_text_file.talon b/community/core/edit_text_file/edit_text_file.talon new file mode 100644 index 0000000..6cd0420 --- /dev/null +++ b/community/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/community/core/edit_text_file/edit_text_file_list.talon-list b/community/core/edit_text_file/edit_text_file_list.talon-list new file mode 100644 index 0000000..2fb6dbc --- /dev/null +++ b/community/core/edit_text_file/edit_text_file_list.talon-list @@ -0,0 +1,17 @@ +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 +contacts json: private/contacts.json +contacts csv: private/contacts.csv diff --git a/community/core/file_extension/file_extension.py b/community/core/file_extension/file_extension.py new file mode 100644 index 0000000..81336a5 --- /dev/null +++ b/community/core/file_extension/file_extension.py @@ -0,0 +1,69 @@ +from talon import Context, Module + +from ..user_settings import track_csv_list + +mod = Module() +mod.list("file_extension", desc="A file extension, such as .py") + +_file_extensions_defaults = { + "dot pie": ".py", + "dot elixir": ".ex", + "dot talon": ".talon", + "dot talon list": ".talon-list", + "dot mark down": ".md", + "dot shell": ".sh", + "dot vim": ".vim", + "dot see": ".c", + "dot see sharp": ".cs", + "dot com": ".com", + "dot net": ".net", + "dot org": ".org", + "dot us": ".us", + "dot U S": ".us", + "dot co dot UK": ".co.uk", + "dot exe": ".exe", + "dot bin": ".bin", + "dot bend": ".bin", + "dot jason": ".json", + "dot jay son": ".json", + "dot J S": ".js", + "dot java script": ".js", + "dot TS": ".ts", + "dot type script": ".ts", + "dot csv": ".csv", + "totssv": ".csv", + "tot csv": ".csv", + "dot cassie": ".csv", + "dot text": ".txt", + "dot julia": ".jl", + "dot J L": ".jl", + "dot html": ".html", + "dot css": ".css", + "dot sass": ".sass", + "dot svg": ".svg", + "dot png": ".png", + "dot wave": ".wav", + "dot flack": ".flac", + "dot doc": ".doc", + "dot doc x": ".docx", + "dot pdf": ".pdf", + "dot tar": ".tar", + "dot g z": ".gz", + "dot g zip": ".gzip", + "dot zip": ".zip", + "dot toml": ".toml", + "dot java": ".java", + "dot class": ".class", + "dot log": ".log", +} + +ctx = Context() + + +@track_csv_list( + "file_extensions.csv", + headers=("File extension", "Name"), + default=_file_extensions_defaults, +) +def on_update(values): + ctx.lists["self.file_extension"] = values diff --git a/community/core/file_extension/file_extension.talon b/community/core/file_extension/file_extension.talon new file mode 100644 index 0000000..9213b7d --- /dev/null +++ b/community/core/file_extension/file_extension.talon @@ -0,0 +1 @@ +{user.file_extension}: "{file_extension}" diff --git a/community/core/formatters/code_formatter.talon-list b/community/core/formatters/code_formatter.talon-list new file mode 100644 index 0000000..aa02462 --- /dev/null +++ b/community/core/formatters/code_formatter.talon-list @@ -0,0 +1,19 @@ +list: user.code_formatter +- +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 +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/community/core/formatters/formatters.py b/community/core/formatters/formatters.py new file mode 100644 index 0000000..8a49e55 --- /dev/null +++ b/community/core/formatters/formatters.py @@ -0,0 +1,482 @@ +import logging +import re +from abc import ABC, abstractmethod +from typing import Callable, Optional, Union + +from talon import Context, Module, actions, app, registry +from talon.grammar import Phrase + + +class Formatter(ABC): + def __init__(self, id: str): + self.id = id + + @abstractmethod + def format(self, text: str) -> str: + pass + + @abstractmethod + def unformat(self, text: str) -> str: + pass + + +class CustomFormatter(Formatter): + def __init__( + self, + id: str, + format: Callable[[str], str], + unformat: Optional[Callable[[str], str]] = None, + ): + super().__init__(id) + self._format = format + self._unformat = unformat + + def format(self, text: str) -> str: + return self._format(text) + + def unformat(self, text: str) -> str: + if self._unformat: + return self._unformat(text) + return text + + +class CodeFormatter(Formatter): + def __init__( + self, + id: str, + delimiter: str, + format_first: Callable[[str], str], + format_rest: Callable[[str], str], + ): + super().__init__(id) + self._delimiter = delimiter + self._format_first = format_first + self._format_rest = format_rest + + def format(self, text: str) -> str: + return self._format_delim( + text, self._delimiter, self._format_first, self._format_rest + ) + + def unformat(self, text: str) -> str: + return remove_code_formatting(text) + + def _format_delim( + self, + text: str, + delimiter: str, + format_first: Callable[[str], str], + format_rest: Callable[[str], str], + ): + # Strip anything that is not alpha-num, whitespace, dot or comma + text = re.sub(r"[^\w\d\s.,]+", "", text) + # Split on anything that is not alpha-num + words = re.split(r"([^\w\d]+)", text) + groups = [] + group = [] + first = True + + for word in words: + if word.isspace(): + continue + # Word is number + if word.isnumeric(): + first = True + # Word is symbol + elif not word.isalpha(): + groups.append(delimiter.join(group)) + word = word.strip() + if word != ".": + word += " " + first = True + groups.append(word) + group = [] + continue + elif first: + first = False + if format_first: + word = format_first(word) + elif format_rest: + word = format_rest(word) + group.append(word) + + groups.append(delimiter.join(group)) + return "".join(groups) + + +class TitleFormatter(Formatter): + _words_to_keep_lowercase = ( + "a an and as at but by en for if in nor of on or per the to v via vs".split() + ) + + def format(self, text: str) -> str: + words = [x for x in re.split(r"(\s+)", text) if x] + words = self._title_case_words(words) + return "".join(words) + + def unformat(self, text: str) -> str: + return unformat_upper(text) + + def _title_case_word( + self, word: str, is_first: bool, is_last: bool, following_symbol: bool + ) -> str: + if not word.islower() or ( + word in self._words_to_keep_lowercase + and not is_first + and not is_last + and not following_symbol + ): + return word + + if "-" in word: + words = word.split("-") + words = self._title_case_words(words) + return "-".join(words) + + return word.capitalize() + + def _title_case_words(self, words: list[str]) -> list[str]: + following_symbol = False + for i, word in enumerate(words): + if word.isspace(): + continue + is_first = i == 0 + is_last = i == len(words) - 1 + words[i] = self._title_case_word(word, is_first, is_last, following_symbol) + following_symbol = not word[-1].isalnum() + return words + + +class CapitalizeFormatter(Formatter): + def format(self, text: str) -> str: + return re.sub(r"^\s*\S+", lambda m: capitalize_first(m.group()), text) + + def unformat(self, text: str) -> str: + return unformat_upper(text) + + +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] + 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: + return unformat_upper(text) + + +def capitalize_first(text: str) -> str: + stripped = text.lstrip() + prefix = text[: len(text) - len(stripped)] + return prefix + stripped[:1].upper() + stripped[1:] + + +def capitalize(text: str) -> str: + return text.capitalize() + + +def lower(text: str) -> str: + return text.lower() + + +def unformat_upper(text: str) -> str: + return text.lower() if text.isupper() else text + + +def remove_code_formatting(text: str) -> str: + """Remove format from text""" + # Split on delimiters. + result = re.sub(r"[-_.:/]+", " ", text) + # Split camel case. Including numbers + result = de_camel(result) + # Delimiter/camel case successfully split. Lower case to restore "original" text. + if text != result: + return result.lower() + return text + + +def de_camel(text: str) -> str: + """Replacing camelCase boundaries with blank space""" + Ll = "a-zåäö" + Lu = "A-ZÅÄÖ" + L = f"{Ll}{Lu}" + low_to_upper = rf"(?<=[{Ll}])(?=[{Lu}])" # camel|Case + upper_to_last_upper = rf"(?<=[L{Lu}])(?=[{Lu}][{Ll}])" # IP|Address + letter_to_digit = rf"(?<=[{L}])(?=[\d])" # version|10 + digit_to_letter = rf"(?<=[\d])(?=[{L}])" # 2|x + return re.sub( + rf"{low_to_upper}|{upper_to_last_upper}|{letter_to_digit}|{digit_to_letter}", + " ", + text, + ) + + +formatter_list = [ + CustomFormatter("NOOP", lambda text: text), + CustomFormatter("TRAILING_SPACE", lambda text: f"{text} "), + CustomFormatter("DOUBLE_QUOTED_STRING", lambda text: f'"{text}"'), + CustomFormatter("SINGLE_QUOTED_STRING", lambda text: f"'{text}'"), + CustomFormatter("SPACE_SURROUNDED_STRING", lambda text: f" {text} "), + CustomFormatter("ALL_CAPS", lambda text: text.upper()), + CustomFormatter("ALL_LOWERCASE", lambda text: text.lower()), + CustomFormatter("COMMA_SEPARATED", lambda text: re.sub(r"\s+", ", ", text)), + CustomFormatter("REMOVE_FORMATTING", remove_code_formatting), + TitleFormatter("CAPITALIZE_ALL_WORDS"), + # The sentence formatter being called `CAPITALIZE_FIRST_WORD` is a bit of a misnomer, but kept for backward compatibility. + SentenceFormatter("CAPITALIZE_FIRST_WORD"), + # This is the formatter that actually just capitalizes the first word + CapitalizeFormatter("CAPITALIZE"), + CodeFormatter("NO_SPACES", "", lower, lower), + CodeFormatter("PRIVATE_CAMEL_CASE", "", lower, capitalize), + CodeFormatter("PUBLIC_CAMEL_CASE", "", capitalize, capitalize), + CodeFormatter("SNAKE_CASE", "_", lower, lower), + CodeFormatter("DASH_SEPARATED", "-", lower, lower), + CodeFormatter("DOT_SEPARATED", ".", lower, lower), + CodeFormatter("SLASH_SEPARATED", "/", lower, lower), + CodeFormatter("ALL_SLASHES", "/", lambda text: f"/{text.lower()}", lower), + CodeFormatter("DOUBLE_UNDERSCORE", "__", lower, lower), + CodeFormatter("DOUBLE_COLON_SEPARATED", "::", lower, lower), +] + +formatters_dict = {f.id: f for f in formatter_list} + +mod = Module() +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") + +# The last phrase spoken, without & with formatting. Used for reformatting. +last_phrase = "" +last_phrase_formatted = "" + + +def format_phrase( + m: Union[str, Phrase], formatters: str, unformat: bool = False +) -> str: + global last_phrase, last_phrase_formatted + last_phrase = m + + if isinstance(m, str): + text = m + else: + text = " ".join(actions.dictate.replace_words(actions.dictate.parse_words(m))) + + result = last_phrase_formatted = format_text_without_adding_to_history( + text, formatters, unformat + ) + + actions.user.add_phrase_to_history(result) + # Arguably, we shouldn't be dealing with history here, but somewhere later + # down the line. But we have a bunch of code that relies on doing it this + # way and I don't feel like rewriting it just now. -rntz, 2020-11-04 + return result + + +def format_text_without_adding_to_history( + text: str, formatters: str, unformat: bool = False +) -> str: + """Formats a text according to formatters. formatters is a comma-separated string of formatters (e.g. 'TITLE_CASE,SNAKE_CASE')""" + if not text: + return text + + text, pre, post = shrink_to_string_inside(text) + + for i, formatter_name in enumerate(reversed(formatters.split(","))): + formatter = formatters_dict[formatter_name] + if unformat and i == 0: + text = formatter.unformat(text) + text = formatter.format(text) + + return f"{pre}{text}{post}" + + +string_delimiters = [ + ['"""', '"""'], + ['"', '"'], + ["'", "'"], +] + + +def shrink_to_string_inside(text: str) -> tuple[str, str, str]: + for [left, right] in string_delimiters: + if text.startswith(left) and text.endswith(right): + return text[len(left) : -len(right)], left, right + return text, "", "" + + +@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(list(m)) + + +@mod.capture(rule="{self.code_formatter}+") +def code_formatters(m) -> str: + "Returns a comma-separated string of code formatters e.g. 'SNAKE,DUBSTRING'" + return ",".join(m.code_formatter_list) + + +@mod.capture( + rule=" ( | )*" +) +def format_text(m) -> str: + """Formats text and returns a string""" + out = "" + formatters = m[0] + for chunk in m[1:]: + if isinstance(chunk, ImmuneString): + out += chunk.string + else: + out += format_phrase(chunk, formatters) + return out + + +@mod.capture(rule=" ") +def format_code(m) -> str: + """Formats code and returns a string""" + return format_phrase(m.text, m.code_formatters) + + +class ImmuneString: + """Wrapper that makes a string immune from formatting.""" + + def __init__(self, string): + self.string = string + + +@mod.capture( + # Add anything else into this that you want to have inserted when + # using a prose formatter. + rule="( | (numb | numeral) )" +) +def formatter_immune(m) -> ImmuneString: + """Symbols and numbers that can be interspersed into a prose formatter + (i.e., not dictated immediately after the name of the formatter) + + They will be inserted directly, without being formatted. + + """ + if hasattr(m, "number"): + value = m.number + else: + value = m[0] + 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: + """Formats a phrase according to formatters. formatters is a comma-separated string of formatters (e.g. 'CAPITALIZE_ALL_WORDS,DOUBLE_QUOTED_STRING')""" + return format_phrase(phrase, formatters) + + def insert_formatted(phrase: Union[str, Phrase], formatters: str): + """Inserts a phrase formatted according to formatters. Formatters is a comma separated list of formatters (e.g. 'CAPITALIZE_ALL_WORDS,DOUBLE_QUOTED_STRING')""" + actions.insert(format_phrase(phrase, formatters)) + + def insert_with_history(text: str): + """Inserts some text, remembering it in the phrase history.""" + actions.user.deprecate_action("2022-12-11", "user.insert_with_history") + + actions.user.add_phrase_to_history(text) + actions.insert(text) + + def formatters_reformat_last(formatters: str): + """Clears and reformats last formatted phrase""" + global last_phrase, last_phrase_formatted + if actions.user.get_last_phrase() != last_phrase_formatted: + # The last thing we inserted isn't the same as the last thing we + # formatted, so abort. + logging.warning( + "formatters_reformat_last(): Last phrase wasn't a formatter!" + ) + return + actions.user.clear_last_phrase() + actions.user.insert_formatted(last_phrase, formatters) + + def reformat_text(text: str, formatters: str) -> str: + """Re-formats as """ + return format_phrase(text, formatters, True) + + def formatters_reformat_selection(formatters: str): + """Reformats the current selection as """ + selected = actions.edit.selected_text() + if not selected: + app.notify("Asked to reformat selection, but nothing selected!") + return + # Delete separately for compatibility with programs that don't overwrite + # selected text (e.g. Emacs) + actions.edit.delete() + text = actions.user.reformat_text(selected, formatters) + actions.insert(text) + + def get_formatters_words() -> dict: + """Returns words currently used as formatters, and a demonstration string using those formatters""" + formatters_help_demo = {} + 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 += " *" + formatters_help_demo[phrase] = demo + return formatters_help_demo + + def get_reformatters_words() -> dict: + """Returns words currently used as re-formatters, and a demonstration string using those re-formatters""" + formatters_help_demo = {} + 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 += " *" + formatters_help_demo[phrase] = demo + return formatters_help_demo + + def insert_many(strings: list[str]) -> None: + """Insert a list of strings, sequentially.""" + for string in strings: + actions.insert(string) diff --git a/community/core/formatters/prose_formatter.talon-list b/community/core/formatters/prose_formatter.talon-list new file mode 100644 index 0000000..45c901a --- /dev/null +++ b/community/core/formatters/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/community/core/formatters/reformatter.talon-list b/community/core/formatters/reformatter.talon-list new file mode 100644 index 0000000..712ede5 --- /dev/null +++ b/community/core/formatters/reformatter.talon-list @@ -0,0 +1,4 @@ +list: user.reformatter +- +cap: CAPITALIZE +unformat: REMOVE_FORMATTING diff --git a/community/core/formatters/word_formatter.talon-list b/community/core/formatters/word_formatter.talon-list new file mode 100644 index 0000000..87d9c9e --- /dev/null +++ b/community/core/formatters/word_formatter.talon-list @@ -0,0 +1,7 @@ +list: user.word_formatter +- + +word: NOOP +trot: TRAILING_SPACE +proud: CAPITALIZE_FIRST_WORD +leap: TRAILING_SPACE,CAPITALIZE_FIRST_WORD diff --git a/community/core/help/help.py b/community/core/help/help.py new file mode 100644 index 0000000..75874d8 --- /dev/null +++ b/community/core/help/help.py @@ -0,0 +1,890 @@ +import itertools +import math +import re +from collections import defaultdict +from itertools import islice +from textwrap import wrap +from typing import Any, Iterable, Tuple + +from talon import Context, Module, actions, imgui, registry, settings + +mod = Module() +mod.list("help_contexts", desc="list of available contexts") +mod.tag("help_open", "tag for commands that are available only when help is visible") +mod.setting( + "help_max_contexts_per_page", + type=int, + default=20, + desc="Max contexts to display per page in help", +) +mod.setting( + "help_max_command_lines_per_page", + type=int, + default=50, + desc="Max lines of command to display per page in help", +) +mod.setting( + "help_sort_contexts_by_specificity", + type=bool, + default=True, + desc="If true contexts are sorted by specificity before alphabetically. If false, contexts are just sorted alphabetically.", +) + +ctx = Context() +# context name -> commands +context_command_map = {} + +# rule word -> Set[(context name, rule)] +rule_word_map: dict[str, set[tuple[str, str]]] = defaultdict(set) +search_phrase = None + +# context name -> actual context +context_map = {} + +current_context_page = 1 +# sorted list of diplay names +sorted_display_list = [] +# display names -> context name +display_name_to_context_name_map = {} +selected_context = None +selected_context_page = 1 + +total_page_count = 1 + +cached_active_contexts_list = [] + +live_update = True +show_enabled_contexts_only = False + +selected_list = None +current_list_page = 1 + + +def update_title(): + global live_update + global show_enabled_contexts_only + + if live_update: + if gui_context_help.showing: + if selected_context is None: + refresh_context_command_map(show_enabled_contexts_only) + else: + update_active_contexts_cache(registry.last_active_contexts) + if gui_operators.showing: + update_operators_text() + + +@imgui.open(y=0) +def gui_formatters(gui: imgui.GUI): + global formatters_words + if formatters_reformat: + gui.text("re-formatters help") + else: + gui.text("formatters help") + gui.line() + + for key, val in formatters_words.items(): + gui.text(f"{val}: {key}") + + gui.spacer() + gui.text("* prose formatter") + gui.spacer() + if gui.button("Help close"): + gui_formatters.hide() + + +def update_operators_text(): + """For operators implemented for the active language, map spoken forms including operator prefix to + the operator text for operators implemented as text insertion + or an asterisk for operators implemented as a function call. + """ + global operators_text, total_page_count + try: + operators = actions.user.code_get_operators() + + # Associate the names of the operator lists with the corresponding prefix + op_list_names = ["array", "assignment", "bitwise", "lambda", "math", "pointer"] + names_with_prefix = [(name, "op") for name in op_list_names] + names_with_prefix.append(("math_comparison", "is")) + + # Fill in the list by iterating over the operator lists + operators_text = [] + has_operator_without_text_implementation = False + for name, prefix in names_with_prefix: + operators_list = actions.user.talon_get_active_registry_list( + "user.code_operators_" + name + ) + has_added_first_list_item = False + for operator_name, operator_text in sorted(operators_list.items()): + # Only display operators implemented for the active language + if operator_text in operators: + # If the operator is implemented as text insertion, + # display the operator text + operator = operators.get(operator_text) + if type(operator) == str: + text = ": " + operator + # Otherwise display the operator name from list + else: + has_operator_without_text_implementation = True + text = "*" + # Only add the header if an item in the list is defined in operators + if not has_added_first_list_item: + has_added_first_list_item = True + operators_text.append(f"{name} operators:") + + operators_text.append(f" {prefix} {operator_name}{text}") + if has_operator_without_text_implementation: + operators_text.append( + "* operator is implemented as a function call and cannot be displayed" + ) + page_size = settings.get("user.help_max_command_lines_per_page") + total_page_count = math.ceil(len(operators_text) / page_size) + # This exception will get raised if there is no operators object defined in the active context + except NotImplementedError: + operators_text = None + + +@imgui.open(y=0) +def gui_operators(gui: imgui.GUI): + global operators_text + + if operators_text is None: + gui.text("Help: Operators (1/1)") + gui.line() + gui.text("There is no active programming language when you opened this menu") + gui.text("or the language does not have operator support.") + else: + page_size = settings.get("user.help_max_command_lines_per_page") + page_start = page_size * (current_list_page - 1) + page_end = page_start + page_size + gui.text(f"Help: Operators ({current_list_page}/{total_page_count})") + gui.line() + for text in operators_text[page_start:page_end]: + gui.text(text) + + if total_page_count > 1: + gui.spacer() + if gui.button("Help next"): + actions.user.help_next() + + if gui.button("Help previous"): + actions.user.help_previous() + gui.spacer() + if gui.button("Help close"): + gui_operators.hide() + + +def format_context_title(context_name: str) -> str: + global cached_active_contexts_list + return "{} [{}]".format( + context_name, + ( + "ACTIVE" + if context_map.get(context_name, None) in cached_active_contexts_list + else "INACTIVE" + ), + ) + + +def format_context_button(index: int, context_label: str, context_name: str) -> str: + global cached_active_contexts_list + global show_enabled_contexts_only + + if not show_enabled_contexts_only: + return "{}. {}{}".format( + index, + context_label, + ( + "*" + if context_map.get(context_name, None) in cached_active_contexts_list + else "" + ), + ) + else: + return f"{index}. {context_label} " + + +# translates 1-based index -> actual index in sorted_context_map_keys +def get_context_page(index: int) -> int: + return math.ceil(index / settings.get("user.help_max_contexts_per_page")) + + +def get_total_context_pages() -> int: + return math.ceil( + len(sorted_display_list) / settings.get("user.help_max_contexts_per_page") + ) + + +def get_current_context_page_length() -> int: + start_index = (current_context_page - 1) * settings.get( + "user.help_max_contexts_per_page" + ) + return len( + sorted_display_list[ + start_index : start_index + settings.get("user.help_max_contexts_per_page") + ] + ) + + +def get_command_line_count(command: tuple[str, str]) -> int: + """This should be kept in sync with draw_commands""" + _, body = command + lines = len(body.split("\n")) + if lines == 1: + return 1 + else: + return lines + 1 + + +def get_pages(item_line_counts: list[int]) -> list[int]: + """Given some set of indivisible items with given line counts, + return the page number each item should appear on. + + If an item will cross a page boundary, it is moved to the next page, + so that pages may be shorter than the maximum lenth, but not longer. The only + exception is when an item is longer than the maximum page length, in which + case that item will be placed on a longer page. + """ + current_page_line_count = 0 + current_page = 1 + pages = [] + for line_count in item_line_counts: + if line_count + current_page_line_count > settings.get( + "user.help_max_command_lines_per_page" + ): + if current_page_line_count == 0: + # Special case, render a larger page. + page = current_page + current_page_line_count = 0 + else: + page = current_page + 1 + current_page_line_count = line_count + current_page += 1 + else: + current_page_line_count += line_count + page = current_page + pages.append(page) + return pages + + +@imgui.open(y=0) +def gui_context_help(gui: imgui.GUI): + global context_command_map + global current_context_page + global selected_context + global selected_context_page + global sorted_display_list + global show_enabled_contexts_only + global cached_active_contexts_list + global total_page_count + global search_phrase + + # if no selected context, draw the contexts + if selected_context is None and search_phrase is None: + total_page_count = get_total_context_pages() + + if not show_enabled_contexts_only: + gui.text( + f"Help: All ({current_context_page}/{total_page_count}) (* = active)" + ) + else: + gui.text( + "Help: Active Contexts Only ({}/{})".format( + current_context_page, total_page_count + ) + ) + + gui.line() + + current_item_index = 1 + current_selection_index = 1 + current_group = "" + for display_name, group, _ in sorted_display_list: + target_page = get_context_page(current_item_index) + context_name = display_name_to_context_name_map[display_name] + if current_context_page == target_page: + if current_group != group: + if current_group: + gui.line() + gui.text(f"{group}:") + current_group = group + + button_name = format_context_button( + current_selection_index, + display_name, + context_name, + ) + + if gui.button(button_name): + selected_context = context_name + current_selection_index = current_selection_index + 1 + + current_item_index += 1 + + if total_page_count > 1: + gui.spacer() + if gui.button("Help next"): + actions.user.help_next() + + if gui.button("Help previous"): + actions.user.help_previous() + + # if there's a selected context, draw the commands for it + else: + if selected_context is not None: + draw_context_commands(gui) + elif search_phrase is not None: + draw_search_commands(gui) + + gui.spacer() + if total_page_count > 1: + if gui.button("Help next"): + actions.user.help_next() + + if gui.button("Help previous"): + actions.user.help_previous() + + if gui.button("Help return"): + actions.user.help_return() + + if gui.button("Help refresh"): + actions.user.help_refresh() + + if gui.button("Help close"): + actions.user.help_hide() + + +def draw_context_commands(gui: imgui.GUI): + global selected_context + global total_page_count + global selected_context_page + + context_title = format_context_title(selected_context) + title = f"Context: {context_title}" + commands = context_command_map[selected_context].items() + item_line_counts = [get_command_line_count(command) for command in commands] + pages = get_pages(item_line_counts) + total_page_count = max(pages, default=1) + draw_commands_title(gui, title) + + filtered_commands = [ + command + for command, page in zip(commands, pages) + if page == selected_context_page + ] + + draw_commands(gui, filtered_commands) + + +def draw_search_commands(gui: imgui.GUI): + global search_phrase + global total_page_count + global cached_active_contexts_list + global selected_context_page + + title = f"Search: {search_phrase}" + commands_grouped = get_search_commands(search_phrase) + commands_flat = list(itertools.chain.from_iterable(commands_grouped.values())) + + sorted_commands_grouped = sorted( + commands_grouped.items(), + key=lambda item: context_map[item[0]] not in cached_active_contexts_list, + ) + + pages = get_pages( + [ + sum(get_command_line_count(command) for command in commands) + 3 + for _, commands in sorted_commands_grouped + ] + ) + total_page_count = max(pages, default=1) + + draw_commands_title(gui, title) + + current_item_index = 1 + for (context, commands), page in zip(sorted_commands_grouped, pages): + if page == selected_context_page: + gui.text(format_context_title(context)) + gui.line() + draw_commands(gui, commands) + gui.spacer() + + +def get_search_commands(phrase: str) -> dict[str, tuple[str, str]]: + global rule_word_map + tokens = search_phrase.split(" ") + + viable_commands = rule_word_map[tokens[0]] + for token in tokens[1:]: + viable_commands &= rule_word_map[token] + + # sets have no stable sort order, unlike dicts + viable_commands = list(viable_commands) + viable_commands.sort() + + commands_grouped = defaultdict(list) + for context, rule in viable_commands: + command = context_command_map[context][rule] + commands_grouped[context].append((rule, command)) + + return commands_grouped + + +def draw_commands_title(gui: imgui.GUI, title: str): + global selected_context_page + global total_page_count + + gui.text(f"{title} ({selected_context_page}/{total_page_count})") + gui.line() + + +def draw_commands(gui: imgui.GUI, commands: Iterable[tuple[str, str]]): + for key, val in commands: + val = val.split("\n") + if len(val) > 1: + gui.text(f"{key}:") + for line in val: + gui.text(f" {line}") + else: + gui.text(f"{key}: {val[0]}") + + +def reset(): + global current_context_page + global sorted_display_list + global selected_context + global search_phrase + global selected_context_page + global show_enabled_contexts_only + global display_name_to_context_name_map + global selected_list + global current_list_page + + current_context_page = 1 + sorted_display_list = [] + selected_context = None + search_phrase = None + selected_context_page = 1 + show_enabled_contexts_only = False + display_name_to_context_name_map = {} + selected_list = None + current_list_page = 1 + + +def update_active_contexts_cache(active_contexts): + # print("update_active_contexts_cache") + global cached_active_contexts_list + cached_active_contexts_list = active_contexts + + +# example usage todo: make a list definable in .talon +# overrides = {"generic browser": "broswer"} +overrides = {} + + +def refresh_context_command_map(enabled_only=False): + active_contexts = registry.last_active_contexts + + local_context_map = {} + local_display_name_to_context_name_map = {} + local_context_command_map = {} + cached_short_context_names = {} + + for context_name, context in registry.contexts.items(): + splits = context_name.split(".") + + if "talon" == splits[-1]: + display_name = splits[-2].replace("_", " ") + + short_names = actions.user.create_spoken_forms( + display_name, + generate_subsequences=False, + ) + + if short_names[0] in overrides: + short_names = [overrides[short_names[0]]] + elif len(short_names) == 2 and short_names[1] in overrides: + short_names = [overrides[short_names[1]]] + + if enabled_only and context in active_contexts or not enabled_only: + local_context_command_map[context_name] = {} + for command_alias, val in context.commands.items(): + if command_alias in registry.commands or not enabled_only: + local_context_command_map[context_name][ + str(val.rule.rule) + ] = val.target.code + if len(local_context_command_map[context_name]) == 0: + local_context_command_map.pop(context_name) + else: + for short_name in short_names: + cached_short_context_names[short_name] = context_name + + # the last entry will contain no symbols + local_display_name_to_context_name_map[display_name] = context_name + local_context_map[context_name] = context + + # Update all the global state after we've performed our calculations + global context_map + global context_command_map + global sorted_display_list + global show_enabled_contexts_only + global display_name_to_context_name_map + global rule_word_map + + context_map = local_context_map + context_command_map = local_context_command_map + sorted_display_list = get_sorted_display_keys( + local_context_map, + local_display_name_to_context_name_map, + ) + show_enabled_contexts_only = enabled_only + display_name_to_context_name_map = local_display_name_to_context_name_map + rule_word_map = refresh_rule_word_map(local_context_command_map) + + ctx.lists["self.help_contexts"] = cached_short_context_names + update_active_contexts_cache(active_contexts) + + +def get_sorted_display_keys( + context_map: dict[str, Any], + display_name_to_context_name_map: dict[str, str], +): + if settings.get("user.help_sort_contexts_by_specificity"): + return get_sorted_keys_by_context_specificity( + context_map, + display_name_to_context_name_map, + ) + return [ + (display_name, "", 0) + for display_name in sorted(display_name_to_context_name_map.keys()) + ] + + +def get_sorted_keys_by_context_specificity( + context_map: dict[str, Any], + display_name_to_context_name_map: dict[str, str], +) -> list[Tuple[str, str, int]]: + def get_group(display_name) -> Tuple[str, str, int]: + try: + context_name = display_name_to_context_name_map[display_name] + context = context_map[context_name] + keys = context._match.keys() + if any(key for key in keys if key.startswith("app.")): + return (display_name, "Application-specific", 2) + if keys: + return (display_name, "Context-dependent", 1) + return (display_name, "Global", 0) + except Exception as ex: + return (display_name, "", 0) + + grouped_list = [ + get_group(display_name) + for display_name in display_name_to_context_name_map.keys() + ] + return sorted( + grouped_list, + key=lambda item: (-item[2], item[0]), + ) + + +def refresh_rule_word_map(context_command_map): + rule_word_map = defaultdict(set) + + for context_name, commands in context_command_map.items(): + for rule in commands: + tokens = {token for token in re.split(r"\W+", rule) if token.isalpha()} + for token in tokens: + rule_word_map[token].add((context_name, rule)) + + return rule_word_map + + +events_registered = False + + +def register_events(register: bool): + global events_registered + if register: + if not events_registered and live_update: + events_registered = True + # registry.register('post:update_contexts', contexts_updated) + registry.register("update_commands", commands_updated) + else: + events_registered = False + # registry.unregister('post:update_contexts', contexts_updated) + registry.unregister("update_commands", commands_updated) + + +def hide_all_help_guis(): + gui_context_help.hide() + gui_formatters.hide() + gui_list_help.hide() + gui_operators.hide() + + +def paginate_list(data, SIZE=None): + chunk_size = SIZE or settings.get("user.help_max_command_lines_per_page") + it = iter(data) + for i in range(0, len(data), chunk_size): + yield {k: data[k] for k in islice(it, chunk_size)} + + +def draw_list_commands(gui: imgui.GUI): + global selected_list + global total_page_count + global selected_context_page + + talon_list = actions.user.talon_get_active_registry_list(selected_list) + # numpages = math.ceil(len(talon_list) / SIZE) + + pages_list = [] + + for item in paginate_list(talon_list): + pages_list.append(item) + # print(pages_list) + + total_page_count = len(pages_list) + return pages_list + + +@imgui.open(y=0) +def gui_list_help(gui: imgui.GUI): + global total_page_count + global current_list_page + global selected_list + + pages_list = draw_list_commands(gui) + total_page_count = len(pages_list) + # print(pages_list[current_page]) + + if total_page_count == 0: + page_info = "empty" + else: + page_info = f"{current_list_page}/{total_page_count}" + + gui.text(f"List: {selected_list} ({page_info})") + + # Extract description from list declaration, i.e. mod.list(..., desc=...)) + if (desc := registry.decls.lists[selected_list].desc) is not None: + for line in wrap(desc): + gui.text(line) + + gui.line() + + if len(pages_list) > 0: + for key, value in pages_list[current_list_page - 1].items(): + gui.text(f"{value}: {key}") + + gui.spacer() + + if total_page_count > 1: + if gui.button("Help next"): + actions.user.help_next() + + if gui.button("Help previous"): + actions.user.help_previous() + + if gui.button("Help return"): + actions.user.help_return() + + if gui.button("Help refresh"): + actions.user.help_refresh() + + if gui.button("Help close"): + actions.user.help_hide() + + +@mod.action_class +class Actions: + def help_list(ab: str): + """Provides the symbol dictionary""" + # what you say is stored as a trigger + global selected_list + reset() + selected_list = ab + gui_list_help.show() + register_events(True) + ctx.tags = ["user.help_open"] + + def help_formatters(ab: dict, reformat: bool): + """Provides the list of formatter keywords""" + # what you say is stored as a trigger + global formatters_words, formatters_reformat + formatters_words = ab + formatters_reformat = reformat + reset() + hide_all_help_guis() + gui_formatters.show() + register_events(False) + ctx.tags = ["user.help_open"] + + def help_operators(): + """Displays the list of operator names""" + reset() + hide_all_help_guis() + update_operators_text() + gui_operators.show() + register_events(True) + ctx.tags = ["user.help_open"] + + def help_context_enabled(): + """Display contextual command info""" + reset() + refresh_context_command_map(enabled_only=True) + hide_all_help_guis() + gui_context_help.show() + register_events(True) + ctx.tags = ["user.help_open"] + + def help_context(): + """Display contextual command info""" + reset() + refresh_context_command_map() + hide_all_help_guis() + gui_context_help.show() + register_events(True) + ctx.tags = ["user.help_open"] + + def help_search(phrase: str): + """Display command info for search phrase""" + global search_phrase + + reset() + search_phrase = phrase + refresh_context_command_map() + hide_all_help_guis() + gui_context_help.show() + register_events(True) + ctx.tags = ["user.help_open"] + + def help_selected_context(m: str): + """Display command info for selected context""" + global selected_context + global selected_context_page + + if not gui_context_help.showing: + reset() + refresh_context_command_map() + else: + selected_context_page = 1 + update_active_contexts_cache(registry.last_active_contexts) + + selected_context = m + hide_all_help_guis() + gui_context_help.show() + register_events(True) + ctx.tags = ["user.help_open"] + + def help_next(): + """Navigates to next page""" + global current_context_page + global selected_context + global selected_context_page + global total_page_count + + global current_list_page + + if gui_context_help.showing: + if selected_context is None and search_phrase is None: + if current_context_page != total_page_count: + current_context_page += 1 + else: + current_context_page = 1 + else: + if selected_context_page != total_page_count: + selected_context_page += 1 + else: + selected_context_page = 1 + + if gui_list_help.showing or gui_operators.showing: + if current_list_page != total_page_count: + current_list_page += 1 + else: + current_list_page = 1 + + def help_select_index(index: int): + """Select the context by a number""" + global sorted_display_list, selected_context + if gui_context_help.showing: + if index < settings.get("user.help_max_contexts_per_page") and ( + (current_context_page - 1) + * settings.get("user.help_max_contexts_per_page") + + index + < len(sorted_display_list) + ): + if selected_context is None: + selected_context = display_name_to_context_name_map[ + sorted_display_list[ + (current_context_page - 1) + * settings.get("user.help_max_contexts_per_page") + + index + ][0] + ] + + def help_previous(): + """Navigates to previous page""" + global current_context_page + global selected_context + global selected_context_page + global total_page_count + + global current_list_page + + if gui_context_help.showing: + if selected_context is None and search_phrase is None: + if current_context_page != 1: + current_context_page -= 1 + else: + current_context_page = total_page_count + + else: + if selected_context_page != 1: + selected_context_page -= 1 + else: + selected_context_page = total_page_count + + if gui_list_help.showing or gui_operators.showing: + if current_list_page != total_page_count: + current_list_page -= 1 + else: + current_list_page = 1 + + def help_return(): + """Returns to the main help window""" + global selected_context + global selected_context_page + global show_enabled_contexts_only + + if gui_context_help.showing: + refresh_context_command_map(show_enabled_contexts_only) + selected_context_page = 1 + selected_context = None + + def help_refresh(): + """Refreshes the help""" + global show_enabled_contexts_only + global selected_context + + if gui_context_help.showing: + if selected_context is None: + refresh_context_command_map(show_enabled_contexts_only) + else: + update_active_contexts_cache(registry.last_active_contexts) + + def help_hide(): + """Hides the help""" + reset() + + # print("help_hide - alphabet gui_alphabet: {}".format(gui_alphabet.showing)) + # print( + # "help_hide - gui_context_help showing: {}".format(gui_context_help.showing) + # ) + + hide_all_help_guis() + refresh_context_command_map() + register_events(False) + ctx.tags = [] + + +def commands_updated(_): + update_title() diff --git a/community/core/help/help.talon b/community/core/help/help.talon new file mode 100644 index 0000000..54c5146 --- /dev/null +++ b/community/core/help/help.talon @@ -0,0 +1,25 @@ +help alphabet: user.help_list("user.letter") +help symbols: user.help_list("user.symbol_key") +help numbers: user.help_list("user.number_key") +help punctuation: user.help_list("user.punctuation") +help modifier: user.help_list("user.modifier_key") +help special keys: user.help_list("user.special_key") +help function keys: user.help_list("user.function_key") +help arrows: user.help_list("user.arrow_key") +help context$: user.help_context() +help active$: user.help_context_enabled() +help search $: user.help_search(text) +help clip search: user.help_search(clip.text()) +help this search: user.help_search(edit.selected_text()) +help context {user.help_contexts}$: user.help_selected_context(help_contexts) +help help: user.help_search("help") +help scope$: user.help_scope_toggle() +help snip: user.help_list("user.snippet") +help operators: user.help_operators() +help keywords: user.help_list("user.code_keyword") +help keywords unprefixed: user.help_list("user.code_keyword_unprefixed") + +(help formatters | help format | format help): + user.help_formatters(user.get_formatters_words(), false) +(help re formatters | help re format | re format help): + user.help_formatters(user.get_reformatters_words(), true) diff --git a/community/core/help/help_open.talon b/community/core/help/help_open.talon new file mode 100644 index 0000000..f46bde2 --- /dev/null +++ b/community/core/help/help_open.talon @@ -0,0 +1,8 @@ +tag: user.help_open +- +help next$: user.help_next() +help (previous | last)$: user.help_previous() +help $: user.help_select_index(number - 1) +help return$: user.help_return() +help refresh$: user.help_refresh() +help close$: user.help_hide() diff --git a/community/core/help/help_scope.py b/community/core/help/help_scope.py new file mode 100644 index 0000000..0478573 --- /dev/null +++ b/community/core/help/help_scope.py @@ -0,0 +1,70 @@ +from talon import Context, Module, actions, imgui, scope, settings, ui + +ctx = Context() +mod = Module() +mod.tag("help_scope_open", "tag for showing the scope help gui") + +mod.setting( + "help_scope_max_length", + type=int, + default=50, +) + + +@imgui.open(x=ui.main_screen().x) +def gui(gui: imgui.GUI): + gui.text("Scope") + gui.line() + gui.spacer() + gui.text("Modes") + gui.line() + for mode in sorted(scope.get("mode")): + gui.text(mode) + gui.spacer() + gui.text("Tags") + gui.line() + for tag in sorted(scope.get("tag")): + gui.text(tag) + gui.spacer() + gui.text("Misc") + gui.line() + ignore = {"main", "mode", "tag"} + keys = {*scope.data.keys(), *scope.data["main"].keys()} + for key in sorted(keys): + if key not in ignore: + value = scope.get(key) + print_value(gui, key, value, ignore) + gui.spacer() + if gui.button("Hide"): + actions.user.help_scope_toggle() + + +def print_value(gui: imgui.GUI, path: str, value, ignore: set[str] = {}): + if isinstance(value, dict): + for key in value: + if key not in ignore: + p = f"{path}.{key}" if path else key + print_value(gui, p, value[key]) + elif value: + gui.text(f"{path}: {format_value(value)}") + + +def format_value(value): + if isinstance(value, (list, set)): + value = ", ".join(sorted(value)) + setting_max_length = settings.get("user.help_scope_max_length") + if isinstance(value, str) and len(value) > setting_max_length + 4: + return f"{value[:setting_max_length]} ..." + return value + + +@mod.action_class +class Actions: + def help_scope_toggle(): + """Toggle help scope gui""" + if gui.showing: + ctx.tags = [] + gui.hide() + else: + ctx.tags = ["user.help_scope_open"] + gui.show() diff --git a/community/core/help/help_scope_open.talon b/community/core/help/help_scope_open.talon new file mode 100644 index 0000000..2e42b84 --- /dev/null +++ b/community/core/help/help_scope_open.talon @@ -0,0 +1,4 @@ +tag: user.help_scope_open +- + +scope (hide | close)$: user.help_scope_toggle() diff --git a/community/core/homophones/homophones.csv b/community/core/homophones/homophones.csv new file mode 100644 index 0000000..f187430 --- /dev/null +++ b/community/core/homophones/homophones.csv @@ -0,0 +1,677 @@ +Aaron,Erin +able,Abel +acclamation,acclimation +acts,ax +Adam,atom +add,ad +addition,edition +adds,ads,adz +adduce,educe +adherence,adherents +ado,adieu +aerial,ariel +affected,effected +afterward,afterword +aid,aide +air,heir,err +ale,ail +align,a line,aline +all,awl +allowed,aloud +alluded,eluded +alter,altar +analyst,annalist +appetite,apatite +apprize,apprise +arc,ark +ascent,assent +assistance,assistants +attendance,attendants +augur,auger +aunt,ant +away,aweigh +axis,axes +axle,axel +Babel,babble +bad,bade,bed +bah,baa +bail,bale,baal +bait,bate +baited,bated +bald,balled,bawled +ball,bawl +band,banned +baron,barren +barred,bard +barrel,beryl +base,bass +based,baste +baseline,bassline +basil,basal +basis,bases +basque,bask +be,bee +beach,beech +bear,bare +beat,beet +been,bin +beer,bier +beetle,beatle +bell,belle +better,bettor +bib,bibb +bird,burred +birth,berth +bite,byte,bight +bizarre,bazaar +block,bloc +blue,blew +board,bored +bold,bowled +bomb,balm,bombe +booty,bootie +border,boarder +bore,boar +born,borne +borough,burrow,burro +bought,bot +boulder,bolder +bow,beau +bow,bough +bowed,bode +boy,buoy +braid,brayed +brays,braise +breach,breech +bread,bred +break,brake +brewed,brood +brews,bruise +bridal,bridle +broach,brooch +browse,brows +brute,brut +build,billed +bullion,bouillon,boolean +burger,burgher +bury,berry +bust,bussed +but,butt +by,buy,bye +cached,cashed +caches,cashes +caching,cashing +caddy,caddie +Cain,cane +calendar,calender +callous,callus +cannon,canon +cantor,canter +canvas,canvass +capital,capitol +carol,carrel +carrot,carat,caret,karat +cash,cache +cast,caste +caster,castor +cause,caws +cedar,seeder +ceiling,sealing +chance,chants +chased,chaste +chauffeur,shofar +cheap,cheep +check,Czech +chic,sheik +Chile,chili,chilly +choir,quire +choose,chews +cited,sided,sighted +clack,claque +clammer,clamor,clamber +clause,claws +click,clique +climb,clime +close,clothes,cloze +clue,clew +coal,cole +coarser,courser +coat,cote +coax,cokes +collared,collard +complacent,complaisant +complement,compliment +conceded,conceited +consonants,consonance +consul,console,consol +continents,continence +cops,copse +coral,choral +cord,chord,cored +core,corps +coughers,coffers +coulee,coolie +council,counsel +coup,coo +coupe,coop +course,coarse +cousin,cozen +coward,cowered +coy,koi +craft,kraft +crawl,kraal +creek,creak +crepe,crape +cruel,crewel +cruise,crews +current,currant +cursor,curser +Cyprus,cypress +damn,dam +Dane,deign +days,daze +dear,deer +dense,dents +descent,dissent +die,dye +diffused,defused +discrete,discreet +disperse,disburse +do,dough,doe +do,due,dew +doc,dock +does,doze +done,dun +draft,draught +dual,duel +ducked,duct +ducks,ducts +dying,dyeing +earn,urn +effect,affect +effects,affects +eight,ate +eke,eek +elude,allude +elusive,illusive,allusive +emend,amend +end,and +ensure,insure +errant,arrant +eve,eave +exceed,accede +except,accept +excepting,accepting +exercise,exorcise +eyes,ayes +facts,fax +faint,feint +fair,fare +fairy,ferry +fawn,faun +feet,feat +fens,fends +fete,fate +few,phew +find,fined +finish,Finnish +fish,phish +fished,phished +fisher,fissure +fishing,phishing +flare,flair +flee,flea +flew,flu,flue +flocks,phlox +flow,floe +flower,flour +flyer,flier +foe,faux +fold,foaled +for,four,fore +foregone,forgone +fort,forte +forward,foreword +foul,fowl +fourth,forth +frank,franc +freeze,frees,frieze +friar,fryer +fur,fir +gaffe,gaff +gale,Gail +gamble,gambol +gate,gait +gator,gaiter +gauge,gage +gel,jell +gene,Jean +gilder,guilder +gnome,Nome +gopher,gofer +gorilla,guerilla +gourd,gored +grade,grayed +graft,graphed +graham,gram +graze,grays +great,grate +greater,grater +Greece,grease +grill,grille +grizzly,grisly +grown,groan +guest,guessed +guild,gild +guilt,gilt +hail,hale +hair,hare +hall,haul +handmade,handmaid +handsome,hansom +hangar,hanger +have,halve +haze,hays +he'd,heed +he'll,heal,heel +heard,herd +heart,hart +here,hear +heroin,heroine +hey,hay +high,hi +higher,hire +him,hymn +ho,hoe +hold,holed +holy,wholly,holey +hoop,whoop +horde,hoard +horse,hoarse +hose,hoes +hostile,hostel +Hugh,hue,hew +humorous,humerus +hurts,hertz +I,eye,aye +I'd,eyed +I'll,aisle,isle +idle,idol,idyll +illicit,elicit +illusion,allusion +imminent,immanent +impassable,impassible +in,inn +innocence,innocents +innumerable,enumerable +insight,incite +instance,instants +intense,intents +islet,eyelet +its,it's +jam,jamb +jibe,gibe +Jim,gym +jinx,jinks +json,jason +kernel,colonel +knickers,nickers +knit,nit +knock,nock +knows,nose +lacks,lax +laid,lade +lama,llama +lane,lain +laps,lapse,Lapps +latter,ladder +lay,lei +lays,leis,laze +leach,leech +lead,led +leak,leek +lean,lien +least,leased +lee,lea +lens,lends +lesson,lessen +let's,lets +levy,levee +liar,lyre,lier +lie,lye +light,lite +liken,lichen +links,lynx +literal,littoral +load,lode,lowed +loan,lone +loathe,loath +loch,lock +lochs,locks,lox +loot,lute +Lou,lieu +low,lo +lumber,lumbar +made,maid +main,Maine,mane +male,mail +mall,maul +manner,manor +mantle,mantel +mark,marc +martial,marshal +martin,marten +Mary,marry,merry +mast,massed +mat,matte +matter,madder +mayor,mare +maze,maize +mean,mien +meet,meat,mete +metal,medal,meddle,mettle +meteor,meatier +might,mite +mill,mil +mince,mints +mind,mined +minor,miner +missed,mist +missile,missal +moan,mown +moat,mote +mode,mowed +mood,mooed +moose,mousse +morning,mourning +mourn,morn +Mrs,misses +mucus,mucous +mule,mewl +muscle,mussel +muse,mews +must,mussed +mustard,mustered +nap,knap +naval,navel +nave,knave +nay,neigh +need,knead,kneed +new,knew,gnu +nice,gneiss +Nice,niece +night,knight +no,know +none,nun +not,knot +oh,owe +one,won +or,ore,oar +oral,aural +oriole,aureole +our,hour +ours,hours +outcast,outcaste +overdue,overdo +overseas,oversees +owed,ode +packed,pact +paean,peon,paeon +pain,pane +pair,pear,pare +pale,pail +palette,palate,pallet +parish,perish +parley,parlay +past,passed +paste,paced +patients,patience +patted,padded +Paul,pall +pause,paws +peak,peek,pique +peas,pees +pedal,peddle,petal +pee,pea +peel,peal +peer,pier +penance,pennants +per,purr +perl,pearl,purl +pervade,purveyed +Pete,peat +petrol,petrel +pew,pugh +phase,faze +Phil,fill +phrase,frays +picot,pekoe +pie,pi +piece,peace +pigeon,pidgin +pilot,Pilate +pistol,pistil +plane,plain +plaque,plack +plate,plait +please,pleas +plum,plumb +poll,pole +pour,pore +praise,prays,preys +pray,prey +precedence,precedents +premier,premiere +presence,presents +pride,pried +primer,primmer +prince,prints +principle,principal +profit,prophet +pros,prose +pull,pool +quartz,quarts +queue,cue +queues,cues +quince,quints +rabbit,rabbet +rack,wrack +rain,reign,rein +raise,rays,raze +read,red +read,reed +real,reel +residents,residence +rest,wrest +review,revue +right,write,rite +rights,writes,rites +rigor,rigger +ring,wring +road,rode,rowed +roads,rhodes +role,roll +Rome,roam +room,rheum +rose,rows +rot,wrought +rough,ruff +route,root +row,roe +rude,rued +rue,roux +rumor,roomer +rung,wrung +Russell,rustle +rye,wry +sachet,sashay +sack,sac +sacks,sax +sale,sail +sane,seine +saver,savor +scalar,scaler +see,sea +seed,cede +seem,seam +seen,scene +seer,sear,sere +sees,seize,seas +sell,cell +seller,cellar +sense,cents,scents +sensor,censor +sent,cent,scent +serial,cereal +series,Ceres +session,cession +sewing,sowing +sheer,shear +shoe,shoo +shoot,chute +shown,shone +sick,sic +side,sighed +signet,cygnet +sink,sync +sinking,syncing +site,sight,cite +size,sighs +skull,scull +slay,sleigh +slew,slough,slue +slight,sleight +slow,sloe +so,sow,sew +sold,soled +some,sum +son,sun +sore,soar +sorry,sari +soul,sole +spade,spayed +stake,steak +stare,stair +stationary,stationery +stayed,staid +steel,steal +step,steppe +straight,strait +straightened,straitened +style,stile +sue,Sioux +suit,soot +summary,summery +Sunday,sundae +surf,serf +surge,serge +swayed,suede +sweet,suite +sword,soared +symbol,cymbal +tacked,tact +tale,tail +talk,tock +taper,tapir +taught,taut +taupe,tope +tax,tacks +tea,tee +team,teem +tear,tare +tear,tier +tease,teas,tees +tens,tends +tense,tents +terry,tarry +than,then +the,thee +their,there,they're +there's,theirs +through,threw +thrown,throne +throws,throes +tick,tic +tie,Thai +tied,tide +Tigris,tigress +timber,timbre +time,thyme +to,two,too +toe,tow +told,tolled +tool,tulle +tort,torte +torturous,tortuous +towed,toad,toed +tracked,tract +trader,traitor +troop,troupe +trust,trussed +tucks,tux +turbine,turban +turn,tern +tutor,Tudor,tooter +undo,undue +use,ewes,yews +utter,udder +vain,vein,vane +valence,valance +variants,variance +veil,vale +vein,vane +Venus,venous +versus,verses +very,vary +vice,vise +vile,vial +wade,weighed +wait,weight +waiter,wader +Wales,whales,wails +walk,wok +want,wont +war,wore +ward,word +waste,waist +wave,waive +wax,whacks +way,weigh,whey +Wayne,wane,wain +ways,weighs +we,wee +we'd,weed +we'll,wheel +we've,weave +wears,where's,wares +week,weak +weekly,weakly +were,whir,we're +wet,whet +whale,wail,wale +wheeled,wield +where,wear,ware +whether,weather,wether +whether,weather +which,witch +while,wile +whoa,woe +whole,hole +whose,who's +wind,whined,wined +wine,whine +with,width +word,whirred +world,whirled,whorled +worn,warn +would,wood +wrap,rap +wrapped,rapped,rapt +wrapper,rapper +wreak,reek +wretch,retch +wrote,rote +yoke,yolk +you,yew,ewe +you'll,Yule +your,you're,yore diff --git a/community/core/homophones/homophones.py b/community/core/homophones/homophones.py new file mode 100644 index 0000000..d415995 --- /dev/null +++ b/community/core/homophones/homophones.py @@ -0,0 +1,243 @@ +import os + +from talon import Context, Module, actions, app, clip, fs, imgui, ui + +######################################################################## +# global settings +######################################################################## + +# a list of homophones where each line is a comma separated list +# e.g. where,wear,ware +# a suitable one can be found here: +# https://github.com/pimentel/homophones +cwd = os.path.dirname(os.path.realpath(__file__)) +homophones_file = os.path.join(cwd, "homophones.csv") +# if quick_replace, then when a word is selected and only one homophone exists, +# replace it without bringing up the options +quick_replace = True +show_help = False +######################################################################## + +ctx = Context() +mod = Module() + +mod.list("homophones_canonicals", desc="list of words ") +mod.tag( + "homophones_open", + desc="Tag for enabling homophones commands when the associated gui is open", +) + +main_screen = ui.main_screen() + + +def update_homophones(name, flags): + if name != homophones_file: + return + + phones = {} + canonical_list = [] + with open(homophones_file) as f: + for line in f: + words = line.rstrip().split(",") + words = [x for x in words if x.strip() != ""] + canonical_list.append(words[0]) + merged_words = set(words) + for word in words: + old_words = phones.get(word.lower(), []) + merged_words.update(old_words) + merged_words = sorted(merged_words) + for word in merged_words: + phones[word.lower()] = merged_words + + global all_homophones + all_homophones = phones + ctx.lists["self.homophones_canonicals"] = canonical_list + + +update_homophones(homophones_file, None) +fs.watch(cwd, update_homophones) +active_word_list = None +is_selection = False + + +def close_homophones(): + gui.hide() + ctx.tags = [] + + +PHONES_FORMATTERS = [ + lambda word: word.capitalize(), + lambda word: word.upper(), +] + + +def find_matching_format_function(word_with_formatting, format_functions): + """Finds the formatter function from a list of formatter functions which transforms a word into itself. + Returns an identity function if none exists""" + for formatter in format_functions: + formatted_word = formatter(word_with_formatting) + if word_with_formatting == formatted_word: + return formatter + + return lambda word: word + + +def raise_homophones(word_to_find_homophones_for, forced=False, selection=False): + global quick_replace + global active_word_list + global show_help + global force_raise + global is_selection + + force_raise = forced + is_selection = selection + + if is_selection: + word_to_find_homophones_for = word_to_find_homophones_for.strip() + formatter = find_matching_format_function( + word_to_find_homophones_for, PHONES_FORMATTERS + ) + + word_to_find_homophones_for = word_to_find_homophones_for.lower() + + # We support plurals, but very naively. If we can't find your word but your word ends in an s, presume its plural + # and attempt to find the singular, then present the presumed plurals back. This could be improved! + if word_to_find_homophones_for in all_homophones: + valid_homophones = all_homophones[word_to_find_homophones_for] + elif ( + word_to_find_homophones_for.endswith("s") + and word_to_find_homophones_for[:-1] in all_homophones + ): + valid_homophones = map( + lambda w: w + "s", all_homophones[word_to_find_homophones_for[:-1]] + ) + else: + app.notify( + "homophones.py", f'"{word_to_find_homophones_for}" not in homophones list' + ) + return + + # Move current word to end of list to reduce searcher's cognitive load + valid_homophones_reordered = list( + filter( + lambda word_from_list: word_from_list.lower() + != word_to_find_homophones_for, + valid_homophones, + ) + ) + [word_to_find_homophones_for] + active_word_list = list(map(formatter, valid_homophones_reordered)) + + if ( + is_selection + and len(active_word_list) == 2 + and quick_replace + and not force_raise + ): + if word_to_find_homophones_for == active_word_list[0].lower(): + new = active_word_list[1] + else: + new = active_word_list[0] + + clip.set(new) + actions.edit.paste() + return + + ctx.tags = ["user.homophones_open"] + show_help = False + gui.show() + + +@imgui.open(x=main_screen.x + main_screen.width / 2.6, y=main_screen.y) +def gui(gui: imgui.GUI): + global active_word_list + if show_help: + gui.text("Homophone help - todo") + else: + gui.text("Select a homophone") + gui.line() + index = 1 + for word in active_word_list: + if gui.button(f"Choose {index}: {word}"): + actions.insert(actions.user.homophones_select(index)) + actions.user.homophones_hide() + index = index + 1 + + if gui.button("Phones (hide | exit)"): + actions.user.homophones_hide() + + +def show_help_gui(): + global show_help + show_help = True + gui.show() + + +@mod.capture(rule="{self.homophones_canonicals}") +def homophones_canonical(m) -> str: + "Returns a single string" + return m.homophones_canonicals + + +@mod.action_class +class Actions: + def homophones_hide(): + """Hides the homophones display""" + close_homophones() + + def homophones_show(m: str): + """Show the homophones display""" + raise_homophones(m, False, False) + + def homophones_show_auto(): + """Show homophones for selection, or current word if selection is empty.""" + text = actions.edit.selected_text() + if text: + raise_homophones(text, False, True) + else: + actions.edit.select_word() + actions.user.homophones_show_selection() + + def homophones_show_selection(): + """Show the homophones display for the selected text""" + raise_homophones(actions.edit.selected_text(), False, True) + + def homophones_force_show(m: str): + """Show the homophones display forcibly""" + raise_homophones(m, True, False) + + def homophones_force_show_selection(): + """Show the homophones display for the selected text forcibly""" + raise_homophones(actions.edit.selected_text(), True, True) + + def homophones_select(number: int) -> str: + """selects the homophone by number""" + if number <= len(active_word_list) and number > 0: + return active_word_list[number - 1] + + error = "homophones.py index {} is out of range (1-{})".format( + number, len(active_word_list) + ) + app.notify(error) + raise error + + def homophones_get(word: str) -> [str] or None: + """Get homophones for the given word""" + word = word.lower() + if word in all_homophones: + return all_homophones[word] + return None + + +ctx_homophones_open = Context() +ctx_homophones_open.matches = """ +tag: user.homophones_open +""" + + +@ctx_homophones_open.action_class("user") +class UserActions: + def choose(number_small: int): + """Choose the nth homophone""" + result = actions.user.homophones_select(number_small) + actions.insert(result) + actions.user.homophones_hide() diff --git a/community/core/homophones/homophones.talon b/community/core/homophones/homophones.talon new file mode 100644 index 0000000..262a252 --- /dev/null +++ b/community/core/homophones/homophones.talon @@ -0,0 +1,19 @@ +phones : user.homophones_show(homophones_canonical) +phones that: user.homophones_show_auto() +phones force : + user.homophones_force_show(homophones_canonical) +phones force: user.homophones_force_show_selection() +phones (hide | exit): user.homophones_hide() +phones word: + edit.select_word() + user.homophones_show_selection() +phones [] word left: + n = ordinals or 1 + user.words_left(n - 1) + edit.extend_word_left() + user.homophones_show_selection() +phones [] word right: + n = ordinals or 1 + user.words_right(n - 1) + edit.extend_word_right() + user.homophones_show_selection() diff --git a/community/core/homophones/homophones_open.talon b/community/core/homophones/homophones_open.talon new file mode 100644 index 0000000..2b7417e --- /dev/null +++ b/community/core/homophones/homophones_open.talon @@ -0,0 +1,7 @@ +tag: user.homophones_open +- + +choose : + result = user.homophones_select(number_small) + insert(user.formatted_text(result, formatters)) + user.homophones_hide() diff --git a/community/core/keys/arrow_key.talon-list b/community/core/keys/arrow_key.talon-list new file mode 100644 index 0000000..42bc477 --- /dev/null +++ b/community/core/keys/arrow_key.talon-list @@ -0,0 +1,6 @@ +list: user.arrow_key +- +down: down +left: left +right: right +up: up diff --git a/community/core/keys/function_key.talon-list b/community/core/keys/function_key.talon-list new file mode 100644 index 0000000..936eec4 --- /dev/null +++ b/community/core/keys/function_key.talon-list @@ -0,0 +1,27 @@ +list: user.function_key +- +f one: f1 +f two: f2 +f three: f3 +f four: f4 +f five: f5 +f six: f6 +f seven: f7 +f eight: f8 +f nine: f9 +f ten: f10 +f eleven: f11 +f twelve: f12 +f thirteen: f13 +f fourteen: f14 +f fifteen: f15 +f sixteen: f16 +f seventeen: f17 +f eighteen: f18 +f nineteen: f19 +f twenty: f20 +# these f keys are not supported by all platforms (eg Mac) and are disabled by default +#f twenty one: f21 +#f twenty two: f22 +#f twenty three: f23 +#f twenty four: f24 diff --git a/community/core/keys/keypad_key.talon-list b/community/core/keys/keypad_key.talon-list new file mode 100644 index 0000000..286542d --- /dev/null +++ b/community/core/keys/keypad_key.talon-list @@ -0,0 +1,21 @@ +list: user.keypad_key +- + +key pad zero: keypad_0 +key pad one: keypad_1 +key pad two: keypad_2 +key pad three: keypad_3 +key pad four: keypad_4 +key pad five: keypad_5 +key pad six: keypad_6 +key pad seven: keypad_7 +key pad eight: keypad_8 +key pad nine: keypad_9 +key pad point: keypad_decimal +key pad plus: keypad_plus +key pad minus: keypad_minus +key pad star: keypad_multiply +key pad slash: keypad_divide +key pad equals: keypad_equals +key pad clear: keypad_clear +key pad enter: keypad_enter diff --git a/community/core/keys/keys.py b/community/core/keys/keys.py new file mode 100644 index 0000000..171993a --- /dev/null +++ b/community/core/keys/keys.py @@ -0,0 +1,128 @@ +from talon import Context, Module, actions, app + +from .symbols import ( + dragon_punctuation_dict, + punctuation_dict, + symbol_key_dict, +) + +mod = Module() +ctx = Context() + +ctx_dragon = Context() +ctx_dragon.matches = r""" +speech.engine: dragon +""" + +mod.list("letter", desc="The spoken phonetic alphabet") +mod.list("symbol_key", desc="All symbols from the keyboard") +mod.list("arrow_key", desc="All arrow keys") +mod.list("number_key", desc="All number keys") +mod.list("modifier_key", desc="All modifier keys") +mod.list("function_key", desc="All function keys") +mod.list("special_key", desc="All special keys") +mod.list("keypad_key", desc="All keypad keys") +mod.list("punctuation", desc="words for inserting punctuation into text") + + +@mod.capture(rule="{self.modifier_key}+") +def modifiers(m) -> str: + "One or more modifier keys" + return "-".join(m.modifier_key_list) + + +@mod.capture(rule="{self.arrow_key}") +def arrow_key(m) -> str: + "One directional arrow key" + return m.arrow_key + + +@mod.capture(rule="+") +def arrow_keys(m) -> str: + "One or more arrow keys separated by a space" + return str(m) + + +@mod.capture(rule="{self.number_key}") +def number_key(m) -> str: + "One number key" + return m.number_key + + +@mod.capture(rule="{self.keypad_key}") +def keypad_key(m) -> str: + "One keypad key" + return m.keypad_key + + +@mod.capture(rule="{self.letter}") +def letter(m) -> str: + "One letter key" + return m.letter + + +@mod.capture(rule="{self.special_key}") +def special_key(m) -> str: + "One special key" + return m.special_key + + +@mod.capture(rule="{self.symbol_key}") +def symbol_key(m) -> str: + "One symbol key" + return m.symbol_key + + +@mod.capture(rule="{self.function_key}") +def function_key(m) -> str: + "One function key" + return m.function_key + + +@mod.capture(rule="( | | )") +def any_alphanumeric_key(m) -> str: + "any alphanumeric key" + return str(m) + + +@mod.capture( + rule="( | | " + "| | | | )" +) +def unmodified_key(m) -> str: + "A single key with no modifiers" + return str(m) + + +@mod.capture(rule="{self.modifier_key}* ") +def key(m) -> str: + "A single key with optional modifiers" + try: + mods = m.modifier_key_list + except AttributeError: + mods = [] + return "-".join(mods + [m.unmodified_key]) + + +@mod.capture(rule="+") +def keys(m) -> str: + "A sequence of one or more keys with optional modifiers" + return " ".join(m.key_list) + + +@mod.capture(rule="{self.letter}+") +def letters(m) -> str: + "Multiple letter keys" + return "".join(m.letter_list) + + +@mod.action_class +class Actions: + def get_punctuation_words(): + """Gets the user.punctuation list""" + return punctuation_dict + + +ctx.lists["user.punctuation"] = punctuation_dict +ctx.lists["user.symbol_key"] = symbol_key_dict +ctx_dragon.lists["user.punctuation"] = dragon_punctuation_dict diff --git a/community/core/keys/keys.talon b/community/core/keys/keys.talon new file mode 100644 index 0000000..048cf3f --- /dev/null +++ b/community/core/keys/keys.talon @@ -0,0 +1,12 @@ +: key(letter) +(ship | uppercase) [(lowercase | sunk)]: + user.insert_formatted(letters, "ALL_CAPS") +: key(symbol_key) +: key(function_key) +: key(special_key) +: key(keypad_key) + : key("{modifiers}-{unmodified_key}") +# for key combos consisting only of modifiers, eg. `press super`. +press : key(modifiers) +# for consistency with dictation mode and explicit arrow keys if you need them. +press : key(keys) diff --git a/community/core/keys/letter.talon-list b/community/core/keys/letter.talon-list new file mode 100644 index 0000000..af2db4e --- /dev/null +++ b/community/core/keys/letter.talon-list @@ -0,0 +1,30 @@ +list: user.letter +- +# for common alternative spoken forms for letters, visit +# https://talon.wiki/Resource%20Hub/Speech%20Recognition/improving_recognition_accuracy#collected-alternatives-to-the-default-alphabet +air: a +bat: b +cap: c +drum: d +each: e +fine: f +gust: g +harp: h +sit: i +jury: j +crunch: k +look: l +made: m +near: n +odd: o +pit: p +quench: q +red: r +sun: s +trap: t +urge: u +vest: v +whale: w +plex: x +yank: y +zip: z diff --git a/community/core/keys/mac/modifier_key.talon-list b/community/core/keys/mac/modifier_key.talon-list new file mode 100644 index 0000000..a25059a --- /dev/null +++ b/community/core/keys/mac/modifier_key.talon-list @@ -0,0 +1,12 @@ +list: user.modifier_key +os: mac +- + +alt: alt +control: ctrl +shift: shift +super: cmd +command: cmd +option: alt +function: fn +globe: fn diff --git a/community/core/keys/mac/special_key.talon-list b/community/core/keys/mac/special_key.talon-list new file mode 100644 index 0000000..6708750 --- /dev/null +++ b/community/core/keys/mac/special_key.talon-list @@ -0,0 +1,18 @@ +list: user.special_key +os: mac +- + +end: end +home: home +minus: minus +return: return +enter: keypad_enter +page down: pagedown +page up: pageup +escape: escape +space: space +tab: tab +insert: insert +wipe: backspace +delete: backspace +forward delete: delete diff --git a/community/core/keys/number_key.talon-list b/community/core/keys/number_key.talon-list new file mode 100644 index 0000000..b83e24b --- /dev/null +++ b/community/core/keys/number_key.talon-list @@ -0,0 +1,12 @@ +list: user.number_key +- +zero: 0 +one: 1 +two: 2 +three: 3 +four: 4 +five: 5 +six: 6 +seven: 7 +eight: 8 +nine: 9 diff --git a/community/core/keys/symbols.py b/community/core/keys/symbols.py new file mode 100644 index 0000000..2159f7c --- /dev/null +++ b/community/core/keys/symbols.py @@ -0,0 +1,102 @@ +# fmt: off + +# define the spoken forms for symbols in command and dictation mode +punctuation_dict = {} + +# for dragon, we add a couple of mappings that don't work for conformer +# i.e. dragon supports some actual symbols as the spoken form +dragon_punctuation_dict = { + "`": "`", + ",": ",", +} + +# define the spoken forms for symbols that are intended for command mode only +symbol_key_dict = {} + +# define spoken form for symbols for use in create_spoken_forms.py functionality +# we define a handful of symbol only. at present, this is restricted to one entry per symbol. +symbols_for_create_spoken_forms = { + # for application names like "Movies & TV" + "and": "&", + # for emails + "at": "@", + "dot": ".", + # for application names like "notepad++" + "plus": "+", +} + + +class Symbol: + character: str + command_and_dictation_forms: list[str] = None + command_forms: list[str] = None + + def __init__( + self, character: str, command_and_dictation_forms=None, command_forms=None + ): + self.character = character + + if command_and_dictation_forms: + self.command_and_dictation_forms = ( + [command_and_dictation_forms] + if isinstance(command_and_dictation_forms, str) + else command_and_dictation_forms + ) + + if command_forms: + self.command_forms = ( + [command_forms] if isinstance(command_forms, str) else command_forms + ) + +currency_symbols = [ + Symbol("$", ["dollar sign"], ["dollar"]), + Symbol("£", ["pound sign"], ["pound"]), +] + +symbols = [ + Symbol("`", ["back tick"], ["grave"]), + Symbol(",", ["comma", "coma"]), + Symbol(".", ["period", "full stop"], ["dot", "point"]), + Symbol(";", ["semicolon"]), + Symbol(":", ["colon"]), + Symbol("?", ["question mark"], ["question"]), + Symbol("!", ["exclamation mark", "exclamation point"], ["bang"]), + Symbol("*", ["asterisk"], ["star"]), + Symbol("#", ["hash sign", "number sign"], ["hash"]), + Symbol("%", ["percent sign"], ["percent"]), + Symbol("@", ["at symbol", "at sign"]), + Symbol("&", ["ampersand", "and sign"], ["amper"]), + Symbol("-", ["hyphen"], ["minus", "dash"]), + Symbol("=", None, ["equals"]), + Symbol("+", None, ["plus"]), + Symbol("~", None, ["tilde"]), + Symbol("_", None, ["down score", "underscore"]), + Symbol("(", ["paren", "L paren", "left paren"], None), + Symbol(")", ["R paren", "right paren"], None), + Symbol("[", None,["brack", "L brack", "bracket", "L bracket", "left bracket", "square", "L square", "left square",],), + Symbol("]", None, ["R brack", "R bracket", "right bracket", "R square", "right square"]), + Symbol("/", ["forward slash"], ["slash"]), + Symbol("\\", None, ["backslash"]), + Symbol("{", None, ["brace", "L brace", "left brace", "curly bracket", "left curly bracket"],), + Symbol("}", None, ["R brace", "right brace","R curly bracket", "right curly bracket"]), + Symbol("<", None, ["angle", "L Angle", "left angle", "less than"]), + Symbol(">", None, ["rangle", "R angle", "right angle", "greater than"]), + Symbol("^", None, ["caret"]), + Symbol("|", None, ["pipe"]), + Symbol("'", None, ["quote", "apostrophe"]), + Symbol('"', None, ["dub quote", "double quote"]), +] + +# by convention, symbols should include currency symbols +symbols.extend(currency_symbols) + +for symbol in symbols: + if symbol.command_and_dictation_forms: + for spoken_form in symbol.command_and_dictation_forms: + punctuation_dict[spoken_form] = symbol.character + symbol_key_dict[spoken_form] = symbol.character + dragon_punctuation_dict[spoken_form] = symbol.character + + if symbol.command_forms: + for spoken_form in symbol.command_forms: + symbol_key_dict[spoken_form] = symbol.character diff --git a/community/core/keys/win/modifier_key.talon-list b/community/core/keys/win/modifier_key.talon-list new file mode 100644 index 0000000..8e8a0d3 --- /dev/null +++ b/community/core/keys/win/modifier_key.talon-list @@ -0,0 +1,13 @@ +list: user.modifier_key + + +- +alt: alt +control: ctrl +roll: ctrl +shift: shift +big: shift +# super is the windows key +super: super +command: ctrl +ope: alt \ No newline at end of file diff --git a/community/core/keys/win/special_key.talon-list b/community/core/keys/win/special_key.talon-list new file mode 100644 index 0000000..194fb5a --- /dev/null +++ b/community/core/keys/win/special_key.talon-list @@ -0,0 +1,20 @@ +list: user.special_key +os: windows +os: linux +- + +end: end +home: home +minus: minus +enter: enter +page down: pagedown +page up: pageup +escape: escape +space: space +tab: tab +insert: insert +wipe: backspace +delete: backspace +forward delete: delete +menu key: menu +print screen: printscr diff --git a/community/core/menu_choose/menu_choose.py b/community/core/menu_choose/menu_choose.py new file mode 100644 index 0000000..52bb2eb --- /dev/null +++ b/community/core/menu_choose/menu_choose.py @@ -0,0 +1,14 @@ +from talon import Context, Module, actions + +mod = Module() + + +@mod.action_class +class Actions: + def choose(number_small: int): + """Choose the nth item""" + actions.key(f"down:{number_small-1} enter") + + def choose_up(number_small: int): + """Choose the nth item up""" + actions.key(f"up:{number_small} enter") diff --git a/community/core/menu_choose/menu_choose.talon b/community/core/menu_choose/menu_choose.talon new file mode 100644 index 0000000..9aa2b2e --- /dev/null +++ b/community/core/menu_choose/menu_choose.talon @@ -0,0 +1,3 @@ +# pick item from a dropdown +choose : user.choose(number_small) +choose up : user.choose_up(number_small) diff --git a/community/core/modes/code_languages.py b/community/core/modes/code_languages.py new file mode 100644 index 0000000..befcabd --- /dev/null +++ b/community/core/modes/code_languages.py @@ -0,0 +1,80 @@ +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("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"]), + Language("java", "java", ["java"]), + Language("javascript", "java script", ["js"]), + Language("javascriptreact", "java script react", ["jsx"]), + # 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"]), + # These languages doesn't actually have a language mode, but we do have snippets. + Language("cpp", "see plus plus", ["cpp", "hpp"]), + Language("csv", "csv", ["csv"]), + Language("html", "html", ["html"]), + Language("json", "json", ["json"]), + Language("shellscript", "shell script", ["sh"]), + Language("xml", "xml", ["xml"]), +] + +# 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/community/core/modes/command_and_dictation_mode.talon b/community/core/modes/command_and_dictation_mode.talon new file mode 100644 index 0000000..6f8dba7 --- /dev/null +++ b/community/core/modes/command_and_dictation_mode.talon @@ -0,0 +1,13 @@ +mode: command +mode: dictation +- +^dictation mode$: + mode.disable("sleep") + mode.disable("command") + mode.enable("dictation") + user.code_clear_language_mode() + user.gdb_disable() +^command mode$: + mode.disable("sleep") + mode.disable("dictation") + mode.enable("command") diff --git a/community/core/modes/deep_sleep.py b/community/core/modes/deep_sleep.py new file mode 100644 index 0000000..d6cbdad --- /dev/null +++ b/community/core/modes/deep_sleep.py @@ -0,0 +1,41 @@ +from talon import Context, Module, actions + +mod = Module() +mod.tag( + "deep_sleep", + desc="Tag for enabling deep sleep, requiring a longer wakeup command (defined in `sleep_mode_deep.talon`)", +) + +ctx = Context() + + +@mod.action_class +class Actions: + def deep_sleep_enable(): + """Enable deep sleep. + + Deep sleep requires a longer wakeup command to exit sleep + mode, helping prevent unintended wakeups from conversations, + meetings, listening to videos, etc. + + Instead of invoking this action directly, consider enabling + the `user.deep_sleep` tag in applications where unwanted + wakeups are more likely or problematic, such as meeting + apps. With this tag active, any sleep command triggers deep + sleep. + + You can also manually activate deep sleep by defining a custom + voice command using this action. + + Note: If the user.deep_sleep_disable action is not used to + wake up from deep sleep, then the deep sleep tag will stay + active. + + """ + ctx.tags = ["user.deep_sleep"] + actions.speech.disable() + + def deep_sleep_disable(): + """Disable deep sleep""" + ctx.tags = [] + actions.speech.enable() diff --git a/community/core/modes/dictation_mode.talon b/community/core/modes/dictation_mode.talon new file mode 100644 index 0000000..a1e2436 --- /dev/null +++ b/community/core/modes/dictation_mode.talon @@ -0,0 +1,76 @@ +mode: dictation +- +^press $: key(modifiers) +^press $: key(keys) + +# Everything here should call `user.dictation_insert()` instead of `insert()`, to correctly auto-capitalize/auto-space. +: user.dictation_insert(raw_prose) +cap: user.dictation_format_cap() +# Hyphenated variants are for Dragon. +(no cap | no-caps): user.dictation_format_no_cap() +(no space | no-space): user.dictation_format_no_space() +^cap that$: user.dictation_reformat_cap() +^(no cap | no-caps) that$: user.dictation_reformat_no_cap() +^(no space | no-space) that$: user.dictation_reformat_no_space() + +# Navigation +go up (line | lines): + edit.up() + repeat(number_small - 1) +go down (line | lines): + edit.down() + repeat(number_small - 1) +go left (word | words): + edit.word_left() + repeat(number_small - 1) +go right (word | words): + edit.word_right() + repeat(number_small - 1) +go line start: edit.line_start() +go line end: edit.line_end() + +# Selection +select left (word | words): + edit.extend_word_left() + repeat(number_small - 1) +select right (word | words): + edit.extend_word_right() + repeat(number_small - 1) +select left (character | characters): + edit.extend_left() + repeat(number_small - 1) +select right (character | characters): + edit.extend_right() + repeat(number_small - 1) +clear left (word | words): + edit.extend_word_left() + repeat(number_small - 1) + edit.delete() +clear right (word | words): + edit.extend_word_right() + repeat(number_small - 1) + edit.delete() +clear left (character | characters): + edit.extend_left() + repeat(number_small - 1) + edit.delete() +clear right (character | characters): + edit.extend_right() + repeat(number_small - 1) + edit.delete() + +# Formatting +formatted : user.dictation_insert_raw(format_text) +^format selection $: user.formatters_reformat_selection(formatters) + +# Corrections +nope that | scratch that: user.clear_last_phrase() +(nope | scratch) selection: edit.delete() +select that: user.select_last_phrase() +spell that : user.dictation_insert(letters) +spell that : + result = user.formatted_text(letters, formatters) + user.dictation_insert_raw(result) + +# Escape, type things that would otherwise be commands +^escape $: user.dictation_insert(user.text) diff --git a/community/core/modes/dragon_mode.talon b/community/core/modes/dragon_mode.talon new file mode 100644 index 0000000..179e182 --- /dev/null +++ b/community/core/modes/dragon_mode.talon @@ -0,0 +1,8 @@ +#defines modes specific to Dragon. +speech.engine: dragon +mode: all +- +# wakes Dragon on Mac, deactivates talon speech commands +dragon mode: user.dragon_mode() +#sleep dragon on Mac, activates talon speech commands +talon mode: user.talon_mode() diff --git a/community/core/modes/language_modes.py b/community/core/modes/language_modes.py new file mode 100644 index 0000000..f819719 --- /dev/null +++ b/community/core/modes/language_modes.py @@ -0,0 +1,73 @@ +from talon import Context, Module, actions, app + +from .code_languages import code_languages, code_special_file_map + +mod = Module() +ctx = Context() + +ctx_forced = Context() +ctx_forced.matches = r""" +tag: user.code_language_forced +""" + + +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.") + +# 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 language ids +extension_lang_map = { + f".{ext}": lang.id for lang in code_languages for ext in lang.extensions +} + +language_ids = {lang.id for lang in code_languages} +forced_language = "" + + +@ctx.action_class("code") +class CodeActions: + def language(): + file_name = actions.win.filename() + if file_name in code_special_file_map: + return code_special_file_map[file_name] + + file_extension = actions.win.file_ext().lower() + return extension_lang_map.get(file_extension, "") + + +@ctx_forced.action_class("code") +class ForcedCodeActions: + def language(): + return forced_language + + +@mod.action_class +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_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. + ctx.tags = [] + ctx.tags = ["user.code_language_forced"] + + def code_clear_language_mode(): + """Clears the active language mode, and re-enables code.language: extension matching""" + 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/community/core/modes/language_modes.talon b/community/core/modes/language_modes.talon new file mode 100644 index 0000000..ac916b9 --- /dev/null +++ b/community/core/modes/language_modes.talon @@ -0,0 +1,3 @@ +^force {user.language_mode}$: user.code_set_language_mode(language_mode) +show [forced] language mode: user.code_show_forced_language_mode() +^clear language mode$: user.code_clear_language_mode() diff --git a/community/core/modes/modes.py b/community/core/modes/modes.py new file mode 100644 index 0000000..d746dee --- /dev/null +++ b/community/core/modes/modes.py @@ -0,0 +1,64 @@ +from talon import Context, Module, actions, app, speech_system + +mod = Module() +ctx_sleep = Context() +ctx_awake = Context() + +modes = { + "presentation": "a more strict form of sleep where only a more strict wake up command works", +} + +for key, value in modes.items(): + mod.mode(key, value) + +ctx_sleep.matches = r""" +mode: sleep +""" + +ctx_awake.matches = r""" +not mode: sleep +""" + + +@ctx_sleep.action_class("speech") +class ActionsSleepMode: + def disable(): + actions.app.notify("Talon is already asleep") + + +@ctx_awake.action_class("speech") +class ActionsAwakeMode: + def enable(): + actions.app.notify("Talon is already awake") + + +@mod.action_class +class Actions: + def talon_mode(): + """For windows and Mac with Dragon, enables Talon commands and Dragon's command mode.""" + actions.speech.enable() + + engine = speech_system.engine.name + # app.notify(engine) + if "dragon" in engine: + if app.platform == "mac": + actions.user.dragon_engine_sleep() + elif app.platform == "windows": + actions.user.dragon_engine_wake() + # note: this may not do anything for all versions of Dragon. Requires Pro. + actions.user.dragon_engine_command_mode() + + def dragon_mode(): + """For windows and Mac with Dragon, disables Talon commands and exits Dragon's command mode""" + engine = speech_system.engine.name + # app.notify(engine) + + if "dragon" in engine: + # app.notify("dragon mode") + actions.speech.disable() + if app.platform == "mac": + actions.user.dragon_engine_wake() + elif app.platform == "windows": + actions.user.dragon_engine_wake() + # note: this may not do anything for all versions of Dragon. Requires Pro. + actions.user.dragon_engine_normal_mode() diff --git a/community/core/modes/modes_not_dragon.talon b/community/core/modes/modes_not_dragon.talon new file mode 100644 index 0000000..d590e32 --- /dev/null +++ b/community/core/modes/modes_not_dragon.talon @@ -0,0 +1,34 @@ +mode: command +mode: dictation +mode: sleep +not speech.engine: dragon +- +# The optional afterwards allows these to match even if you say arbitrary text +# after this command, without having to wait for the speech timeout. + +# This is handy because you often need to put Talon asleep in order to immediately +# talk to humans, and it's annoying to have to say "sleep all", wait for the timeout, +# and then resume your conversation. + +# With this, you can say "sleep all hey bob" and Talon will immediately go to +# sleep and ignore "hey bob". Note that subtitles will show "sleep all hey bob", +# because it's part of the rule definition, but "hey bob" will be ignored, because +# we don't do anything with the in the body of the command. + +# We define this *only* if the speech engine isn't Dragon, because if you're using Dragon, +# "go to sleep" is used to specifically control Dragon, and not affect Talon. +# +# It's a useful and well known command, though, so if you're using any other speech +# engine, this controls Talon. +^go to sleep []$: speech.disable() +^talon sleep []$: + speech.disable() + user.deprecate_command("2025-06-25", "talon sleep (without dragon)", "go to sleep") + +^sleep all []$: + user.switcher_hide_running() + user.history_disable() + user.homophones_hide() + user.help_hide() + user.mouse_sleep() + speech.disable() diff --git a/community/core/modes/sleep_mode.talon b/community/core/modes/sleep_mode.talon new file mode 100644 index 0000000..330d658 --- /dev/null +++ b/community/core/modes/sleep_mode.talon @@ -0,0 +1,9 @@ +mode: sleep +- +settings(): + # Stop continuous scroll/gaze scroll with a pop + user.mouse_enable_pop_stops_scroll = false + # Stop pop click with 'control mouse' mode + user.mouse_enable_pop_click = 0 + # Stop mouse scroll down using hiss noise + user.mouse_enable_hiss_scroll = false diff --git a/community/core/modes/sleep_mode_deep.talon b/community/core/modes/sleep_mode_deep.talon new file mode 100644 index 0000000..8567b1c --- /dev/null +++ b/community/core/modes/sleep_mode_deep.talon @@ -0,0 +1,5 @@ +mode: sleep +tag: user.deep_sleep +- + +^wake up and listen$: user.deep_sleep_disable() diff --git a/community/core/modes/sleep_mode_dragon.talon b/community/core/modes/sleep_mode_dragon.talon new file mode 100644 index 0000000..5e39689 --- /dev/null +++ b/community/core/modes/sleep_mode_dragon.talon @@ -0,0 +1,25 @@ +mode: all +speech.engine: dragon +- +# The optional afterwards allows these to match even if you say arbitrary text +# after this command, without having to wait for the speech timeout. + +# This is handy because you often need to put Talon asleep in order to immediately +# talk to humans, and it's annoying to have to say "sleep all", wait for the timeout, +# and then resume your conversation. + +# With this, you can say "sleep all hey bob" and Talon will immediately go to +# sleep and ignore "hey bob". Note that subtitles will show "sleep all hey bob", +# because it's part of the rule definition, but "hey bob" will be ignored, because +# we don't do anything with the in the body of the command. +^talon sleep []$: speech.disable() +^talon wake []$: speech.enable() + +^sleep all []$: + user.switcher_hide_running() + user.history_disable() + user.homophones_hide() + user.help_hide() + user.mouse_sleep() + speech.disable() + user.dragon_engine_sleep() diff --git a/community/core/modes/sleep_mode_not_dragon.talon b/community/core/modes/sleep_mode_not_dragon.talon new file mode 100644 index 0000000..184103f --- /dev/null +++ b/community/core/modes/sleep_mode_not_dragon.talon @@ -0,0 +1,18 @@ +mode: command +mode: dictation +mode: sleep +not speech.engine: dragon +not tag: user.deep_sleep +- + +# We define this *only* if the speech engine isn't Dragon, because if you're using Dragon, +# "wake up" is used to specifically control Dragon, and not affect Talon. +# +# It's a useful and well known command, though, so if you're using any other speech +# engine, this controls Talon. + +^(wake up)+$: speech.enable() + +^talon wake []$: + speech.enable() + user.deprecate_command("2025-06-25", "talon wake (without dragon)", "wake up") diff --git a/community/core/modes/sleep_mode_pop_twice_to_wake.py b/community/core/modes/sleep_mode_pop_twice_to_wake.py new file mode 100644 index 0000000..2fe45f9 --- /dev/null +++ b/community/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/community/core/modes/sleep_mode_wakeup.talon b/community/core/modes/sleep_mode_wakeup.talon new file mode 100644 index 0000000..b4eb758 --- /dev/null +++ b/community/core/modes/sleep_mode_wakeup.talon @@ -0,0 +1,30 @@ +mode: sleep +not tag: user.deep_sleep +- + +#================================================================================ +# Commands to wake Talon +#================================================================================ + +# Note: these have repeaters on them (+) to work around an issue where, in sleep mode, +# you can get into a situation where these commands are difficult to trigger. + +# These commands are fully anchored (^ and $), which means that there must be +# silence before and after saying them in order for them to recognize (this reduces +# false positives during normal sleep mode, normally a good thing). + +# However, ignored background speech during sleep mode also counts as an utterance. + +# Thus, if you say "blah blah blah talon wake", these won't trigger, because "blah +# blah blah" was part of the same utterance. You have to say "blah blah blah" , "talon wake" . + +# Sometimes people would forget the second pause, notice things weren't working, and +# say "talon wake" over and over again before the speech timeout ever gets hit, which +# means that these won't recognize. The (+) handles this case, so if you say +# "talon wake talon wake" , it'll still work. + +^(welcome back)+$: + user.mouse_wake() + user.history_enable() + user.talon_mode() diff --git a/community/core/modes/sleep_mode_wav2letter.talon b/community/core/modes/sleep_mode_wav2letter.talon new file mode 100644 index 0000000..e854a98 --- /dev/null +++ b/community/core/modes/sleep_mode_wav2letter.talon @@ -0,0 +1,6 @@ +mode: sleep +speech.engine: wav2letter +- +#this exists solely to prevent talon from waking up super easily in sleep mode at the moment with wav2letter +#you probably shouldn't have any other commands here +: skip() diff --git a/community/core/mouse_grid/mouse_grid.py b/community/core/mouse_grid/mouse_grid.py new file mode 100644 index 0000000..a811808 --- /dev/null +++ b/community/core/mouse_grid/mouse_grid.py @@ -0,0 +1,313 @@ +# courtesy of https://github.com/timo/ +# see https://github.com/timo/talon_scripts +import math +from typing import Union + +from talon import Context, Module, actions, canvas, cron, ctrl, screen, settings, ui +from talon.skia import Paint, Rect +from talon.types.point import Point2d + +mod = Module() +mod.setting( + "grid_narrow_expansion", + type=int, + default=0, + desc="""After narrowing, grow the new region by this many pixels in every direction, to make things immediately on edges easier to hit, and when the grid is at its smallest, it allows you to still nudge it around""", +) +mod.setting( + "grids_put_one_bottom_left", + type=bool, + default=False, + desc="""Allows you to switch mouse grid and friends between a computer numpad and a phone numpad (the number one goes on the bottom left or the top left)""", +) + +mod.tag("mouse_grid_showing", desc="Tag indicates whether the mouse grid is showing") +mod.tag( + "mouse_grid_enabled", + desc="Deprecated: do not use. Activates legacy m grid command", +) +ctx = Context() + + +class MouseSnapNine: + def __init__(self): + self.screen = None + self.rect = None + self.history = [] + self.img = None + self.mcanvas = None + self.active = False + self.count = 0 + self.was_zoom_mouse_active = False + self.was_control_mouse_active = False + self.was_control1_mouse_active = False + + def setup(self, *, rect: Rect = None, screen_num: int = None): + screens = ui.screens() + # each if block here might set the rect to None to indicate failure + if rect is not None: + try: + screen = ui.screen_containing(*rect.center) + except Exception: + rect = None + if rect is None and screen_num is not None: + screen = screens[screen_num % len(screens)] + rect = screen.rect + if rect is None: + screen = screens[0] + rect = screen.rect + self.rect = rect.copy() + self.screen = screen + self.count = 0 + self.img = None + if self.mcanvas is not None: + self.mcanvas.close() + self.mcanvas = canvas.Canvas.from_screen(screen) + if self.active: + self.mcanvas.register("draw", self.draw) + self.mcanvas.freeze() + + def show(self): + if self.active: + return + # noinspection PyUnresolvedReferences + if actions.tracking.control_zoom_enabled(): + self.was_zoom_mouse_active = True + actions.tracking.control_zoom_toggle(False) + if actions.tracking.control_enabled(): + self.was_control_mouse_active = True + actions.tracking.control_toggle(False) + if actions.tracking.control1_enabled(): + self.was_control1_mouse_active = True + actions.tracking.control1_toggle(False) + self.mcanvas.register("draw", self.draw) + self.mcanvas.freeze() + self.active = True + return + + def close(self): + if not self.active: + return + self.mcanvas.unregister("draw", self.draw) + self.mcanvas.close() + self.mcanvas = None + self.img = None + + self.active = False + + if self.was_control_mouse_active and not actions.tracking.control_enabled(): + actions.tracking.control_toggle(True) + if self.was_control1_mouse_active and not actions.tracking.control1_enabled(): + actions.tracking.control1_toggle(True) + if self.was_zoom_mouse_active and not actions.tracking.control_zoom_enabled(): + actions.tracking.control_zoom_toggle(True) + + self.was_zoom_mouse_active = False + self.was_control_mouse_active = False + self.was_control1_mouse_active = False + + def draw(self, canvas): + paint = canvas.paint + + def draw_grid(offset_x, offset_y, width, height): + canvas.draw_line( + offset_x + width // 3, + offset_y, + offset_x + width // 3, + offset_y + height, + ) + canvas.draw_line( + offset_x + 2 * width // 3, + offset_y, + offset_x + 2 * width // 3, + offset_y + height, + ) + + canvas.draw_line( + offset_x, + offset_y + height // 3, + offset_x + width, + offset_y + height // 3, + ) + canvas.draw_line( + offset_x, + offset_y + 2 * height // 3, + offset_x + width, + offset_y + 2 * height // 3, + ) + + def draw_crosses(offset_x, offset_y, width, height): + for row in range(0, 2): + for col in range(0, 2): + cx = offset_x + width / 6 + (col + 0.5) * width / 3 + cy = offset_y + height / 6 + (row + 0.5) * height / 3 + + canvas.draw_line(cx - 10, cy, cx + 10, cy) + canvas.draw_line(cx, cy - 10, cx, cy + 10) + + grid_stroke = 1 + + def draw_text(offset_x, offset_y, width, height): + canvas.paint.text_align = canvas.paint.TextAlign.CENTER + for row in range(3): + for col in range(3): + text_string = "" + if settings.get("user.grids_put_one_bottom_left"): + text_string = f"{(2 - row)*3+col+1}" + else: + text_string = f"{row*3+col+1}" + text_rect = canvas.paint.measure_text(text_string)[1] + background_rect = text_rect.copy() + background_rect.center = Point2d( + offset_x + width / 6 + col * width / 3, + offset_y + height / 6 + row * height / 3, + ) + background_rect = background_rect.inset(-4) + paint.color = "9999995f" + paint.style = Paint.Style.FILL + canvas.draw_rect(background_rect) + paint.color = "00ff00ff" + canvas.draw_text( + text_string, + offset_x + width / 6 + col * width / 3, + offset_y + height / 6 + row * height / 3 + text_rect.height / 2, + ) + + if self.count < 2: + paint.color = "00ff007f" + for which in range(1, 10): + gap = 35 - self.count * 10 + if not self.active: + gap = 45 + draw_crosses(*self.calc_narrow(which, self.rect)) + + paint.stroke_width = grid_stroke + if self.active: + paint.color = "ff0000ff" + else: + paint.color = "000000ff" + if self.count >= 2: + aspect = self.rect.width / self.rect.height + if aspect >= 1: + w = self.screen.width / 3 + h = w / aspect + else: + h = self.screen.height / 3 + w = h * aspect + x = self.screen.x + (self.screen.width - w) / 2 + y = self.screen.y + (self.screen.height - h) / 2 + self.draw_zoom(canvas, x, y, w, h) + draw_grid(x, y, w, h) + draw_text(x, y, w, h) + else: + draw_grid(self.rect.x, self.rect.y, self.rect.width, self.rect.height) + + paint.textsize += 12 - self.count * 3 + draw_text(self.rect.x, self.rect.y, self.rect.width, self.rect.height) + + def calc_narrow(self, which, rect): + rect = rect.copy() + bdr = settings.get("user.grid_narrow_expansion") + row = int(which - 1) // 3 + col = int(which - 1) % 3 + if settings.get("user.grids_put_one_bottom_left"): + row = 2 - row + rect.x += int(col * rect.width // 3) - bdr + rect.y += int(row * rect.height // 3) - bdr + rect.width = (rect.width // 3) + bdr * 2 + rect.height = (rect.height // 3) + bdr * 2 + return rect + + def narrow(self, which, move=True): + if which < 1 or which > 9: + return + self.save_state() + rect = self.calc_narrow(which, self.rect) + # check count so we don't bother zooming in _too_ far + if self.count < 5: + self.rect = rect.copy() + self.count += 1 + if move: + ctrl.mouse_move(*rect.center) + if self.count >= 2: + self.update_screenshot() + else: + self.mcanvas.freeze() + + def update_screenshot(self): + def finish_capture(): + self.img = screen.capture_rect(self.rect) + self.mcanvas.freeze() + + self.mcanvas.hide() + cron.after("16ms", finish_capture) + + def draw_zoom(self, canvas, x, y, w, h): + if self.img: + src = Rect(0, 0, self.img.width, self.img.height) + dst = Rect(x, y, w, h) + canvas.draw_image_rect(self.img, src, dst) + + def narrow_to_pos(self, x, y): + col_size = int(self.width // 3) + row_size = int(self.height // 3) + col = math.floor((x - self.rect.x) / col_size) + row = math.floor((y - self.rect.x) / row_size) + self.narrow(1 + col + 3 * row, move=False) + + def save_state(self): + self.history.append((self.count, self.rect.copy())) + + def go_back(self): + # FIXME: need window and screen tracking + self.count, self.rect = self.history.pop() + self.mcanvas.freeze() + + +mg = MouseSnapNine() + + +@mod.action_class +class GridActions: + def grid_activate(): + """Show mouse grid""" + if not mg.mcanvas: + mg.setup() + mg.show() + ctx.tags = ["user.mouse_grid_showing"] + + def grid_place_window(): + """Places the grid on the currently active window""" + mg.setup(rect=ui.active_window().rect) + + def grid_reset(): + """Resets the grid to fill the whole screen again""" + if mg.active: + mg.setup() + + def grid_select_screen(screen: int): + """Brings up mouse grid""" + mg.setup(screen_num=screen - 1) + mg.show() + + def grid_narrow_list(digit_list: list[str]): + """Choose fields multiple times in a row""" + for d in digit_list: + actions.self.grid_narrow(int(d)) + + def grid_narrow(digit: Union[int, str]): + """Choose a field of the grid and narrow the selection down""" + mg.narrow(int(digit)) + + def grid_go_back(): + """Sets the grid state back to what it was before the last command""" + mg.go_back() + + def grid_close(): + """Close the active grid""" + ctx.tags = [] + mg.close() + + def grid_is_active(): + """check if grid is already active""" + return mg.active diff --git a/community/core/mouse_grid/mouse_grid.talon b/community/core/mouse_grid/mouse_grid.talon new file mode 100644 index 0000000..7f4efe6 --- /dev/null +++ b/community/core/mouse_grid/mouse_grid.talon @@ -0,0 +1,6 @@ +tag: user.mouse_grid_enabled +- +M grid: + app.notify("please use the voice command 'mouse grid' instead of 'm grid'") + user.grid_select_screen(1) + user.grid_activate() diff --git a/community/core/mouse_grid/mouse_grid_always.talon b/community/core/mouse_grid/mouse_grid_always.talon new file mode 100644 index 0000000..46dc9b1 --- /dev/null +++ b/community/core/mouse_grid/mouse_grid_always.talon @@ -0,0 +1,15 @@ +mouse grid: + user.grid_select_screen(1) + user.grid_activate() + +grid win: + user.grid_place_window() + user.grid_activate() + +grid +: + user.grid_activate() + user.grid_narrow_list(number_key_list) + +grid screen []: + user.grid_select_screen(number or 1) + user.grid_activate() diff --git a/community/core/mouse_grid/mouse_grid_open.talon b/community/core/mouse_grid/mouse_grid_open.talon new file mode 100644 index 0000000..a22aaf2 --- /dev/null +++ b/community/core/mouse_grid/mouse_grid_open.talon @@ -0,0 +1,8 @@ +tag: user.mouse_grid_showing +- +: user.grid_narrow(number_key) +grid (off | close | hide): user.grid_close() + +grid reset: user.grid_reset() + +grid back: user.grid_go_back() diff --git a/community/core/navigation/navigation.py b/community/core/navigation/navigation.py new file mode 100644 index 0000000..0069efa --- /dev/null +++ b/community/core/navigation/navigation.py @@ -0,0 +1,43 @@ +from talon import Context, Module, actions + +mod = Module() +mod.tag("navigation") + +ctx_browser = Context() +ctx_browser.matches = r""" +tag: browser +""" + +ctx_mac = Context() +ctx_mac.matches = r""" +os: mac +""" + + +@ctx_browser.action_class("user") +class BrowserActions: + def go_back(): + actions.browser.go_back() + + def go_forward(): + actions.browser.go_forward() + + +@ctx_mac.action_class("user") +class MacActions: + def go_back(): + actions.key("cmd-[") + + def go_forward(): + actions.key("cmd-]") + + +@mod.action_class +class Actions: + def go_back(): + """Navigate back""" + actions.key("alt-left") + + def go_forward(): + """Navigate forward""" + actions.key("alt-right") diff --git a/community/core/navigation/navigation.talon b/community/core/navigation/navigation.talon new file mode 100644 index 0000000..18bfd2e --- /dev/null +++ b/community/core/navigation/navigation.talon @@ -0,0 +1,5 @@ +tag: user.navigation +- + +go back: user.go_back() +go forward: user.go_forward() diff --git a/community/core/noise.py b/community/core/noise.py new file mode 100644 index 0000000..9fe63cf --- /dev/null +++ b/community/core/noise.py @@ -0,0 +1,51 @@ +""" +Map noises (like pop) to actions so they can have contextually differing behavior +""" + +from talon import Module, actions, cron, noise, settings + +mod = Module() +hiss_cron = None + +mod.setting( + "hiss_scroll_debounce_time", + type=int, + default=100, + desc="How much time a hiss must last for to be considered a hiss rather than part of speech, in ms", +) + + +@mod.action_class +class Actions: + def noise_trigger_pop(): + """ + Called when the user makes a 'pop' noise. Listen to + https://noise.talonvoice.com/static/previews/pop.mp3 for an + example. + """ + actions.skip() + + def noise_trigger_hiss(active: bool): + """ + Called when the user makes a 'hiss' noise. Listen to + https://noise.talonvoice.com/static/previews/hiss.mp3 for an + example. + """ + actions.skip() + + +def noise_trigger_hiss_debounce(active: bool): + """Since the hiss noise triggers while you're talking we need to debounce it""" + global hiss_cron + if active: + hiss_cron = cron.after( + str(f"{settings.get('user.hiss_scroll_debounce_time')}ms"), + lambda: actions.user.noise_trigger_hiss(active), + ) + else: + cron.cancel(hiss_cron) + actions.user.noise_trigger_hiss(active) + + +noise.register("pop", lambda _: actions.user.noise_trigger_pop()) +noise.register("hiss", noise_trigger_hiss_debounce) diff --git a/community/core/numbers/numbers.py b/community/core/numbers/numbers.py new file mode 100644 index 0000000..787ec79 --- /dev/null +++ b/community/core/numbers/numbers.py @@ -0,0 +1,301 @@ +import math +from typing import Iterator, Union + +from talon import Context, Module + +mod = Module() +ctx = Context() + +digit_list = "zero one two three four five six seven eight nine".split() +teens = "ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen".split() +tens = "twenty thirty forty fifty sixty seventy eighty ninety".split() +scales = "hundred thousand million billion trillion quadrillion quintillion sextillion septillion octillion nonillion decillion".split() + +digits_map = {n: i for i, n in enumerate(digit_list)} +digits_map["oh"] = 0 +teens_map = {n: i + 10 for i, n in enumerate(teens)} +tens_map = {n: 10 * (i + 2) for i, n in enumerate(tens)} +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=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""" + + result = {} + + 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: + 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}" + 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 + + +def parse_number(l: list[str]) -> str: + """Parses a list of words into a number/digit string.""" + l = list(scan_small_numbers(l)) + for scale in scales: + l = parse_scale(scale, l) + return "".join(str(n) for n in l) + + +def scan_small_numbers(l: list[str]) -> Iterator[Union[str, int]]: + """ + Takes a list of number words, yields a generator of mixed numbers & strings. + Translates small number terms (<100) into corresponding numbers. + Drops all occurrences of "and". + Smashes digits onto tens words, eg. ["twenty", "one"] -> [21]. + But note that "ten" and "zero" are excluded, ie: + ["ten", "three"] -> [10, 3] + ["fifty", "zero"] -> [50, 0] + Does nothing to scale words ("hundred", "thousand", "million", etc). + """ + # reversed so that repeated pop() visits in left-to-right order + l = [x for x in reversed(l) if x != "and"] + while l: + n = l.pop() + # fuse tens onto digits, eg. "twenty", "one" -> 21 + if n in tens_map and l and digits_map.get(l[-1], 0) != 0: + d = l.pop() + yield numbers_map[n] + numbers_map[d] + # turn small number terms into corresponding numbers + elif n not in scales_map: + yield numbers_map[n] + else: + yield n + + +def parse_scale(scale: str, l: list[Union[str, int]]) -> list[Union[str, int]]: + """Parses a list of mixed numbers & strings for occurrences of the following + pattern: + + + + where is a scale word like "hundred", "thousand", "million", etc and + multiplier and remainder are numbers or strings of numbers of the + appropriate size. For example: + + parse_scale("hundred", [1, "hundred", 2]) -> [102] + parse_scale("thousand", [12, "thousand", 3, 45]) -> [12345] + + We assume that all scales of lower magnitude have already been parsed; don't + call parse_scale("thousand") until you've called parse_scale("hundred"). + """ + scale_value = scales_map[scale] + scale_digits = len(str(scale_value)) + + # Split the list on the desired scale word, then parse from left to right. + left, *splits = split_list(scale, l) + for right in splits: + # (1) Figure out the multiplier by looking to the left of the scale + # word. We ignore non-integers because they are scale words that we + # haven't processed yet; this strategy means that "thousand hundred" + # gets parsed as 1,100 instead of 100,000, but "hundred thousand" is + # parsed correctly as 100,000. + before = 1 # default multiplier + if left and isinstance(left[-1], int) and left[-1] != 0: + before = left.pop() + + # (2) Absorb numbers to the right, eg. in [1, "thousand", 1, 26], "1 + # thousand" absorbs ["1", "26"] to make 1,126. We pull numbers off + # `right` until we fill up the desired number of digits. + after = "" + while right and isinstance(right[0], int): + next = after + str(right[0]) + if len(next) >= scale_digits: + break + after = next + right.pop(0) + after = int(after) if after else 0 + + # (3) Push the parsed number into place, append whatever was left + # unparsed, and continue. + left.append(before * scale_value + after) + left.extend(right) + + return left + + +def split_list(value, l: list) -> Iterator: + """Splits a list by occurrences of a given value.""" + start = 0 + while True: + try: + i = l.index(value, start) + except ValueError: + break + yield l[start:i] + start = i + 1 + yield l[start:] + + +# # ---------- TESTS (uncomment to run) ---------- +# def test_number(expected, string): +# print('testing:', string) +# l = list(scan_small_numbers(string.split())) +# print(" scan --->", l) +# for scale in scales: +# old = l +# l = parse_scale(scale, l) +# if scale in old: print(" parse -->", l) +# else: assert old == l, "parse_scale should do nothing if the scale does not occur in the list" +# result = "".join(str(n) for n in l) +# assert result == parse_number(string.split()) +# assert str(expected) == result, f"parsing {string!r}, expected {expected}, got {result}" + +# test_number(105000, "one hundred and five thousand") +# test_number(1000000, "one thousand thousand") +# test_number(1501000, "one million five hundred one thousand") +# test_number(1501106, "one million five hundred and one thousand one hundred and six") +# test_number(123, "one two three") +# test_number(123, "one twenty three") +# test_number(104, "ten four") # borderline, but valid in some dialects +# test_number(1066, "ten sixty six") # a common way of saying years +# test_number(1906, "nineteen oh six") # year +# test_number(2001, "twenty oh one") # year +# test_number(2020, "twenty twenty") +# test_number(1001, "one thousand one") +# test_number(1010, "one thousand ten") +# test_number(123456, "one hundred and twenty three thousand and four hundred and fifty six") +# test_number(123456, "one twenty three thousand four fifty six") + +# ## failing (and somewhat debatable) tests from old numbers.py +# #test_number(10000011, "one million one one") +# #test_number(100001010, "one million ten ten") +# #test_number(1050006000, "one hundred thousand and five thousand and six thousand") + + +# ---------- CAPTURES ---------- +alt_digits = "(" + "|".join(digits_map.keys()) + ")" +alt_teens = "(" + "|".join(teens_map.keys()) + ")" +alt_tens = "(" + "|".join(tens_map.keys()) + ")" +alt_scales = "(" + "|".join(scales_map.keys()) + ")" +number_word = "(" + "|".join(numbers_map.keys()) + ")" +# don't allow numbers to start with scale words like "hundred", "thousand", etc +leading_words = numbers_map.keys() - scales_map.keys() +leading_words -= {"oh", "o"} # comment out to enable bare/initial "oh" +number_word_leading = f"({'|'.join(leading_words)})" + + +mod.list("number_small", "List of small (0-99) numbers") +mod.tag("unprefixed_numbers", desc="Dont require prefix when saying a number") +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 +@ctx.capture("digit_string", rule=f"({alt_digits} | {alt_teens} | {alt_tens})+") +def digit_string(m) -> str: + return parse_number(list(m)) + + +@ctx.capture("digits", rule="") +def digits(m) -> int: + """Parses a phrase representing a digit sequence, returning it as an integer.""" + return int(m.digit_string) + + +@mod.capture(rule=f"{number_word_leading} ([and] {number_word})*") +def number_string(m) -> str: + """Parses a number phrase, returning that number as a string.""" + return parse_number(list(m)) + + +@ctx.capture("number", rule="") +def number(m) -> int: + """Parses a number phrase, returning it as an integer.""" + return int(m.number_string) + + +@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}") +def number_small(m) -> int: + return int(m.number_small) + + +@mod.capture(rule="[negative | minus] ") +def number_signed_small(m) -> int: + """Parses an integer between -99 and 99.""" + number = m[-1] + return -number if (m[0] in ["negative", "minus"]) else number diff --git a/community/core/numbers/numbers_prefixed.talon b/community/core/numbers/numbers_prefixed.talon new file mode 100644 index 0000000..6dba7b3 --- /dev/null +++ b/community/core/numbers/numbers_prefixed.talon @@ -0,0 +1 @@ +: "{number_prose_prefixed}" diff --git a/community/core/numbers/numbers_unprefixed.talon b/community/core/numbers/numbers_unprefixed.talon new file mode 100644 index 0000000..0f7ac84 --- /dev/null +++ b/community/core/numbers/numbers_unprefixed.talon @@ -0,0 +1,6 @@ +tag: user.unprefixed_numbers +and not tag: user.continuous_scrolling +and not tag: user.mouse_grid_showing +- + +: "{number_prose_unprefixed}" diff --git a/community/core/numbers/ordinals.py b/community/core/numbers/ordinals.py new file mode 100644 index 0000000..4a68fee --- /dev/null +++ b/community/core/numbers/ordinals.py @@ -0,0 +1,72 @@ +from talon import Context, Module + +# The primitive ordinal words in English below a hundred. +ordinal_words = { + 0: "zeroth", + 1: "first", + 2: "second", + 3: "third", + 4: "fourth", + 5: "fifth", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", + 10: "tenth", + 11: "eleventh", + 12: "twelfth", + 13: "thirteenth", + 14: "fourteenth", + 15: "fifteenth", + 16: "sixteenth", + 17: "seventeenth", + 18: "eighteenth", + 19: "nineteenth", + 20: "twentieth", + 30: "thirtieth", + 40: "fortieth", + 50: "fiftieth", + 60: "sixtieth", + 70: "seventieth", + 80: "eightieth", + 90: "ninetieth", +} +tens_words = "zero ten twenty thirty forty fifty sixty seventy eighty ninety".split() + +# ordinal_numbers maps ordinal words into their corresponding numbers. +ordinal_numbers = {} +ordinal_small = {} + +for n in range(1, 100): + if n in ordinal_words: + word = ordinal_words[n] + else: + (tens, units) = divmod(n, 10) + assert 1 < tens < 10, "we have already handled all ordinals < 20" + assert 0 < units, "we have already handled all ordinals divisible by ten" + word = f"{tens_words[tens]} {ordinal_words[units]}" + if n <= 20: + ordinal_small[word] = str(n) + ordinal_numbers[word] = str(n) + + +mod = Module() +ctx = Context() + +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="{user.ordinals}") +def ordinals(m) -> int: + """Returns a single ordinal as an integer""" + return int(m.ordinals) + + +@mod.capture(rule="{user.ordinals_small}") +def ordinals_small(m) -> int: + """Returns a single small ordinal as an integer""" + return int(m.ordinals_small) diff --git a/community/core/screens/screens.py b/community/core/screens/screens.py new file mode 100644 index 0000000..6587a05 --- /dev/null +++ b/community/core/screens/screens.py @@ -0,0 +1,64 @@ +from talon import Module, cron, ui +from talon.canvas import Canvas + +mod = Module() + + +@mod.action_class +class Actions: + def screens_show_numbering(): + """Show screen number on each screen""" + screens = ui.screens() + number = 1 + for screen in screens: + show_screen_number(screen, number) + number += 1 + + def screens_get_by_number(screen_number: int) -> ui.Screen: + """Get screen by number""" + screens = ui.screens() + length = len(screens) + if screen_number < 1 or screen_number > length: + raise Exception( + f"Non-existing screen {screen_number} in range [1, {length}]" + ) + return screens[screen_number - 1] + + def screens_get_previous(screen: ui.Screen) -> ui.Screen: + """Get the screen before this one""" + return get_screen_by_offset(screen, -1) + + def screens_get_next(screen: ui.Screen) -> ui.Screen: + """Get the screen after this one""" + return get_screen_by_offset(screen, 1) + + +def get_screen_by_offset(screen: ui.Screen, offset: int) -> ui.Screen: + screens = ui.screens() + index = (screens.index(screen) + offset) % len(screens) + return screens[index] + + +def show_screen_number(screen: ui.Screen, number: int): + def on_draw(c): + c.paint.typeface = "arial" + # The min(width, height) is to not get gigantic size on portrait screens + c.paint.textsize = round(min(c.width, c.height) / 2) + text = f"{number}" + rect = c.paint.measure_text(text)[1] + x = c.x + c.width / 2 - rect.x - rect.width / 2 + y = c.y + c.height / 2 + rect.height / 2 + + c.paint.style = c.paint.Style.FILL + c.paint.color = "eeeeee" + c.draw_text(text, x, y) + + c.paint.style = c.paint.Style.STROKE + c.paint.color = "000000" + c.draw_text(text, x, y) + + cron.after("3s", canvas.close) + + canvas = Canvas.from_rect(screen.rect) + canvas.register("draw", on_draw) + canvas.freeze() diff --git a/community/core/screens/screens.talon b/community/core/screens/screens.talon new file mode 100644 index 0000000..6748c2a --- /dev/null +++ b/community/core/screens/screens.talon @@ -0,0 +1 @@ +screen numbers: user.screens_show_numbering() diff --git a/community/core/snippets/README.md b/community/core/snippets/README.md new file mode 100644 index 0000000..586d298 --- /dev/null +++ b/community/core/snippets/README.md @@ -0,0 +1,68 @@ +# Snippets + +Custom format to represent snippets. + +## Features + +- Custom file ending `.snippet`. +- Supports syntax highlighting in VSCode via an [extension](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon) +- Supports auto-formatting in VSCode via an [extension](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon) +- Support for insertion and wrapper snippets. Note that while the snippet file syntax here supports wrapper snippets, you will need to install [Cursorless](https://www.cursorless.org) for wrapper snippets to work. +- Support for phrase formatters. + +## Format + +- A `.snippet` file can contain multiple snippet documents separated by `---`. +- Each snippet document has a context and body separated by `-`. +- Optionally a file can have a single context at the top with no body. This is not a snippet in itself, but default values to be inherited by the other snippet documents in the same file. +- Some context keys supports multiple values. These values are separated by `|`. + - For most keys like `language` or `phrase` multiple values means _or_. You can use phrase _1_ or phrase _2_. The snippet is active in language _A_ or language _B_. + - For `insertionFormatter` multiple values means that the formatters will be applied in sequence. + +### Context fields + +| 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` | + +- `name`: Unique name identifying the snippets. Can be referenced in Python to use the snippet programmatically. +- `description`: A description of the snippet. +- `language`: Language identifier indicating which language the snippet is available for. If omitted the snippet is enabled globally. +- `phrase`: The spoken phrase used to insert the snippet. eg `"snip if"`. +- `insertionScope`: Used by [Cursorless](https://www.cursorless.org) to infer scope when inserting the snippet. eg `"snip if after air"` gets inferred as `"snip if after state air"`. + +### Variables + +It's also possible to set configuration that applies to a specific tab stop (`$0`) or variable (`$try`): + +| Key | Required | Multiple values | Example | +| ------------------ | -------- | --------------- | ----------------------------------- | +| insertionFormatter | No | Yes | `$0.insertionFormatter: SNAKE_CASE` | +| wrapperPhrase | No | Yes | `$0.wrapperPhrase: try \| trying` | +| wrapperScope | No | No | `$0.wrapperScope: statement` | + +- `insertionFormatter`: Formatter to apply to the phrase when inserting the snippet. eg `"snip funk get value"`. If omitted no trailing phrase is available for the snippet. +- `wrapperPhrase`: Used by [Cursorless](https://www.cursorless.org) as the spoken form for wrapping with the snippet. eg `"if wrap air"`. Without Cursorless this spoken form is ignored by Talon. +- `wrapperScope`: Used by [Cursorless](https://www.cursorless.org) to infer scope when wrapping with the snippet. eg `"if wrap air"` gets inferred as `"if wrap state air"`. + +## Formatting and syntax highlighting + +To get formatting, code completion and syntax highlighting for `.snippet` files: install [andreas-talon](https://marketplace.visualstudio.com/items?itemName=AndreasArvidsson.andreas-talon) + +## Examples + +### Single snippet definition + +![snippets1](./images/snippets1.png) + +### Multiple snippet definitions in single file + +![snippets2](./images/snippets2.png) + +### Default context and multiple values + +![snippets3](./images/snippets3.png) diff --git a/community/core/snippets/images/snippets1.png b/community/core/snippets/images/snippets1.png new file mode 100644 index 0000000..d008926 Binary files /dev/null and b/community/core/snippets/images/snippets1.png differ diff --git a/community/core/snippets/images/snippets2.png b/community/core/snippets/images/snippets2.png new file mode 100644 index 0000000..379b779 Binary files /dev/null and b/community/core/snippets/images/snippets2.png differ diff --git a/community/core/snippets/images/snippets3.png b/community/core/snippets/images/snippets3.png new file mode 100644 index 0000000..58e0905 Binary files /dev/null and b/community/core/snippets/images/snippets3.png differ diff --git a/community/core/snippets/snippet_types.py b/community/core/snippets/snippet_types.py new file mode 100644 index 0000000..8ead6e3 --- /dev/null +++ b/community/core/snippets/snippet_types.py @@ -0,0 +1,66 @@ +from dataclasses import dataclass + +from talon import Context + + +class SnippetLists: + insertion: dict[str, str] + with_phrase: dict[str, str] + wrapper: dict[str, str] + + def __init__(self): + self.insertion = {} + self.with_phrase = {} + self.wrapper = {} + + +@dataclass +class SnippetLanguageState: + ctx: Context + lists: SnippetLists + + +@dataclass +class SnippetVariable: + name: str + insertion_formatters: list[str] | None = None + wrapper_phrases: list[str] | None = None + wrapper_scope: str | None = None + + +@dataclass +class Snippet: + name: str + body: str + description: str | None + phrases: list[str] | None + insertion_scopes: list[str] | None + languages: list[str] | None + variables: list[SnippetVariable] + + def get_variable(self, name: str): + for var in self.variables: + if var.name == name: + return var + return None + + def get_variable_strict(self, name: str): + variable = self.get_variable(name) + if variable is None: + raise ValueError(f"Snippet '{self.name}' has no variable '{name}'") + return variable + + +@dataclass +class InsertionSnippet: + body: str + scopes: list[str] | None + languages: list[str] | None + + +@dataclass +class WrapperSnippet: + body: str + variable_name: str + scope: str | None + languages: list[str] | None diff --git a/community/core/snippets/snippets.py b/community/core/snippets/snippets.py new file mode 100644 index 0000000..c3bc7b4 --- /dev/null +++ b/community/core/snippets/snippets.py @@ -0,0 +1,224 @@ +from pathlib import Path +from typing import Union + +from talon import Context, Module, actions, app, fs, settings + +from ..modes.code_languages import code_languages +from .snippet_types import ( + InsertionSnippet, + Snippet, + SnippetLanguageState, + SnippetLists, + WrapperSnippet, +) +from .snippets_parser import create_snippets_from_file + +SNIPPETS_DIR = Path(__file__).parent / "snippets" + +mod = Module() + +mod.list("snippet", "List of insertion snippets") +mod.list("snippet_with_phrase", "List of insertion snippets containing a text phrase") +mod.list("snippet_wrapper", "List of wrapper snippets") + +mod.setting( + "snippets_dir", + type=str, + default=None, + desc="Directory (relative to Talon user) containing additional snippets", +) + +# `_` represents the global context, ie snippets available regardless of language +GLOBAL_ID = "_" + +# { SNIPPET_NAME: Snippet[] } +snippets_map: dict[str, list[Snippet]] = {} + +# { LANGUAGE_ID: SnippetLanguageState } +languages_state_map: dict[str, SnippetLanguageState] = { + GLOBAL_ID: SnippetLanguageState(Context(), SnippetLists()) +} + +# Create a context for each defined language +for lang in code_languages: + ctx = Context() + ctx.matches = f"code.language: {lang.id}" + languages_state_map[lang.id] = SnippetLanguageState(ctx, SnippetLists()) + + +def get_setting_dir(): + setting_dir = settings.get("user.snippets_dir") + if not setting_dir: + return None + + dir = Path(setting_dir) + + if not dir.is_absolute(): + user_dir = Path(actions.path.talon_user()) + dir = user_dir / dir + + return dir.resolve() + + +@mod.action_class +class Actions: + def get_snippets(name: str) -> list[Snippet]: + """Get snippets named """ + if name not in snippets_map: + raise ValueError(f"Unknown snippet '{name}'") + return snippets_map[name] + + def get_snippet(name: str) -> Snippet: + """Get snippet named for the active language""" + snippets: list[Snippet] = actions.user.get_snippets(name) + return get_preferred_snippet(snippets) + + def get_insertion_snippets(name: str) -> list[InsertionSnippet]: + """Get insertion snippets named """ + snippets: list[Snippet] = actions.user.get_snippets(name) + return [ + InsertionSnippet(s.body, s.insertion_scopes, s.languages) for s in snippets + ] + + def get_insertion_snippet(name: str) -> InsertionSnippet: + """Get insertion snippet named for the active language""" + snippet: Snippet = actions.user.get_snippet(name) + return InsertionSnippet( + snippet.body, + snippet.insertion_scopes, + snippet.languages, + ) + + def get_wrapper_snippets(name: str) -> list[WrapperSnippet]: + """Get wrapper snippets named """ + snippet_name, variable_name = split_wrapper_snippet_name(name) + snippets: list[Snippet] = actions.user.get_snippets(snippet_name) + return [to_wrapper_snippet(s, variable_name) for s in snippets] + + def get_wrapper_snippet(name: str) -> WrapperSnippet: + """Get wrapper snippet named for the active language""" + snippet_name, variable_name = split_wrapper_snippet_name(name) + snippet: Snippet = actions.user.get_snippet(snippet_name) + return to_wrapper_snippet(snippet, variable_name) + + +def get_preferred_snippet(snippets: list[Snippet]) -> Snippet: + lang: Union[str, set[str]] = actions.code.language() + languages = [lang] if isinstance(lang, str) else lang + + # First try to find a snippet matching the active language + for snippet in snippets: + if snippet.languages: + for snippet_lang in snippet.languages: + if snippet_lang in languages: + return snippet + + # Then look for a global snippet + for snippet in snippets: + if not snippet.languages: + return snippet + + raise ValueError(f"Snippet not available for language '{lang}'") + + +def split_wrapper_snippet_name(name: str) -> tuple[str, str]: + index = name.rindex(".") + return name[:index], name[index + 1 :] + + +def to_wrapper_snippet(snippet: Snippet, variable_name) -> WrapperSnippet: + """Get wrapper snippet named """ + var = snippet.get_variable_strict(variable_name) + return WrapperSnippet( + snippet.body, + var.name, + var.wrapper_scope, + snippet.languages, + ) + + +def update_snippets(): + global snippets_map + + snippets = get_snippets_from_files() + name_to_snippets: dict[str, list[Snippet]] = {} + language_to_lists: dict[str, SnippetLists] = {} + + for snippet in snippets: + # Map snippet names to actual snippets + name_to_snippets.setdefault(snippet.name, []).append(snippet) + + # Map languages to phrase / name dicts + for language in snippet.languages or [GLOBAL_ID]: + lists = language_to_lists.setdefault(language, SnippetLists()) + + for phrase in snippet.phrases or []: + lists.insertion[phrase] = snippet.name + + for var in snippet.variables: + if var.insertion_formatters: + lists.with_phrase[phrase] = snippet.name + + for var in snippet.variables: + for phrase in var.wrapper_phrases or []: + lists.wrapper[phrase] = f"{snippet.name}.{var.name}" + + snippets_map = name_to_snippets + update_contexts(language_to_lists) + + +def update_contexts(language_to_lists: dict[str, SnippetLists]): + global_lists = language_to_lists[GLOBAL_ID] or SnippetLists() + + for lang, lists in language_to_lists.items(): + if lang not in languages_state_map: + print(f"Found snippets for unknown language: {lang}") + actions.app.notify(f"Found snippets for unknown language: {lang}") + continue + + state = languages_state_map[lang] + insertion = {**global_lists.insertion, **lists.insertion} + with_phrase = {**global_lists.with_phrase, **lists.with_phrase} + wrapper = {**global_lists.wrapper, **lists.wrapper} + updated_lists: dict[str, dict[str, str]] = {} + + if state.lists.insertion != insertion: + state.lists.insertion = insertion + updated_lists["user.snippet"] = insertion + + if state.lists.with_phrase != with_phrase: + state.lists.with_phrase = with_phrase + updated_lists["user.snippet_with_phrase"] = with_phrase + + if state.lists.wrapper != wrapper: + state.lists.wrapper = wrapper + updated_lists["user.snippet_wrapper"] = wrapper + + if updated_lists: + state.ctx.lists.update(updated_lists) + + +def get_snippets_from_files() -> list[Snippet]: + setting_dir = get_setting_dir() + result = [] + + for file in SNIPPETS_DIR.glob("**/*.snippet"): + result.extend(create_snippets_from_file(file)) + + if setting_dir: + for file in setting_dir.glob("**/*.snippet"): + result.extend(create_snippets_from_file(file)) + + return result + + +def on_ready(): + fs.watch(SNIPPETS_DIR, lambda _path, _flags: update_snippets()) + + if get_setting_dir(): + fs.watch(get_setting_dir(), lambda _path, _flags: update_snippets()) + + update_snippets() + + +app.register("ready", on_ready) diff --git a/community/core/snippets/snippets.talon b/community/core/snippets/snippets.talon new file mode 100644 index 0000000..06117f3 --- /dev/null +++ b/community/core/snippets/snippets.talon @@ -0,0 +1,6 @@ +snip {user.snippet}: user.insert_snippet_by_name(snippet) + +snip {user.snippet_with_phrase} : + user.insert_snippet_by_name_with_phrase(snippet_with_phrase, text) + +snip next: user.move_cursor_to_next_snippet_stop() diff --git a/community/core/snippets/snippets/breakStatement.snippet b/community/core/snippets/snippets/breakStatement.snippet new file mode 100644 index 0000000..bb3c10e --- /dev/null +++ b/community/core/snippets/snippets/breakStatement.snippet @@ -0,0 +1,19 @@ +name: breakStatement +phrase: break +insertionScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | rust +- +break; +--- + +language: go | python | scala | stata | r +- +break +--- + +language: lua +- +break $0 +--- diff --git a/community/core/snippets/snippets/breakWithStatement.snippet b/community/core/snippets/snippets/breakWithStatement.snippet new file mode 100644 index 0000000..3a9f5a8 --- /dev/null +++ b/community/core/snippets/snippets/breakWithStatement.snippet @@ -0,0 +1,9 @@ +name: breakWithStatement +phrase: break with +insertionScope: statement +--- + +language: javascript | typescript | javascriptreact | typescriptreact | php | rust +- +break $0; +--- diff --git a/community/core/snippets/snippets/c.snippet b/community/core/snippets/snippets/c.snippet new file mode 100644 index 0000000..b468190 --- /dev/null +++ b/community/core/snippets/snippets/c.snippet @@ -0,0 +1,18 @@ +language: c | cpp +--- +name: typedefStructDeclaration +phrase: typedef struct +$0.insertionFormatter: PUBLIC_CAMEL_CASE +insertionScope: statement +- +typedef struct { + $1 +} $0 +--- + +name: preprocessorPragmaStatement +phrase: pre pragma | pragma +insertionScope: statement +- +#pragma $0 +--- diff --git a/community/core/snippets/snippets/caseStatement.snippet b/community/core/snippets/snippets/caseStatement.snippet new file mode 100644 index 0000000..37e8386 --- /dev/null +++ b/community/core/snippets/snippets/caseStatement.snippet @@ -0,0 +1,44 @@ +name: caseStatement +phrase: case +insertionScope: branch +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact +- +case $1: + $0 +--- + +language: python | php | go +- +case $1: + $0 +--- + +language: ruby +- +when $1 + $0 +--- + +language: scala +- +case $1 => $0 +--- + +language: elixir +- +$1 -> + $0 +--- + +language: r +- + $1 ~ $2, +) +--- + +language: kotlin +- +$1 -> $0 +--- diff --git a/community/core/snippets/snippets/catchStatement.snippet b/community/core/snippets/snippets/catchStatement.snippet new file mode 100644 index 0000000..78066c9 --- /dev/null +++ b/community/core/snippets/snippets/catchStatement.snippet @@ -0,0 +1,46 @@ +name: catchStatement +phrase: catch +--- + +language: cpp +- +catch (const std::exception& ex) { + $0 +} +--- + +language: java | csharp +- +catch(final Exception ex) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +catch(error) { + $0 +} +--- + +language: python +- +except Exception as ex: + $0 +--- + +language: php +- +catch (\Throwable \$exception) { + $0 +} +--- + +language: r +- +warning = function(w) { + $1 +}, error = function(e) { + $0 +}, +--- diff --git a/community/core/snippets/snippets/classDeclaration.snippet b/community/core/snippets/snippets/classDeclaration.snippet new file mode 100644 index 0000000..24a8009 --- /dev/null +++ b/community/core/snippets/snippets/classDeclaration.snippet @@ -0,0 +1,49 @@ +name: classDeclaration +phrase: class +insertionScope: class | statement + +$0.wrapperPhrase: class +$0.wrapperScope: statement +--- + +language: csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | kotlin + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +class $1 { + $0 +} +--- + +language: python + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +class $1: + $0 +--- + +language: ruby + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +class $1 + $0 +end +--- + +language: scala + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +class $1 ($0) +--- + +language: cpp + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +class $1 { + $0 +}; +--- diff --git a/community/core/snippets/snippets/codeBlock.snippet b/community/core/snippets/snippets/codeBlock.snippet new file mode 100644 index 0000000..aa948df --- /dev/null +++ b/community/core/snippets/snippets/codeBlock.snippet @@ -0,0 +1,23 @@ +name: codeBlock +phrase: block +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r | rust | go | css | scss | terraform | protobuf +- +{ + $0 +} +--- + +language: python +- +: + $0 +--- + +language: stata +- + + +$0 +--- diff --git a/community/core/snippets/snippets/commentBlock.snippet b/community/core/snippets/snippets/commentBlock.snippet new file mode 100644 index 0000000..25b5325 --- /dev/null +++ b/community/core/snippets/snippets/commentBlock.snippet @@ -0,0 +1,28 @@ +name: commentBlock +phrase: block comment +insertionScope: statement + +$0.insertionFormatter: CAPITALIZE_FIRST_WORD +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin +- +/* $0 */ +--- + +language: python +- +"""$0""" +--- + +language: xml | html +- + +--- + +language: lua +- +--[[ +$0 +--]] +--- diff --git a/community/core/snippets/snippets/commentDocumentation.snippet b/community/core/snippets/snippets/commentDocumentation.snippet new file mode 100644 index 0000000..252a0d0 --- /dev/null +++ b/community/core/snippets/snippets/commentDocumentation.snippet @@ -0,0 +1,21 @@ +name: commentDocumentation +phrase: doc comment | doc string +insertionScope: statement + +$0.insertionFormatter: CAPITALIZE_FIRST_WORD +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php +- +/** $0 */ +--- + +language: python +- +"""$0""" +--- + +language: rust +- +/// $0 +--- diff --git a/community/core/snippets/snippets/commentLine.snippet b/community/core/snippets/snippets/commentLine.snippet new file mode 100644 index 0000000..5c2dc94 --- /dev/null +++ b/community/core/snippets/snippets/commentLine.snippet @@ -0,0 +1,45 @@ +name: commentLine +phrase: comment + +$0.insertionFormatter: CAPITALIZE_FIRST_WORD +--- + +language: c | cpp | csharp | json | java | javascript | typescript | javascriptreact | typescriptreact | php | go | scala | kotlin | rust +- +// $0 +--- + +language: python | talon | csv | elixir | terraform | ruby | r +- +# $0 +--- + +language: xml | html | markdown +- + +--- + +language: vimscript +- +"$0 +--- + +language: r +- +#$0 +--- + +language: stata +- +* $0 +--- + +language: sql | lua +- +-- $0 +--- + +language: batch +- +REM $0 +--- diff --git a/community/core/snippets/snippets/constructorDeclaration.snippet b/community/core/snippets/snippets/constructorDeclaration.snippet new file mode 100644 index 0000000..6bb8f20 --- /dev/null +++ b/community/core/snippets/snippets/constructorDeclaration.snippet @@ -0,0 +1,24 @@ +name: constructorDeclaration +phrase: constructor +insertionScope: namedFunction | statement +--- + +language: cpp | csharp | java +- +$1($2) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +constructor($1) { + $0 +} +--- + +language: python +- +def __init__(self$1): + $0 +--- diff --git a/community/core/snippets/snippets/continue.snippet b/community/core/snippets/snippets/continue.snippet new file mode 100644 index 0000000..96c963d --- /dev/null +++ b/community/core/snippets/snippets/continue.snippet @@ -0,0 +1,24 @@ +name: continueStatement +phrase: continue +insertionScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | rust +- +continue; +--- + +language: go | python | scala | stata +- +continue +--- + +language: lua +- +goto continue +--- + +language: r +- +next +--- diff --git a/community/core/snippets/snippets/continueWith.snippet b/community/core/snippets/snippets/continueWith.snippet new file mode 100644 index 0000000..bfbdb50 --- /dev/null +++ b/community/core/snippets/snippets/continueWith.snippet @@ -0,0 +1,9 @@ +name: continueWithLabelStatement +phrase: continue with +insertionScope: statement +--- + +language: javascript | typescript | javascriptreact | typescriptreact | php | rust +- +continue $0; +--- diff --git a/community/core/snippets/snippets/defaultStatement.snippet b/community/core/snippets/snippets/defaultStatement.snippet new file mode 100644 index 0000000..fd576e3 --- /dev/null +++ b/community/core/snippets/snippets/defaultStatement.snippet @@ -0,0 +1,37 @@ +name: defaultStatement +phrase: default +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | go +- +default: + $0 +--- + +language: python +- +case _: + $0 +--- + +language: ruby +- +else + $0 +--- + +language: scala +- +case _ => $0 +--- + +language: elixir +- +_ -> + $0 +--- + +language: kotlin +- +else -> $0 +--- diff --git a/community/core/snippets/snippets/doWhileLoopStatement.snippet b/community/core/snippets/snippets/doWhileLoopStatement.snippet new file mode 100644 index 0000000..600ed88 --- /dev/null +++ b/community/core/snippets/snippets/doWhileLoopStatement.snippet @@ -0,0 +1,33 @@ +name: doWhileLoopStatement +phrase: do while +insertionScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact +- +do { + $0 +} while($1); +--- + +language: lua +- +repeat + $0 +until $1 +--- + +language: php +- +do { + $0 +} while ($1); +--- + +language: ruby +- +loop do + $0 + break if $1 +end +--- diff --git a/community/core/snippets/snippets/elixir.snippet b/community/core/snippets/snippets/elixir.snippet new file mode 100644 index 0000000..5afe9e0 --- /dev/null +++ b/community/core/snippets/snippets/elixir.snippet @@ -0,0 +1,12 @@ +language: elixir +--- + +name: conditionStatement +phrase: cond +insertionScope: statement +- +cond do + $1 -> + $0 +end +--- diff --git a/community/core/snippets/snippets/elseIfStatement.snippet b/community/core/snippets/snippets/elseIfStatement.snippet new file mode 100644 index 0000000..83a14c9 --- /dev/null +++ b/community/core/snippets/snippets/elseIfStatement.snippet @@ -0,0 +1,54 @@ +name: elseIfStatement +phrase: elif +insertionScope: statement + +$1.wrapperPhrase: elif cond +$1.wrapperScope: statement +$0.wrapperPhrase: elif +$0.wrapperScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | scala | kotlin | r +- +else if ($1) { + $0 +} +--- + +language: python +- +elif $1: + $0 +--- + +language: lua +- +elseif $1 then + $0 +--- + +language: ruby +- +elsif $1 + $0 +--- + +language: vimscript +- +elseif $1 + $0 +--- + +language: php +- +elseif ($1) { + $0 +} +--- + +language: stata | go | rust +- +else if $1 { + $0 +} +--- diff --git a/community/core/snippets/snippets/elseStatement.snippet b/community/core/snippets/snippets/elseStatement.snippet new file mode 100644 index 0000000..5e4d76a --- /dev/null +++ b/community/core/snippets/snippets/elseStatement.snippet @@ -0,0 +1,38 @@ +name: elseStatement +phrase: else +insertionScope: statement + +$0.wrapperPhrase: else +$0.wrapperScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | r | php | stata | go | scala | kotlin | rust +- +else { + $0 +} +--- + +language: python +- +else: + $0 +--- + +language: lua | elixir +- +else + $0 +--- + +language: vimscript +- +else + $0 +--- + +language: ruby +- +else + $0 +--- diff --git a/community/core/snippets/snippets/finallyStatement.snippet b/community/core/snippets/snippets/finallyStatement.snippet new file mode 100644 index 0000000..9c1c0da --- /dev/null +++ b/community/core/snippets/snippets/finallyStatement.snippet @@ -0,0 +1,24 @@ +name: finallyStatement +phrase: finally +--- + +language: csharp | java | javascript | typescript | javascriptreact | typescriptreact +- +finally { + $0 +} +--- + +language: python +- +finally: + $0 +--- + + +language: r +- +finally = { + $0 +}) +--- diff --git a/community/core/snippets/snippets/forEachMutableStatement.snippet b/community/core/snippets/snippets/forEachMutableStatement.snippet new file mode 100644 index 0000000..9e46c0d --- /dev/null +++ b/community/core/snippets/snippets/forEachMutableStatement.snippet @@ -0,0 +1,25 @@ +name: forEachMutableStatement +phrase: for each mute | each mute +insertionScope: statement +--- + +language: cpp +- +for (auto &$1 : $2) { + $0 +} +--- + +language: java +- +for ($1 : $2) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +for ($1 of $2) { + $0 +} +--- diff --git a/community/core/snippets/snippets/forEachStatement.snippet b/community/core/snippets/snippets/forEachStatement.snippet new file mode 100644 index 0000000..9719598 --- /dev/null +++ b/community/core/snippets/snippets/forEachStatement.snippet @@ -0,0 +1,104 @@ +name: forEachStatement +phrase: for each | each +insertionScope: statement +--- + +language: cpp +- +for (const auto &$1 : $2) { + $0 +} +--- + +language: csharp +- +foreach ($1 in $2) { + $0 +} +--- + +language: java +- +for (final $1 : $2) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +for (const $1 of $2) { + $0 +} +--- + +language: python +- +for $1 in $2: + $0 +--- + +language: rust +- +for $1 in $2 { + $0 +} +--- + +language: lua +- +for $1 in $2 do + $0 +end +--- + +language: ruby +- +.each do |$1| + $0 +end +--- + +language: scala +- +for ($1 <- $0) +--- + +language: terraform +- +for $1 in $2 : $0 +--- + +language: go +- +for $1 := range $2 { + $0 +} +--- + +language: php +- +foreach ($1 as $2) { + $0 +} +--- + +language: r | kotlin +- +for ($1 in $2) { + $0 +} +--- + +language: stata +- +foreach $1 in $2 { + $0 +} +--- + +language: r +- +for ($1 in $2) { + $0 +} +--- diff --git a/community/core/snippets/snippets/forLoopStatement.snippet b/community/core/snippets/snippets/forLoopStatement.snippet new file mode 100644 index 0000000..a66a90e --- /dev/null +++ b/community/core/snippets/snippets/forLoopStatement.snippet @@ -0,0 +1,69 @@ +name: forLoopStatement +phrase: for +insertionScope: statement +--- + +language: scala +- +for ($1) $0 +--- + +language: cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact +- +for ($1) { + $0 +} +--- + +language: php | c +- +for ($1; $2; $3) { + $0 +} +--- + +language: stata +- +forval $1 { + $0 +} +--- + +language: elixir | lua +- +for $1 do + $0 +end +--- + +language: rust +- +for $1 in $2 { + $0 +} +--- + +language: go +- +for $1 { + $0 +} +--- + +language: r | kotlin +- +for ($1 in $2) { + $0 +} +--- + +language: python +- +for $1 in $2: + $0 +--- + +language: terraform +- +for $1 in $2 : $0 +--- diff --git a/community/core/snippets/snippets/forRangeStatement.snippet b/community/core/snippets/snippets/forRangeStatement.snippet new file mode 100644 index 0000000..b2faa5d --- /dev/null +++ b/community/core/snippets/snippets/forRangeStatement.snippet @@ -0,0 +1,66 @@ +name: forRangeStatement +phrase: for range +insertionScope: statement +--- + +language: c | csharp | java +- +for (int i = 0; i < $1; ++i) { + $0 +} +--- + +language: cpp +- +for (size_t i = 0; i < $1; ++i) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +for (let i = 0; i < $1; ++i) { + $0 +} +--- + +language: python +- +for i in range($1): + $0 +--- + +language: scala +- +for (i <- 0 until $1) { + $0 +} +--- + +language: go +- +for i := 0; i < $1; i++ { + $0 +} +--- + +language: stata +- +forval $1 { + $0 +} +--- + +language: r +- +for (${1} in 1:${2}) { + $0 +} +--- + +language: rust +- +for i in 0..$1 { + $0 +} +--- diff --git a/community/core/snippets/snippets/formatString.snippet b/community/core/snippets/snippets/formatString.snippet new file mode 100644 index 0000000..c2af81d --- /dev/null +++ b/community/core/snippets/snippets/formatString.snippet @@ -0,0 +1,33 @@ +name: formatString +phrase: format +--- + +language: cpp +- +std::format("$0") +--- + +language: csharp +- +String.Format("$0") +--- + +language: java +- +String.format("$0") +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +`$0` +--- + +language: python +- +f"$0" +--- + +language: r +- +${1:glue::}glue("$0") +--- diff --git a/community/core/snippets/snippets/functionCall.snippet b/community/core/snippets/snippets/functionCall.snippet new file mode 100644 index 0000000..9416be5 --- /dev/null +++ b/community/core/snippets/snippets/functionCall.snippet @@ -0,0 +1,16 @@ +name: functionCall +phrase: call +insertionScope: statement + +$0.wrapperPhrase: call +--- + +language: c | cpp | csharp | python | talon | java | javascript | typescript | javascriptreact | typescriptreact | css | scss | sql | r +- +$1($0) +--- + +language: stata +- +$1 $0 +--- diff --git a/community/core/snippets/snippets/functionDeclaration.snippet b/community/core/snippets/snippets/functionDeclaration.snippet new file mode 100644 index 0000000..98520fe --- /dev/null +++ b/community/core/snippets/snippets/functionDeclaration.snippet @@ -0,0 +1,75 @@ +name: functionDeclaration +phrase: funk +insertionScope: namedFunction | statement + +$0.wrapperPhrase: funk +$0.wrapperScope: statement +--- + +language: c | cpp | csharp | java + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +- +void $1($2) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +- +function $1($2) { + $0 +} +--- + +language: python + +$1.insertionFormatter: SNAKE_CASE +- +def $1($2): + $0 +--- + +language: rust +$1.insertionFormatter: SNAKE_CASE +- +fn $1($2) { + $0 +} +--- + +language: ruby + +$1.insertionFormatter: SNAKE_CASE +- +def $1($2) + $0 +end +--- + +language: vimscript +- +function $1($2) + $0 +endfunction +--- + +language: php + +$1.insertionFormatter: SNAKE_CASE +- +function $1($2) { + $0 +} +--- + +language: r + +$1.insertionFormatter: SNAKE_CASE +- +$1 <- function($2){ + $0 +} +--- diff --git a/community/core/snippets/snippets/global.snippet b/community/core/snippets/snippets/global.snippet new file mode 100644 index 0000000..47cc1f2 --- /dev/null +++ b/community/core/snippets/snippets/global.snippet @@ -0,0 +1,15 @@ +name: codeQuote +phrase: code +- +``` +$0 +``` +--- + +name: codeQuoteLanguage +phrase: code lang +- +```$1 +$0 +``` +--- diff --git a/community/core/snippets/snippets/goToStatement.snippet b/community/core/snippets/snippets/goToStatement.snippet new file mode 100644 index 0000000..09ad997 --- /dev/null +++ b/community/core/snippets/snippets/goToStatement.snippet @@ -0,0 +1,14 @@ +name: goToStatement +phrase: go to +insertionScope: statement +--- + +language: c | cpp | csharp | php +- +goto $0; +--- + +language: lua +- +goto $0 +--- diff --git a/community/core/snippets/snippets/html.snippet b/community/core/snippets/snippets/html.snippet new file mode 100644 index 0000000..ec453eb --- /dev/null +++ b/community/core/snippets/snippets/html.snippet @@ -0,0 +1,33 @@ +language: html | javascriptreact | typescriptreact +--- + +name: attributeClassName +phrase: class name +- +className="$0" +--- + +name: table +phrase: table +- + + + + + + + + + + + +
$1
$0
+--- + +name: unorderedList +phrase: list +- +
    +
  • $0
  • +
+--- diff --git a/community/core/snippets/snippets/ifElseStatement.snippet b/community/core/snippets/snippets/ifElseStatement.snippet new file mode 100644 index 0000000..fcf1c55 --- /dev/null +++ b/community/core/snippets/snippets/ifElseStatement.snippet @@ -0,0 +1,71 @@ +name: ifElseStatement +phrase: if else +insertionScope: statement + +$1.wrapperPhrase: if else cond +$1.wrapperScope: statement +$2.wrapperPhrase: if else +$2.wrapperScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r +- +if ($1) { + $2 +} else { + $0 +} +--- + +language: python +- +if $1: + $2 +else: + $0 +--- + +language: lua +- +if $1 then + $2 +else + $0 +end +--- + +language: ruby +- +if $1 + $2 +else + $0 +end +--- + +language: vimscript +- +if $1 + $2 +else + $0 +endif +--- + +language: rust | stata | go +- +if $1 { + $2 +} else { + $0 +} +--- + +language: elixir +- +if $1 do + $2 +else + $0 +end +--- diff --git a/community/core/snippets/snippets/ifStatement.snippet b/community/core/snippets/snippets/ifStatement.snippet new file mode 100644 index 0000000..ff0a217 --- /dev/null +++ b/community/core/snippets/snippets/ifStatement.snippet @@ -0,0 +1,57 @@ +name: ifStatement +phrase: if +insertionScope: statement + +$1.wrapperPhrase: if cond +$1.wrapperScope: statement +$0.wrapperPhrase: if +$0.wrapperScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r +- +if ($1) { + $0 +} +--- + +language: python +- +if $1: + $0 +--- + +language: lua +- +if $1 then + $0 +end +--- + +language: ruby +- +if $1 + $0 +end +--- + +language: vimscript +- +if $1 + $0 +endif +--- + +language: rust | stata | go +- +if $1 { + $0 +} +--- + +language: elixir +- +if $1 do + $0 +end +--- diff --git a/community/core/snippets/snippets/import.snippet b/community/core/snippets/snippets/import.snippet new file mode 100644 index 0000000..ee748b3 --- /dev/null +++ b/community/core/snippets/snippets/import.snippet @@ -0,0 +1,64 @@ +name: importStatement +phrase: import + +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +import $0 from "$0"; +--- + +language: python | go | scala +- +import $0 +--- + +language: lua +- +local $1 = require('$0') +--- + +language: ruby +- +require "$0" +--- + +language: java +- +import $0; +--- + +language: php | rust +- +use $0; +--- + +language: r +- +library($0) +--- + +language: stata +- +ssc install $0 +--- + +language: css | scss +- +@import $0 +--- + +language: csharp +- +using $0; +--- + +language: r +- +library(${1}${2:, warn.conflicts = FALSE}) +--- + +language: c +- +#include $0 +--- diff --git a/community/core/snippets/snippets/importFrom.snippet b/community/core/snippets/snippets/importFrom.snippet new file mode 100644 index 0000000..c2af82c --- /dev/null +++ b/community/core/snippets/snippets/importFrom.snippet @@ -0,0 +1,23 @@ +name: importFromStatement +phrase: import from + +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +import $1 from "$0"; +--- + +language: python +- +from $1 import $0 +--- + +language: r +- +library( + ${1}, + include.only = c(${0})${2:, + warn.conflicts = FALSE} +) +--- diff --git a/community/core/snippets/snippets/importStar.snippet b/community/core/snippets/snippets/importStar.snippet new file mode 100644 index 0000000..9b473c3 --- /dev/null +++ b/community/core/snippets/snippets/importStar.snippet @@ -0,0 +1,19 @@ +name: importStarStatement +phrase: import star + +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +import * as $0 from "$0"; +--- + +language: python +- +from $0 import * +--- + +language: rust +- +use $0::*; +--- diff --git a/community/core/snippets/snippets/includeHeaderStatement.snippet b/community/core/snippets/snippets/includeHeaderStatement.snippet new file mode 100644 index 0000000..f597cf3 --- /dev/null +++ b/community/core/snippets/snippets/includeHeaderStatement.snippet @@ -0,0 +1,9 @@ +name: includeHeaderStatement +phrase: include header | include head +insertionScope: statement +--- + +language: c | cpp +- +#include "$0.h" +--- diff --git a/community/core/snippets/snippets/includeLocalStatement.snippet b/community/core/snippets/snippets/includeLocalStatement.snippet new file mode 100644 index 0000000..cafc97f --- /dev/null +++ b/community/core/snippets/snippets/includeLocalStatement.snippet @@ -0,0 +1,9 @@ +name: includeLocalStatement +phrase: include local | include low +insertionScope: statement +--- + +language: c | cpp +- +#include "$0" +--- diff --git a/community/core/snippets/snippets/includeSystemStatement.snippet b/community/core/snippets/snippets/includeSystemStatement.snippet new file mode 100644 index 0000000..c9ea791 --- /dev/null +++ b/community/core/snippets/snippets/includeSystemStatement.snippet @@ -0,0 +1,9 @@ +name: includeSystemStatement +phrase: include system | include sys +insertionScope: statement +--- + +language: c | cpp +- +#include <$0> +--- diff --git a/community/core/snippets/snippets/infiniteLoopStatement.snippet b/community/core/snippets/snippets/infiniteLoopStatement.snippet new file mode 100644 index 0000000..7ad9c97 --- /dev/null +++ b/community/core/snippets/snippets/infiniteLoopStatement.snippet @@ -0,0 +1,11 @@ +name: infiniteLoopStatement +phrase: loop +insertionScope: statement +--- + +language: rust +- +loop { + $0 +} +--- diff --git a/community/core/snippets/snippets/item.snippet b/community/core/snippets/snippets/item.snippet new file mode 100644 index 0000000..c78d58d --- /dev/null +++ b/community/core/snippets/snippets/item.snippet @@ -0,0 +1,19 @@ +name: item +phrase: item +insertionScope: collectionItem +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +$1: $0, +--- + +language: python +- +"$1": $0, +--- + +language: r +- +${1}="$0", +--- diff --git a/community/core/snippets/snippets/javascript.snippet b/community/core/snippets/snippets/javascript.snippet new file mode 100644 index 0000000..abe0aec --- /dev/null +++ b/community/core/snippets/snippets/javascript.snippet @@ -0,0 +1,53 @@ +language: javascript | typescript | javascriptreact | typescriptreact +--- + +name: forInLoopStatement +phrase: for in +insertionScope: statement +- +for (const $1 in $2) { + $0 +} +--- + +name: anonymousFunctionDeclarationAndCall +phrase: self calling +- +(() => { + $0 +})(); +--- + +name: namedLambdaExpression +phrase: arrow funk +insertionScope: statement +- +const $1 = ($2) => { + $0 +} +--- + +name: printTimeStatement +phrase: print time +insertionScope: statement +- +console.time("$0"); +--- + +name: constAssignment +phrase: const +insertionScope: statement + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +- +const $1 = $0; +--- + +name: letAssignment +phrase: let +insertionScope: statement + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +- +let $1 = $0; +--- diff --git a/community/core/snippets/snippets/javascriptreact.snippet b/community/core/snippets/snippets/javascriptreact.snippet new file mode 100644 index 0000000..139190e --- /dev/null +++ b/community/core/snippets/snippets/javascriptreact.snippet @@ -0,0 +1,30 @@ +language: javascript | javascriptreact | typescriptreact +--- + +name: attribute +- +$name={$0} +--- + +name: reactUseState +phrase: use state +insertionScope: statement +- +const [$1, set${1/(.)/${1:/capitalize}/}] = useState($0); +--- + +name: reactUseRef +phrase: use ref +insertionScope: statement +- +const $0 = useRef(); +--- + +name: reactUseEffect +phrase: use effect +insertionScope: statement +- +useEffect(() => { + $0 +}, []); +--- diff --git a/community/core/snippets/snippets/lambdaExpression.snippet b/community/core/snippets/snippets/lambdaExpression.snippet new file mode 100644 index 0000000..4deda0e --- /dev/null +++ b/community/core/snippets/snippets/lambdaExpression.snippet @@ -0,0 +1,38 @@ +name: lambdaExpression +phrase: lambda + +$0.wrapperPhrase: lambda +$0.wrapperScope: statement +--- + +language: csharp | javascript | typescript | javascriptreact | typescriptreact +- +($1) => { + $0 +} +--- + +language: java +- +($1) -> { + $0 +} +--- + +language: python +- +lambda $1: $0 +--- + +language: r +- +function($1){ $0 } +--- + + +language: cpp +- +[$1]($2) { + $0 +} +--- diff --git a/community/core/snippets/snippets/lua.snippet b/community/core/snippets/snippets/lua.snippet new file mode 100644 index 0000000..b169f96 --- /dev/null +++ b/community/core/snippets/snippets/lua.snippet @@ -0,0 +1,28 @@ +language: lua +--- + +name: forInIPairs +phrase: for eye pairs +insertionScope: statement +$1.insertionFormatter: SNAKE_CASE +- +for _, $1 in ipairs($2) do + $0 +end +--- + +name: forInPairs +phrase: for pairs +insertionScope: statement +- +for ${1:k}, ${2:v} in pairs($3) do + $0 +end +--- + +name: tryCatchStatement +phrase: try catch +insertionScope: statement +- +pcall($0) +--- diff --git a/community/core/snippets/snippets/markdown.snippet b/community/core/snippets/snippets/markdown.snippet new file mode 100644 index 0000000..1f65952 --- /dev/null +++ b/community/core/snippets/snippets/markdown.snippet @@ -0,0 +1,45 @@ +language: markdown +--- + +name: link +phrase: link +- +[$1]($0) +--- + +name: image +phrase: image +- +![$1]($0) +--- + + +name: checkbox +phrase: to do | check box | tick box +- +* [ ] $0 +--- + +name: bold +phrase: bold | strong +- +**$0** +--- + +name: italic +phrase: italic | emph | emphasis +- +*$0* +--- + +name: italic bold +phrase: italic bold | bold italic | strong emphasis | strong emph +- +***$0*** +--- + +name: quote +phrase: quote +- +> $0 +--- diff --git a/community/core/snippets/snippets/methodDeclaration.snippet b/community/core/snippets/snippets/methodDeclaration.snippet new file mode 100644 index 0000000..fd260ef --- /dev/null +++ b/community/core/snippets/snippets/methodDeclaration.snippet @@ -0,0 +1,13 @@ +name: methodDeclaration +phrase: method +insertionScope: namedFunction | statement +--- + +language: javascript | typescript | javascriptreact | typescriptreact + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +- +$1($2) { + $0 +} +--- diff --git a/community/core/snippets/snippets/newInstance.snippet b/community/core/snippets/snippets/newInstance.snippet new file mode 100644 index 0000000..9d9330a --- /dev/null +++ b/community/core/snippets/snippets/newInstance.snippet @@ -0,0 +1,13 @@ +name: newInstance +phrase: instance +--- + +language: cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact +- +new $1($0); +--- + +language: python +- +$1($0) +--- diff --git a/community/core/snippets/snippets/preprocessorDefineStatement.snippet b/community/core/snippets/snippets/preprocessorDefineStatement.snippet new file mode 100644 index 0000000..c70d258 --- /dev/null +++ b/community/core/snippets/snippets/preprocessorDefineStatement.snippet @@ -0,0 +1,10 @@ +name: preprocessorDefineStatement +phrase: pre define | define +insertionScope: statement +--- + +language: c | cpp +$0.insertionFormatter: ALL_CAPS,SNAKE_CASE +- +#define $0 +--- diff --git a/community/core/snippets/snippets/preprocessorElseIfStatement.snippet b/community/core/snippets/snippets/preprocessorElseIfStatement.snippet new file mode 100644 index 0000000..d68403c --- /dev/null +++ b/community/core/snippets/snippets/preprocessorElseIfStatement.snippet @@ -0,0 +1,9 @@ +name: preprocessorElseIfStatement +phrase: pre elif +insertionScope: statement +--- + +language: c | cpp +- +#elif $0 +--- diff --git a/community/core/snippets/snippets/preprocessorEndIf.snippet b/community/core/snippets/snippets/preprocessorEndIf.snippet new file mode 100644 index 0000000..2ba48ae --- /dev/null +++ b/community/core/snippets/snippets/preprocessorEndIf.snippet @@ -0,0 +1,9 @@ +name: preprocessorEndIfStatement +phrase: pre end if | end if +insertionScope: statement +--- + +language: c | cpp +- +#endif $0 +--- diff --git a/community/core/snippets/snippets/preprocessorErrorStatement.snippet b/community/core/snippets/snippets/preprocessorErrorStatement.snippet new file mode 100644 index 0000000..78bfc25 --- /dev/null +++ b/community/core/snippets/snippets/preprocessorErrorStatement.snippet @@ -0,0 +1,9 @@ +name: preprocessorErrorStatement +phrase: pre error | error +insertionScope: statement +--- + +language: c | cpp +- +#error $0 +--- diff --git a/community/core/snippets/snippets/preprocessorIfDefineStatement.snippet b/community/core/snippets/snippets/preprocessorIfDefineStatement.snippet new file mode 100644 index 0000000..3f88808 --- /dev/null +++ b/community/core/snippets/snippets/preprocessorIfDefineStatement.snippet @@ -0,0 +1,10 @@ +name: preprocessorIfDefineStatement +phrase: pre if deaf | if deaf +insertionScope: statement +--- + +language: c | cpp +$0.insertionFormatter: ALL_CAPS,SNAKE_CASE +- +#ifdef $0 +--- diff --git a/community/core/snippets/snippets/preprocessorIfStatement.snippet b/community/core/snippets/snippets/preprocessorIfStatement.snippet new file mode 100644 index 0000000..0e811d1 --- /dev/null +++ b/community/core/snippets/snippets/preprocessorIfStatement.snippet @@ -0,0 +1,9 @@ +name: preprocessorIfStatement +phrase: pre if +insertionScope: statement +--- + +language: c | cpp +- +#if $0 +--- diff --git a/community/core/snippets/snippets/preprocessorUndefineStatement.snippet b/community/core/snippets/snippets/preprocessorUndefineStatement.snippet new file mode 100644 index 0000000..7fd824c --- /dev/null +++ b/community/core/snippets/snippets/preprocessorUndefineStatement.snippet @@ -0,0 +1,10 @@ +name: preprocessorUndefineStatement +phrase: pre undeaf | undeaf +insertionScope: statement +--- + +language: c | cpp +$0.insertionFormatter: ALL_CAPS,SNAKE_CASE +- +#undef $0 +--- diff --git a/community/core/snippets/snippets/printStatement.snippet b/community/core/snippets/snippets/printStatement.snippet new file mode 100644 index 0000000..2bc2cee --- /dev/null +++ b/community/core/snippets/snippets/printStatement.snippet @@ -0,0 +1,36 @@ +name: printStatement +phrase: print +insertionScope: statement + +$0.wrapperPhrase: print +--- + +language: cpp +- +std::printf($0); +--- + +language: java +- +System.out.println($0); +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +console.log($0); +--- + +language: python | talon | r +- +print($0) +--- + +language: csharp +- +Console.WriteLine($0); +--- + +language: rust +- +println!($0); +--- diff --git a/community/core/snippets/snippets/python.snippet b/community/core/snippets/snippets/python.snippet new file mode 100644 index 0000000..f823397 --- /dev/null +++ b/community/core/snippets/snippets/python.snippet @@ -0,0 +1,96 @@ +language: python +--- + +name: talonModuleDeclaration +phrase: module +insertionScope: statement +- +mod = Module() +--- + +name: talonContextDeclaration +phrase: context +insertionScope: statement +- +ctx = Context() +--- + +name: talonAppDeclaration +phrase: module app +insertionScope: statement +- +mod.apps.$1 = r""" +$0 +""" +--- + +name: talonModuleClass +phrase: module class +insertionScope: class | statement +- +@mod.action_class +class Actions: + $0 +--- + +name: talonContextMatch +phrase: context match +insertionScope: statement +- +ctx.matches = r""" +$0 +""" +--- + +name: talonContextList +phrase: context list +insertionScope: statement +- +ctx.lists["user.$1"] = { + $0 +} +--- + +name: talonContextClass +phrase: context class +insertionScope: class | statement +- +@ctx.action_class("$1") +class $2Actions: + $0 +--- + +name: suppressError +phrase: suppress error +- +with suppress(AttributeError): + $0 +--- + +name: listComprehension +phrase: list comp +insertionScope: statement +- +[$2 for $2 in $1 if $0] +--- + +name: setComprehension +phrase: set comp +insertionScope: statement +- +{$0 for $0 in $1} +--- + +name: dictComprehension +phrase: dict comp +insertionScope: statement +- +{$0: $2 for $2 in $1} +--- + +name: generatorExpression +phrase: gen comp +insertionScope: statement +- +($2 for $2 in $1 if $0) +--- diff --git a/community/core/snippets/snippets/returnStatement.snippet b/community/core/snippets/snippets/returnStatement.snippet new file mode 100644 index 0000000..0ff5def --- /dev/null +++ b/community/core/snippets/snippets/returnStatement.snippet @@ -0,0 +1,19 @@ +name: returnStatement +phrase: return +insertionScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | rust +- +return $0; +--- + +language: python | go | kotlin | lua | ruby | scala +- +return $0 +--- + +language: r +- +return($0) +--- diff --git a/community/core/snippets/snippets/rust.snippet b/community/core/snippets/snippets/rust.snippet new file mode 100644 index 0000000..37356d3 --- /dev/null +++ b/community/core/snippets/snippets/rust.snippet @@ -0,0 +1,104 @@ +language: rust +--- + +name: implementsStruct +phrase: imp | implement +insertionScope: statement + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +impl $1 { + $0 +} +--- + +name: implementsGenericStruct +phrase: generic imp | generic implement | gen imp | gen implement +insertionScope: statement + +$2.insertionFormatter: PUBLIC_CAMEL_CASE +- +impl<$1> $2<$3> { + $0 +} +--- + +name: enumTypeDeclaration +phrase: enum +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +enum $1 { + $0 +} +--- + +name: traitDeclaration +phrase: trait +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +trait $1 { + $0 +} +--- + +name: traitImplementation +phrase: implement trait | imp trait +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +impl $1 for $2 { + $0 +} +--- + +name: ifLetStatement +phrase: if let +insertionScope: statement +- +if let $1 = $2 { + $0 +} +--- + +name: letElseStatement +phrase: let else +insertionScope: statement +- +let $1 = $2 else { + return $0; +}; +--- + +name: unsafeBlock +phrase: unsafe +- +unsafe { + $0 +} +--- + +name: attributeStatement +phrase: attribute | attr +$0.insertionFormatter: SNAKE_CASE +- +#[$0] +--- + +name: moduleDeclaration +phrase: mod | module +$1.insertionFormatter: SNAKE_CASE +- +mod $1 { + $0 +} +--- + +name: testModuleDeclaration +phrase: test mod | test module +- +#[cfg(test)] +mod tests { + use super::*; + + $0 +} +--- diff --git a/community/core/snippets/snippets/shellscript.snippet b/community/core/snippets/snippets/shellscript.snippet new file mode 100644 index 0000000..17a56b9 --- /dev/null +++ b/community/core/snippets/snippets/shellscript.snippet @@ -0,0 +1,11 @@ +language: shellscript +--- + +name: shebang +phrase: shebang +- +#!/usr/bin/env bash +set -euo pipefail + +$0 +--- diff --git a/community/core/snippets/snippets/sql.snippet b/community/core/snippets/snippets/sql.snippet new file mode 100644 index 0000000..27f8b28 --- /dev/null +++ b/community/core/snippets/snippets/sql.snippet @@ -0,0 +1,20 @@ +language: sql +--- + +name: createTable +phrase: create table +insertionScope: statement +- +CREATE TABLE $1( + $0 +); +--- + +name: withStatement +phrase: with +insertionScope: statement +- +WITH $1 AS ( + SELECT $0 +) +--- diff --git a/community/core/snippets/snippets/structDeclaration.snippet b/community/core/snippets/snippets/structDeclaration.snippet new file mode 100644 index 0000000..07996b0 --- /dev/null +++ b/community/core/snippets/snippets/structDeclaration.snippet @@ -0,0 +1,19 @@ +name: structDeclaration +phrase: struct +$1.insertionFormatter: PUBLIC_CAMEL_CASE +--- + +language: rust +insertionScope: class +- +struct $1 { + $0 +} +--- + +language: cpp +insertionScope: statement +- +struct $1 { + $0 +}; diff --git a/community/core/snippets/snippets/switchStatement.snippet b/community/core/snippets/snippets/switchStatement.snippet new file mode 100644 index 0000000..25031b7 --- /dev/null +++ b/community/core/snippets/snippets/switchStatement.snippet @@ -0,0 +1,67 @@ +name: switchStatement +phrase: switch +insertionScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php +- +switch ($1) { + $0 +} +--- + +language: python +- +match $1: + $0 +--- + +language: rust +- +match $1 { + $0 +} +--- + +language: kotlin +- +when ($1) { + $0 +} +--- + +language: ruby +- +case $1 + $0 +end +--- + +language: scala +- +$1 match { + $0 +} +--- + +language: go +- +switch $1 { + $0 +} +--- + +language: elixir +- +case $1 do + $0 +end +--- + +language: r +- +case_when( + $0 + .default = ${1} +) +--- diff --git a/community/core/snippets/snippets/talon.snippet b/community/core/snippets/snippets/talon.snippet new file mode 100644 index 0000000..61f2416 --- /dev/null +++ b/community/core/snippets/snippets/talon.snippet @@ -0,0 +1,11 @@ +language: talon +--- + +name: voiceCommandDeclaration +phrase: command +insertionScope: command + +$0.insertionFormatter: NOOP +- +$0: user.run_rpc_command("$CLIPBOARD") +--- diff --git a/community/core/snippets/snippets/ternary.snippet b/community/core/snippets/snippets/ternary.snippet new file mode 100644 index 0000000..0cc76e6 --- /dev/null +++ b/community/core/snippets/snippets/ternary.snippet @@ -0,0 +1,33 @@ +name: ternary +phrase: ternary +--- + +language: c | cpp | java | csharp | javascript | typescript | javascriptreact | typescriptreact | terraform | ruby +- +$1 ? $2 : $0 +--- + +language: python +- +$1 if $2 else $0 +--- + +language: lua +- +$1 and $2 or $0 +--- + +language: rust +- +if $1 { $2 } else { $0 } +--- + +language: scala | kotlin +- +if ($1) $2 else $0 +--- + +language: r +- +if ($2) { $1 } else { $0 } +--- diff --git a/community/core/snippets/snippets/throwException.snippet b/community/core/snippets/snippets/throwException.snippet new file mode 100644 index 0000000..32cdd55 --- /dev/null +++ b/community/core/snippets/snippets/throwException.snippet @@ -0,0 +1,33 @@ +name: throwException +phrase: exception +--- + +language: cpp +- +throw std::runtime_error("$0"); +--- + +language: java +- +throw new Exception(String.format("$0")); +--- + +language: csharp +- +throw new Exception(String.Format("$0")); +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +throw Error(`$0`); +--- + +language: python +- +raise ValueError(f"$0") +--- + +language: r +- +stop($0) +--- diff --git a/community/core/snippets/snippets/tryCatchStatement.snippet b/community/core/snippets/snippets/tryCatchStatement.snippet new file mode 100644 index 0000000..93022be --- /dev/null +++ b/community/core/snippets/snippets/tryCatchStatement.snippet @@ -0,0 +1,60 @@ +name: tryCatchStatement +phrase: try catch +insertionScope: statement + +$1.wrapperPhrase: try +$1.wrapperScope: statement +$0.wrapperPhrase: catch +$0.wrapperScope: statement +--- + +language: cpp +- +try { + $1 +} +catch (const std::exception& e) { + $0 +} +--- + +language: csharp | java +- +try { + $1 +} +catch(final Exception ex) { + $0 +} +--- + +language: javascript | typescript | javascriptreact | typescriptreact +- +try { + $1 +} +catch(error) { + $0 +} +--- + +language: python +- +try: + $1 +except Exception as ex: + $0 +--- + +language: r +- +tryCatch({ + $0 +}, warning = function(w) { + $1 +}, error = function(e) { + $2 +}, finally = { + $3 +}) +--- diff --git a/community/core/snippets/snippets/tryStatement.snippet b/community/core/snippets/snippets/tryStatement.snippet new file mode 100644 index 0000000..613ce6d --- /dev/null +++ b/community/core/snippets/snippets/tryStatement.snippet @@ -0,0 +1,31 @@ +name: tryStatement +phrase: try +insertionScope: statement +--- + +language: cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php +- +try { + $0 +} +--- + +language: python +- +try: + $0 +--- + +language: elixir +- +try do + $0 +end +--- + +language: r +- +tryCatch({ + $0 +}, +--- diff --git a/community/core/snippets/snippets/typescript.snippet b/community/core/snippets/snippets/typescript.snippet new file mode 100644 index 0000000..7de546a --- /dev/null +++ b/community/core/snippets/snippets/typescript.snippet @@ -0,0 +1,13 @@ +language: typescript | typescriptreact +--- + +name: interfaceDeclaration +phrase: interface +insertionScope: statement + +$1.insertionFormatter: PUBLIC_CAMEL_CASE +- +interface $1 { + $0 +} +--- diff --git a/community/core/snippets/snippets/unlessStatement.snippet b/community/core/snippets/snippets/unlessStatement.snippet new file mode 100644 index 0000000..be1337d --- /dev/null +++ b/community/core/snippets/snippets/unlessStatement.snippet @@ -0,0 +1,11 @@ +name: unlessStatement +phrase: unless +insertionScope: statement +--- + +language: ruby +- +unless $1 + $0 +end +--- diff --git a/community/core/snippets/snippets/untilLoopStatement.snippet b/community/core/snippets/snippets/untilLoopStatement.snippet new file mode 100644 index 0000000..5bd3d30 --- /dev/null +++ b/community/core/snippets/snippets/untilLoopStatement.snippet @@ -0,0 +1,11 @@ +name: untilLoopStatement +phrase: until +insertionScope: statement +--- + +language: ruby +- +until $1 + $0 +end +--- diff --git a/community/core/snippets/snippets/whileLoopStatement.snippet b/community/core/snippets/snippets/whileLoopStatement.snippet new file mode 100644 index 0000000..b66370d --- /dev/null +++ b/community/core/snippets/snippets/whileLoopStatement.snippet @@ -0,0 +1,45 @@ +name: whileLoopStatement +phrase: while +insertionScope: statement +--- + +language: c | cpp | csharp | java | javascript | typescript | javascriptreact | typescriptreact | php | scala | kotlin | r +- +while ($1) { + $0 +} +--- + +language: python +- +while $1: + $0 +--- + +language: ruby +- +while $1 + $0 +end +--- + +language: go +- +for $1 { + $0 +} +--- + +language: rust | stata +- +while $1 { + $0 +} +--- + +language: elixir | lua +- +while $1 do + $0 +end +--- diff --git a/community/core/snippets/snippets/withStatement.snippet b/community/core/snippets/snippets/withStatement.snippet new file mode 100644 index 0000000..27aa50e --- /dev/null +++ b/community/core/snippets/snippets/withStatement.snippet @@ -0,0 +1,13 @@ +name: withStatement +phrase: with +insertionScope: statement + +$0.wrapperPhrase: with +$0.wrapperScope: statement +--- + +language: python +- +with $1: + $0 +--- diff --git a/community/core/snippets/snippets/xml.snippet b/community/core/snippets/snippets/xml.snippet new file mode 100644 index 0000000..76c52f8 --- /dev/null +++ b/community/core/snippets/snippets/xml.snippet @@ -0,0 +1,23 @@ +language: xml | html | javascriptreact | typescriptreact +--- + +name: element +phrase: element | tag +insertionScope: xmlElement + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +$0.wrapperPhrase: element | tag +$0.wrapperScope: xmlElement +- +<$1> + $0 + +--- + +name: attribute +phrase: attribute + +$1.insertionFormatter: PRIVATE_CAMEL_CASE +- +$1=$0 +--- diff --git a/community/core/snippets/snippets_insert.py b/community/core/snippets/snippets_insert.py new file mode 100644 index 0000000..f95e47f --- /dev/null +++ b/community/core/snippets/snippets_insert.py @@ -0,0 +1,56 @@ +import re + +from talon import Module, actions + +from .snippet_types import Snippet +from .snippets_insert_raw_text import go_to_next_stop_raw, insert_snippet_raw_text + +mod = Module() + + +@mod.action_class +class Actions: + def insert_snippet(body: str): + """Insert snippet""" + insert_snippet_raw_text(body) + + def move_cursor_to_next_snippet_stop(): + """Moves the cursor to the next snippet stop""" + go_to_next_stop_raw() + + def insert_snippet_by_name( + name: str, + substitutions: dict[str, str] = None, + ): + """Insert snippet """ + snippet: Snippet = actions.user.get_snippet(name) + body = snippet.body + + if substitutions: + for k, v in substitutions.items(): + reg = re.compile(rf"\${k}|\$\{{{k}\}}") + if not reg.search(body): + raise ValueError( + f"Can't substitute non existing variable '{k}' in snippet '{name}'" + ) + body = reg.sub(v, body) + + actions.user.insert_snippet(body) + + def insert_snippet_by_name_with_phrase(name: str, phrase: str): + """Insert snippet with phrase """ + snippet: Snippet = actions.user.get_snippet(name) + substitutions = {} + + for variable in snippet.variables: + if variable.insertion_formatters is not None: + formatters = ",".join(variable.insertion_formatters) + formatted_phrase = actions.user.formatted_text(phrase, formatters) + substitutions[variable.name] = formatted_phrase + + if not substitutions: + raise ValueError( + f"Can't use snippet phrase. No variable with insertion formatter in snippet '{name}'" + ) + + actions.user.insert_snippet_by_name(name, substitutions) diff --git a/community/core/snippets/snippets_insert_raw_text.py b/community/core/snippets/snippets_insert_raw_text.py new file mode 100644 index 0000000..721f5cc --- /dev/null +++ b/community/core/snippets/snippets_insert_raw_text.py @@ -0,0 +1,219 @@ +import re +from collections import defaultdict +from dataclasses import dataclass + +from talon import Module, actions, app, settings + +mod = Module() + + +mod.setting( + "snippet_raw_text_spaces_per_tab", + type=int, + default=4, + desc="""The number of spaces per tab when inserting snippets as raw text. Set to -1 to insert tabs as tabs, such as in code editors that can expand tabs in pasted or typed text. This setting is provided for applications like web browsers and chat apps.""", +) + +mod.setting( + "snippet_raw_text_paste", + type=bool, + default=False, + desc="""If true, inserting snippets as raw text will always be done through pasting""", +) + +RE_STOP = re.compile(r"\$(\d+|\w+)|\$\{(\d+|\w+)\}|\$\{(\d+|\w+):(.+)\}") +LAST_SNIPPET_HOLE_KEY_VALUE = 1000 + + +@dataclass +class Stop: + name: str + rows_up: int + columns_left: int + row: int + col: int + + def compute_sorting_key(self) -> int: + """Returns a key value used to sort stops""" + if self.name == "0": + return LAST_SNIPPET_HOLE_KEY_VALUE + if self.name.isdigit(): + return int(self.name) + return 999 + + +stop_stack: list[Stop] = [] + + +def go_to_next_stop_raw(): + """Goes to the next snippet stop if it exists""" + global stop_stack + if len(stop_stack) > 1: + current_stop = stop_stack.pop() + next_stop = stop_stack[-1] + move_to_correct_row(current_stop, next_stop) + move_to_correct_column(next_stop) + else: + stop_stack = [] + + +def insert_snippet_raw_text(body: str): + """Insert snippet as raw text without editor support""" + updated_snippet, stops = parse_snippet(body) + sorted_stops = compute_stops_sorted_always_moving_left_to_right(stops) + stop = get_first_stop(sorted_stops) + + update_stop_information(sorted_stops) + + if settings.get("user.snippet_raw_text_paste"): + actions.user.paste(updated_snippet) + else: + actions.insert(updated_snippet) + + if stop: + up(stop.rows_up) + move_to_correct_column(stop) + + +def update_stop_information(stops: list[Stop]): + global stop_stack + if len(stops) > 1: + stop_stack = stops[:] + stop_stack.reverse() + else: + stop_stack = [] + + +def compute_stops_sorted_always_moving_left_to_right(stops: list[Stop]) -> list[Stop]: + """Without editor support, moving from right to left is problematic. Each line of stops is sorted by the smallest snippet hole key in the line. Each line gets sorted from left to right.""" + # Separate the stops by line keeping track of the smallest key in each line + lines = defaultdict(list) + smallest_keys = defaultdict(lambda: LAST_SNIPPET_HOLE_KEY_VALUE) + for stop in stops: + lines[stop.row].append(stop) + line_key = smallest_keys[stop.row] + smallest_keys[stop.row] = min(line_key, stop.compute_sorting_key()) + + # If a line was from right to left, notify user and sort + if is_any_line_from_right_to_left(lines.values()): + app.notify( + "The snippet you inserted got adjusted to move from left to right because editor support is unavailable." + ) + sorted_stops: list[Stop] = [] + # Sort lines by key + sorted_lines = sorted( + lines.values(), key=lambda line: smallest_keys[line[0].row] + ) + # Add every line sorted from left to right + for line in sorted_lines: + sorted_line = sorted(line, key=lambda stop: stop.col) + sorted_stops.extend(sorted_line) + return sorted_stops + return sorted(stops, key=lambda stop: stop.compute_sorting_key()) + + +def is_any_line_from_right_to_left(lines) -> bool: + for line in lines: + # Lines with only one stop are always in order + if len(line) > 1: + stop = line[0] + stop_key = stop.compute_sorting_key() + for next_stop in line[1:]: + next_key = next_stop.compute_sorting_key() + # If the ordering between the keys and columns are inconsistent, + # the stops on this line go from right to left + if (next_key < stop_key) != (next_stop.col < stop.col): + return True + stop_key = next_key + stop = next_stop + return False + + +def move_to_correct_column(stop: Stop): + actions.edit.line_end() + move_cursor_left(stop.columns_left) + + +def move_to_correct_row(current_stop: Stop, next_stop: Stop): + start = current_stop.row + end = next_stop.row + if start < end: + for _ in range(end - start): + actions.edit.down() + elif start > end: + for _ in range(start - end): + actions.edit.up() + + +def format_tabs(text: str) -> str: + """Possibly replaces tabs with spaces in the given text.""" + spaces_per_tab: int = settings.get("user.snippet_raw_text_spaces_per_tab") + if spaces_per_tab < 0: + return text + return re.sub(r"\t", " " * spaces_per_tab, text) + + +def parse_snippet(body: str): + # Some IM services will send the message on a tab + body = format_tabs(body) + + # Replace variable with appropriate value/text + body = re.sub(r"\$TM_SELECTED_TEXT", lambda _: actions.edit.selected_text(), body) + body = re.sub(r"\$CLIPBOARD", lambda _: actions.clip.text(), body) + + lines = body.splitlines() + stops: list[Stop] = [] + + for i, line in enumerate(lines): + match = RE_STOP.search(line) + + while match: + stops.append( + Stop( + name=match.group(1) or match.group(2) or match.group(3), + rows_up=len(lines) - i - 1, + columns_left=0, + row=i, + col=match.start(), + ) + ) + + # Remove tab stops and variables. + stop_text = match.group(0) + default_value = match.group(4) or "" + line = line.replace(stop_text, default_value, 1) + + # Might have multiple stops on the same line + match = RE_STOP.search(line) + + # Update existing line + lines[i] = line + + # Can't calculate column left until line text is fully updated + for stop in stops: + stop.columns_left = len(lines[stop.row]) - stop.col + + updated_snippet = "\n".join(lines) + + return updated_snippet, stops + + +def up(n: int): + """Move cursor up rows""" + for _ in range(n): + actions.edit.up() + + +def move_cursor_left(n: int): + """Move cursor left columns""" + for _ in range(n): + actions.edit.left() + + +def get_first_stop(stops: list[Stop]): + if not stops: + return None + stop = stops[0] + if stop.rows_up == 0 and stop.columns_left == 0: + return None + return stop diff --git a/community/core/snippets/snippets_parser.py b/community/core/snippets/snippets_parser.py new file mode 100644 index 0000000..863a667 --- /dev/null +++ b/community/core/snippets/snippets_parser.py @@ -0,0 +1,451 @@ +import logging +import re +from copy import deepcopy +from pathlib import Path +from typing import Callable, Union + +from .snippet_types import Snippet, SnippetVariable + + +class SnippetDocument: + file: str + line_doc: int + 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 + body: str | None = None + + def __init__(self, file: str, line_doc: int, line_body: int): + self.file = file + self.line_doc = line_doc + self.line_body = line_body + + +def create_snippets_from_file(file: Path) -> list[Snippet]: + documents = parse_file(file) + return create_snippets(documents) + + +def create_snippets(documents: list[SnippetDocument]) -> list[Snippet]: + if len(documents) == 0: + return [] + + if documents[0].body is None: + default_context = documents[0] + documents = documents[1:] + else: + default_context = SnippetDocument("", -1, -1) + + snippets: list[Snippet] = [] + + for doc in documents: + snippet = create_snippet(doc, default_context) + if snippet: + snippets.append(snippet) + + return snippets + + +def create_snippet( + document: SnippetDocument, + default_context: SnippetDocument, +) -> Snippet | None: + body = normalize_snippet_body_tabs(document.body) + variables = combine_variables(default_context.variables, document.variables) + body, variables = add_final_stop_to_snippet_body(body, variables) + + snippet = Snippet( + 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, + variables=variables, + body=body, + ) + + if not validate_snippet(document, snippet): + return None + + return snippet + + +def validate_snippet(document: SnippetDocument, snippet: Snippet) -> bool: + is_valid = True + + if not snippet.name: + 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 not is_variable_in_body(variable.name, snippet.body): + error( + document.file, + document.line_body, + f"Variable '{var_name}' missing in body '{snippet.body}'", + ) + is_valid = False + + if variable.insertion_formatters is not None and snippet.phrases is None: + error( + document.file, + document.line_doc, + f"Snippet phrase required when using '{var_name}.insertionFormatter'", + ) + is_valid = False + + if variable.wrapper_scope is not None and variable.wrapper_phrases is None: + error( + document.file, + document.line_doc, + f"'{var_name}.wrapperPhrase' required when using '{var_name}.wrapperScope'", + ) + is_valid = False + + return is_valid + + +def is_variable_in_body(variable_name: str, body: str) -> bool: + return ( + re.search(create_variable_regular_expression(variable_name), body) is not None + ) + + +def create_variable_regular_expression(variable_name: str) -> str: + # $value or ${value} or ${value:default} + # *? is used to find the smallest possible match. + # This stops multiple stops from being treated as a single stop. + return rf"\${variable_name}|\${{{variable_name}.*?}}" + + +def combine_variables( + default_variables: list[SnippetVariable], + document_variables: list[SnippetVariable], +) -> list[SnippetVariable]: + variables: dict[str, SnippetVariable] = {} + + for variable in [*default_variables, *document_variables]: + if variable.name not in variables: + variables[variable.name] = SnippetVariable(variable.name) + + new_variable = variables[variable.name] + + if variable.insertion_formatters is not None: + new_variable.insertion_formatters = variable.insertion_formatters + + if variable.wrapper_phrases is not None: + new_variable.wrapper_phrases = variable.wrapper_phrases + + if variable.wrapper_scope is not None: + new_variable.wrapper_scope = variable.wrapper_scope + + return list(variables.values()) + + +def add_final_stop_to_snippet_body( + body: str, variables: list[SnippetVariable] +) -> tuple[str, list[SnippetVariable]]: + """Make the snippet body end with stop $0 to allow exiting the snippet with `snip next`. + If the snippet has a stop named `0`, it will get replaced with the largest number of a snippet variable name + plus 1 with the original variable metadata for stop `0` now associated with the replacement. + """ + if body: + final_stop_matches = find_variable_matches("0", body) + + # Only make a change if the snippet body does not end with a final stop. + if not ( + len(final_stop_matches) > 0 and final_stop_matches[-1].end() == len(body) + ): + biggest_variable_number: int | None = find_largest_variable_number(body) + if biggest_variable_number is not None: + replacement_name = str(biggest_variable_number + 1) + body = replace_final_stop(body, replacement_name, final_stop_matches) + variables = replace_variables_for_final_stop( + variables, replacement_name + ) + body += "$0" + + return body, variables + + +def replace_final_stop(body: str, replacement_name: str, final_stop_matches) -> str: + # Dealing with matches in reverse means replacing a match + # does not change the location of the remaining matches. + for match in reversed(final_stop_matches): + replacement = match.group().replace("0", replacement_name, 1) + body = body[: match.start()] + replacement + body[match.end() :] + return body + + +def replace_variables_for_final_stop(variables, replacement_name: str): + variables_clone = deepcopy(variables) + for variable in variables_clone: + if variable.name == "0": + variable.name = replacement_name + return variables_clone + + +def find_variable_matches(variable_name: str, body: str) -> list[re.Match[str]]: + """Find every match of a variable in the body""" + expression = create_variable_regular_expression(variable_name) + matches = [m for m in re.finditer(expression, body)] + return matches + + +def find_largest_variable_number(body: str) -> int | None: + # Find all snippet stops with a numeric variable name + # +? is used to find the smallest possible match. + # We need this here to avoid treating multiple stops as a single one + regular_expression = rf"\$\d+?|\${{\d+?:.*?}}|\${{\d+?}}" + matches = re.findall(regular_expression, body) + if matches: + numbers = [ + compute_first_integer_in_string(match) + for match in matches + if match is not None + ] + if numbers: + return max(numbers) + return None + + +def compute_first_integer_in_string(text: str) -> int | None: + start_index: int | None = None + ending_index: int | None = None + for i, char in enumerate(text): + if char.isdigit(): + if start_index is None: + start_index = i + ending_index = i + 1 + elif start_index is not None: + break + if start_index is not None: + integer_text = text[start_index:ending_index] + return int(integer_text) + return None + + +def normalize_snippet_body_tabs(body: str | None) -> str: + if not body: + return "" + + # If snippet body already contains tabs. No change. + if "\t" in body: + return body + + lines = [] + smallest_indentation = None + + for line in body.splitlines(): + match = re.search(r"^\s+", line) + indentation = match.group() if match is not None else "" + + # Keep track of smallest non-empty indentation + if len(indentation) > 0 and ( + smallest_indentation is None or len(indentation) < len(smallest_indentation) + ): + smallest_indentation = indentation + + lines.append({"indentation": indentation, "rest": line[len(indentation) :]}) + + # No indentation found in snippet body. No change. + if smallest_indentation is None: + return body + + normalized_lines = [ + reconstruct_line(smallest_indentation, line["indentation"], line["rest"]) + for line in lines + ] + + return "\n".join(normalized_lines) + + +def reconstruct_line(smallest_indentation: str, indentation: str, rest: str) -> str: + # Update indentation by replacing each occurrent of smallest space indentation with a tab + indentation = indentation.replace(smallest_indentation, "\t") + return f"{indentation}{rest}" + + +# ---------- Snippet file parser ---------- + + +def parse_file(file: Path) -> list[SnippetDocument]: + with open(file, encoding="utf-8") as f: + content = f.read() + return parse_file_content(file.name, content) + + +def parse_file_content(file: str, text: str) -> list[SnippetDocument]: + doc_texts = re.split(r"^---\n?$", text, flags=re.MULTILINE) + documents: list[SnippetDocument] = [] + line = 0 + + for i, doc_text in enumerate(doc_texts): + optional_body = i == 0 and len(doc_texts) > 1 + document = parse_document(file, line, optional_body, doc_text) + if document is not None: + documents.append(document) + line += doc_text.count("\n") + 1 + + return documents + + +def parse_document( + 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 + org_doc = SnippetDocument(file, line, line_body) + document = parse_context(file, line, org_doc, parts[0]) + + if len(parts) == 2: + body = parse_body(parts[1]) + if body is not None: + if document is None: + document = org_doc + document.body = body + + if document and not document.body and not optional_body: + error(file, line, f"Missing body in snippet document '{text}'") + return None + + return document + + +def parse_context( + file: str, + line: int, + document: SnippetDocument, + text: str, +) -> Union[SnippetDocument, None]: + lines = [l.strip() for l in text.splitlines()] + keys: set[str] = set() + variables: dict[str, SnippetVariable] = {} + + def get_variable(name: str) -> SnippetVariable: + if name not in variables: + variables[name] = SnippetVariable(name) + return variables[name] + + for i, line_text in enumerate(lines): + if line_text: + parse_context_line( + file, + line + i, + document, + keys, + get_variable, + line_text, + ) + + if len(keys) == 0: + return None + + document.variables = list(variables.values()) + + return document + + +def parse_context_line( + file: str, + line: int, + document: SnippetDocument, + keys: set[str], + get_variable: Callable[[str], SnippetVariable], + text: str, +): + parts = text.split(":") + + if len(parts) != 2: + error(file, line, f"Invalid line '{text}'") + return + + key = parts[0].strip() + value = parts[1].strip() + + if not key or not value: + error(file, line, f"Invalid line '{text}'") + return + + if key in keys: + warn(file, line, f"Duplicate key '{key}'") + + keys.add(key) + + match key: + case "name": + document.name = value + case "description": + document.description = value + case "phrase": + document.phrases = parse_vector_value(value) + case "insertionScope": + document.insertionScopes = parse_vector_value(value) + case "language": + document.languages = parse_vector_value(value) + case _: + if key.startswith("$"): + parse_variable(file, line, get_variable, key, value) + else: + warn(file, line, f"Unknown key '{key}'") + + +def parse_variable( + file: str, + line_numb: int, + get_variable: Callable[[str], SnippetVariable], + key: str, + value: str, +): + parts = key.split(".") + + if len(parts) != 2: + error(file, line_numb, f"Invalid variable key '{key}'") + return + + name = parts[0][1:] + field = parts[1] + + match field: + case "insertionFormatter": + get_variable(name).insertion_formatters = parse_vector_value(value) + case "wrapperPhrase": + get_variable(name).wrapper_phrases = parse_vector_value(value) + case "wrapperScope": + get_variable(name).wrapper_scope = value + case _: + warn(file, line_numb, f"Unknown variable key '{key}'") + + +def parse_body(text: str) -> Union[str, None]: + # Find first line that is not empty. Preserve indentation. + match_leading = re.search(r"^[ \t]*\S", text, flags=re.MULTILINE) + + if match_leading is None: + return None + + return text[match_leading.start() :].rstrip() + + +def parse_vector_value(value: str) -> list[str]: + return [v.strip() for v in value.split("|")] + + +def error(file: str, line: int, message: str): + logging.error(f"{file}:{line+1} | {message}") + + +def warn(file: str, line: int, message: str): + logging.warning(f"{file}:{line+1} | {message}") diff --git a/community/core/system_command.py b/community/core/system_command.py new file mode 100644 index 0000000..cb80098 --- /dev/null +++ b/community/core/system_command.py @@ -0,0 +1,17 @@ +import os +import subprocess + +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def system_command(cmd: str): + """execute a command on the system""" + os.system(cmd) + + def system_command_nb(cmd: str): + """execute a command on the system without blocking""" + subprocess.Popen(cmd, shell=True) diff --git a/community/core/system_paths-DESKTOP-3LCTGIL.talon-list b/community/core/system_paths-DESKTOP-3LCTGIL.talon-list new file mode 100644 index 0000000..c64580d --- /dev/null +++ b/community/core/system_paths-DESKTOP-3LCTGIL.talon-list @@ -0,0 +1,16 @@ +list: user.system_paths +hostname: DESKTOP-3LCTGIL +- +user: C:\Users\ajet +desktop: C:\Users\ajet\Desktop +desk: C:\Users\ajet\Desktop +documents: C:\Users\ajet\Documents +docks: C:\Users\ajet\Documents +downloads: C:\Users\ajet\Downloads +music: C:\Users\ajet\Music +pictures: C:\Users\ajet\Pictures +videos: C:\Users\ajet\Videos +talon home: C:\Users\ajet\AppData\Roaming\talon +talon recordings: C:\Users\ajet\AppData\Roaming\talon\recordings +talon user: C:\Users\ajet\AppData\Roaming\talon\user +profile: C:\Users\ajet diff --git a/community/core/system_paths.py b/community/core/system_paths.py new file mode 100644 index 0000000..bdcb15c --- /dev/null +++ b/community/core/system_paths.py @@ -0,0 +1,74 @@ +""" +This module gives us the list {user.system_paths} and the capture that wraps +the list to easily refer to system paths in talon and python files. It also creates a file +system_paths-.talon-list in the core folder so the user can easily add their own +custom paths. +""" + +from pathlib import Path + +from talon import Module, actions, app + +mod = Module() +mod.list("system_paths", desc="List of system paths") + + +def on_ready(): + # If user.system_paths defined otherwise, don't generate a file + if actions.user.talon_get_active_registry_list("user.system_paths"): + return + + hostname = actions.user.talon_get_hostname() + system_paths = Path(__file__).with_name(f"system_paths-{hostname}.talon-list") + if system_paths.is_file(): + return + + home = Path.home() + talon_home = Path(actions.path.talon_home()) + + default_system_paths = { + "user": home, + "desktop": home / "Desktop", + "desk": home / "Desktop", + "documents": home / "Documents", + "docks": home / "Documents", + "downloads": home / "Downloads", + "music": home / "Music", + "pictures": home / "Pictures", + "videos": home / "Videos", + "talon home": talon_home, + "talon recordings": talon_home / "recordings", + "talon user": actions.path.talon_user(), + } + + if app.platform == "windows": + default_system_paths["profile"] = home + onedrive_path = home / "OneDrive" + + # this is probably not the correct way to check for OneDrive, quick and dirty + if (onedrive_path / "Desktop").is_dir(): + default_system_paths["desktop"] = onedrive_path / "Desktop" + default_system_paths["documents"] = onedrive_path / "Documents" + default_system_paths["one drive"] = onedrive_path + default_system_paths["pictures"] = onedrive_path / "Pictures" + else: + default_system_paths["home"] = home + + with open(system_paths, "x") as f: + print("list: user.system_paths", file=f) + print(f"hostname: {hostname}", file=f) + print("-", file=f) + for spoken_form, path in default_system_paths.items(): + path = str(path) + if not str.isprintable(path) or "'" in path or '"' in path: + path = repr(path) + + print(f"{spoken_form}: {path}", file=f) + + +@mod.capture(rule="{user.system_paths}") +def system_path(m) -> str: + return m.system_paths + + +app.register("ready", on_ready) diff --git a/community/core/tags.py b/community/core/tags.py new file mode 100644 index 0000000..a9d2f76 --- /dev/null +++ b/community/core/tags.py @@ -0,0 +1,17 @@ +from talon import Module + +mod = Module() + +tagList = [ + "disassembler", + "git", # commandline tag for git commands + "ida", + "tabs", + "generic_windows_shell", + "generic_unix_shell", + "readline", + "taskwarrior", # commandline tag for taskwarrior commands + "tmux", +] +for entry in tagList: + mod.tag(entry, f"tag to load {entry} and/or related plugins ") diff --git a/community/core/text/currency.talon-list b/community/core/text/currency.talon-list new file mode 100644 index 0000000..6c9f6f2 --- /dev/null +++ b/community/core/text/currency.talon-list @@ -0,0 +1,8 @@ +list: user.currency +- +dollar: $ +dollars: $ +euro: € +euros: € +pound: £ +pounds: £ diff --git a/community/core/text/phrase_ender.talon-list b/community/core/text/phrase_ender.talon-list new file mode 100644 index 0000000..b1a2ac9 --- /dev/null +++ b/community/core/text/phrase_ender.talon-list @@ -0,0 +1,4 @@ +list: user.phrase_ender +- + +over: "" diff --git a/community/core/text/phrase_history.py b/community/core/text/phrase_history.py new file mode 100644 index 0000000..062bf3b --- /dev/null +++ b/community/core/text/phrase_history.py @@ -0,0 +1,85 @@ +import logging + +from talon import Module, actions, imgui + +mod = Module() + +# list of recent phrases, most recent first +phrase_history = [] +phrase_history_length = 40 +phrase_history_display_length = 40 + + +@mod.action_class +class Actions: + def get_last_phrase() -> str: + """Gets the last phrase""" + return phrase_history[0] if phrase_history else "" + + def get_recent_phrase(number: int) -> str: + """Gets the nth most recent phrase""" + try: + return phrase_history[number - 1] + except IndexError: + return "" + + def clear_last_phrase(): + """Clears the last phrase""" + # Currently, this removes the cleared phrase from the phrase history, so + # that repeated calls clear successively earlier phrases, which is often + # useful. But it would be nice if we could do this without removing + # those phrases from the history entirely, so that they were still + # accessible for copying, for example. + if not phrase_history: + logging.warning("clear_last_phrase(): No last phrase to clear!") + return + for _ in phrase_history.pop(0): + actions.key("backspace") + + def select_last_phrase(): + """Selects the last phrase""" + if not phrase_history: + logging.warning("select_last_phrase(): No last phrase to select!") + return + for _ in phrase_history[0]: + actions.edit.extend_left() + + def before_last_phrase(): + """Moves left before the last phrase""" + try: + for _ in phrase_history.pop(0): + actions.edit.left() + except IndexError: + logging.warning("before_last_phrase(): No last phrase to move before!") + + def add_phrase_to_history(text: str): + """Adds a phrase to the phrase history""" + global phrase_history + phrase_history.insert(0, text) + phrase_history = phrase_history[:phrase_history_length] + + def toggle_phrase_history(): + """Toggles list of recent phrases""" + if gui.showing: + gui.hide() + else: + gui.show() + + def phrase_history_hide(): + """Hides the recent phrases window""" + + gui.hide() + + +@imgui.open() +def gui(gui: imgui.GUI): + gui.text("Recent phrases") + gui.text("Say 'recent repeat ' retype a phrase on this list.") + gui.text("Say 'recent copy ' to copy a phrase from this list.") + gui.line() + for index, text in enumerate(phrase_history[:phrase_history_display_length], 1): + gui.text(f"{index}: {text}") + + gui.spacer() + if gui.button("Recent close"): + actions.user.phrase_history_hide() diff --git a/community/core/text/prose_modifiers.talon-list b/community/core/text/prose_modifiers.talon-list new file mode 100644 index 0000000..256412d --- /dev/null +++ b/community/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/community/core/text/prose_snippets.talon-list b/community/core/text/prose_snippets.talon-list new file mode 100644 index 0000000..7a4113b --- /dev/null +++ b/community/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/community/core/text/text.talon b/community/core/text/text.talon new file mode 100644 index 0000000..aa21433 --- /dev/null +++ b/community/core/text/text.talon @@ -0,0 +1,32 @@ +#provide both anchored and unachored commands via 'over' +phrase $: + user.add_phrase_to_history(text) + insert(text) +phrase {user.phrase_ender}: + user.add_phrase_to_history(text) + insert("{text}{phrase_ender}") +{user.prose_formatter} $: user.insert_formatted(prose, prose_formatter) +{user.prose_formatter} {user.phrase_ender}: + user.insert_formatted(prose, prose_formatter) + insert(phrase_ender) ++$: user.insert_many(format_code_list) ++ {user.phrase_ender}: + user.insert_many(format_code_list) + insert(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) +recent list: user.toggle_phrase_history() +recent close: user.phrase_history_hide() +recent repeat : + recent_phrase = user.get_recent_phrase(number_small) + user.add_phrase_to_history(recent_phrase) + insert(recent_phrase) +recent copy : clip.set_text(user.get_recent_phrase(number_small)) +select that: user.select_last_phrase() +before that: user.before_last_phrase() +nope that | scratch that: user.clear_last_phrase() +nope that was : user.formatters_reformat_last(formatters) +(abbreviate | abreviate | brief) {user.abbreviation}: "{abbreviation}" + (abbreviate | abreviate | brief) {user.abbreviation}: + user.insert_formatted(abbreviation, formatters) diff --git a/community/core/text/text_and_dictation.py b/community/core/text/text_and_dictation.py new file mode 100644 index 0000000..c99d4b2 --- /dev/null +++ b/community/core/text/text_and_dictation.py @@ -0,0 +1,558 @@ +# Descended from https://github.com/dwiel/talon_community/blob/master/misc/dictation.py +import re +from typing import Callable, Optional + +from talon import Context, Module, actions, grammar, settings, ui + +from ..numbers.numbers import get_spoken_form_under_one_hundred + +mod = Module() + +mod.setting( + "context_sensitive_dictation", + type=bool, + default=False, + desc="Look at surrounding text to improve auto-capitalization/spacing in dictation mode. By default, this works by selecting that text & copying it to the clipboard, so it may be slow or fail in some applications.", +) + +mod.setting( + "context_sensitive_dictation_peek_character", + type=str, + default=" ", + desc="This is the character inserted during dictation_peek to ensure that some text is selected even if the cursor is at the start or end of the document. This should be a single character only.", +) + +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() +ctx_dragon = Context() +ctx_dragon.matches = r""" +speech.engine: dragon +""" + +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: + return getattr(DictationFormat, m.prose_modifiers) + + +@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.""" + if hasattr(m, "vocabulary"): + return m.vocabulary + elif hasattr(m, "abbreviation"): + return m.abbreviation + else: + return " ".join( + actions.dictate.replace_words(actions.dictate.parse_words(m.word)) + ) + + +@mod.capture(rule="({user.vocabulary} | | )+") +def text(m) -> str: + """A sequence of words, including user-defined vocabulary.""" + return format_phrase(m) + + +@mod.capture( + rule=( + "(" + "{user.vocabulary}" + "| {user.punctuation}" + "| {user.prose_snippets}" + "| " + "| " + "| " + "| " + "| " + "| " + "| " + "| " + ")+" + ) +) +def prose(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("”", '"') + + +@mod.capture( + 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 and contacts +@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) + result = "" + for i, word in enumerate(words): + if i > 0 and needs_space_between(words[i - 1], word): + result += " " + result += word + return result + + +def capture_to_words(m): + words = [] + for item in m: + words.extend( + actions.dictate.replace_words(actions.dictate.parse_words(item)) + if isinstance(item, grammar.vm.Phrase) + else [item] + ) + return words + + +def apply_formatting(m): + formatter = DictationFormat() + formatter.state = None + result = "" + for item in m: + # prose modifiers (cap/no cap/no space) produce formatter callbacks. + if isinstance(item, Callable): + item(formatter) + else: + words = ( + actions.dictate.replace_words(actions.dictate.parse_words(item)) + if isinstance(item, grammar.vm.Phrase) + else [item] + ) + for word in words: + result += formatter.format(word) + return result + + +# There must be a simpler way to do this, but I don't see it right now. +no_space_after = re.compile( + r""" + (?: + [\s\-_/#@([{‘“] # characters that never need space after them + | (? bool: + return not text or no_space_before.search(text) + + +def omit_space_after(text: str) -> bool: + return not text or no_space_after.search(text) + + +def needs_space_between(before: str, after: str) -> bool: + return not (omit_space_after(before) or omit_space_before(after)) + + +# # TESTS, uncomment to enable +# assert needs_space_between("a", "break") +# assert needs_space_between("break", "a") +# assert needs_space_between(".", "a") +# assert needs_space_between("said", "'hello") +# assert needs_space_between("hello'", "said") +# assert needs_space_between("hello.", "'John") +# assert needs_space_between("John.'", "They") +# assert needs_space_between("paid", "$50") +# assert needs_space_between("50$", "payment") +# assert not needs_space_between("", "") +# assert not needs_space_between("a", "") +# assert not needs_space_between("a", " ") +# assert not needs_space_between("", "a") +# assert not needs_space_between(" ", "a") +# assert not needs_space_between("a", ",") +# assert not needs_space_between("'", "a") +# assert not needs_space_between("a", "'") +# assert not needs_space_between("and-", "or") +# assert not needs_space_between("mary", "-kate") +# assert not needs_space_between("$", "50") +# assert not needs_space_between("US", "$") +# assert not needs_space_between("(", ")") +# assert not needs_space_between("(", "e.g.") +# assert not needs_space_between("example", ")") +# assert not needs_space_between("example", '".') +# assert not needs_space_between("example", '."') +# assert not needs_space_between("hello'", ".") +# assert not needs_space_between("hello.", "'") + +no_cap_after = re.compile( + r"""( + e\.g\. + | i\.e\. + )$""", + re.VERBOSE, +) + + +def auto_capitalize(text, state=None): + """ + Auto-capitalizes text. Text must contain complete words, abbreviations, and + formatted expressions. `state` argument means: + + - None: Don't capitalize initial word. + - "sentence start": Capitalize initial word. + - "after newline": Don't capitalize initial word, but we're after a newline. + Used for double-newline detection. + + Returns (capitalized text, updated state). + """ + output = "" + # Imagine a metaphorical "capitalization charge" travelling through the + # string left-to-right. + charge = state == "sentence start" + newline = state == "after newline" + sentence_end = False + for c in text: + # Sentence endings followed by space & double newlines create a charge. + if (sentence_end and c in " \n\t") or (newline and c == "\n"): + charge = True + # Alphanumeric characters and commas/colons absorb charge & try to + # capitalize (for numbers & punctuation this does nothing, which is what + # we want). + elif charge and (c.isalnum() or c in ",:"): + charge = False + c = c.capitalize() + # Otherwise the charge just passes through. + output += c + newline = c == "\n" + sentence_end = c in ".!?" and not no_cap_after.search(output) + return output, ( + "sentence start" + if charge or sentence_end + else "after newline" if newline else None + ) + + +# ---------- DICTATION AUTO FORMATTING ---------- # +class DictationFormat: + def __init__(self): + self.reset() + + def reset(self): + self.reset_context() + self.force_no_space = False + self.force_capitalization = None # Can also be "cap" or "no cap". + + def reset_context(self): + self.before = "" + self.state = "sentence start" + + def update_context(self, before): + if before is None: + return + self.reset_context() + self.pass_through(before) + + def pass_through(self, text): + _, self.state = auto_capitalize(text, self.state) + self.before = text or self.before + + def format(self, text, auto_cap=True): + if not self.force_no_space and needs_space_between(self.before, text): + text = " " + text + self.force_no_space = False + if auto_cap: + text, self.state = auto_capitalize(text, self.state) + if self.force_capitalization == "cap": + text = format_first_letter(text, lambda s: s.capitalize()) + self.force_capitalization = None + if self.force_capitalization == "no cap": + text = format_first_letter(text, lambda s: s.lower()) + self.force_capitalization = None + self.before = text or self.before + return text + + # These are used as callbacks by prose modifiers / dictation_mode commands. + def cap(self): + self.force_capitalization = "cap" + + def no_cap(self): + self.force_capitalization = "no cap" + + def no_space(self): + # This is typically used after repositioning the cursor, so it is helpful to + # reset capitalization as well. + # + # FIXME: this sets state to "sentence start", capitalizing the next + # word. probably undesirable, since most places are not the start of + # sentences? + self.reset_context() + self.force_no_space = True + + +def format_first_letter(text, formatter): + i = -1 + for i, c in enumerate(text): + if c.isalpha(): + break + if i >= 0 and i < len(text): + text = text[:i] + formatter(text[i]) + text[i + 1 :] + return text + + +dictation_formatter = DictationFormat() +ui.register("app_deactivate", lambda app: dictation_formatter.reset()) +ui.register("win_focus", lambda win: dictation_formatter.reset()) + + +def reformat_last_utterance(formatter): + text = actions.user.get_last_phrase() + actions.user.clear_last_phrase() + text = formatter(text) + actions.user.add_phrase_to_history(text) + actions.insert(text) + + +@mod.action_class +class Actions: + def dictation_format_reset(): + """Resets the dictation formatter""" + return dictation_formatter.reset() + + def dictation_format_cap(): + """Sets the dictation formatter to capitalize""" + dictation_formatter.cap() + + def dictation_format_no_cap(): + """Sets the dictation formatter to not capitalize""" + dictation_formatter.no_cap() + + def dictation_format_no_space(): + """Sets the dictation formatter to not prepend a space""" + dictation_formatter.no_space() + + def dictation_reformat_cap(): + """Capitalizes the last utterance""" + reformat_last_utterance( + lambda s: format_first_letter(s, lambda c: c.capitalize()) + ) + + def dictation_reformat_no_cap(): + """Lowercases the last utterance""" + reformat_last_utterance(lambda s: format_first_letter(s, lambda c: c.lower())) + + def dictation_reformat_no_space(): + """Removes space before the last utterance""" + reformat_last_utterance(lambda s: s[1:] if s.startswith(" ") else s) + + def dictation_insert_raw(text: str): + """Inserts text as-is, without invoking the dictation formatter.""" + actions.user.dictation_insert(text, auto_cap=False) + + def dictation_insert(text: str, auto_cap: bool = True) -> str: + """Inserts dictated text, formatted appropriately.""" + add_space_after = False + if settings.get("user.context_sensitive_dictation"): + # Peek left if we might need leading space or auto-capitalization; + # peek right if we might need trailing space. NB. We peek right + # BEFORE insertion to avoid breaking the undo-chain between the + # inserted text and the trailing space. + need_left = not omit_space_before(text) or ( + auto_cap and text != auto_capitalize(text, "sentence start")[0] + ) + need_right = not omit_space_after(text) + before, after = actions.user.dictation_peek(need_left, need_right) + dictation_formatter.update_context(before) + add_space_after = after is not None and needs_space_between(text, after) + text = dictation_formatter.format(text, auto_cap) + # Straighten curly quotes that were introduced to obtain proper + # spacing. The formatter context still has the original curly quotes + # so that future dictation is properly formatted. + text = text.replace("“", '"').replace("”", '"') + actions.user.add_phrase_to_history(text) + actions.user.insert_between(text, " " if add_space_after else "") + + def dictation_peek(left: bool, right: bool) -> tuple[Optional[str], Optional[str]]: + """ + Gets text around the cursor to inform auto-spacing and -capitalization. + Returns (before, after), where `before` is some text before the cursor, + and `after` some text after it. Results are not guaranteed; `before` + and/or `after` may be None, indicating no information. If `before` is + the empty string, this means there is nothing before the cursor (we are + at the beginning of the document); likewise for `after`. + + To optimize performance, pass `left = False` if you won't need + `before`, and `right = False` if you won't need `after`. + + dictation_peek() is intended for use before inserting text, so it may + delete any currently selected text. + """ + if not (left or right): + return None, None + before, after = None, None + # Inserting a character ensures we select something even if we're at + # document start; some editors 'helpfully' copy the current line if we + # edit.copy() while nothing is selected. + actions.insert(settings.get("user.context_sensitive_dictation_peek_character")) + if left: + # In principle the previous word should suffice, but some applications + # have a funny concept of what the previous word is (for example, they + # may only take the "`" at the end of "`foo`"). To be double sure we + # take two words left. I also tried taking a line up + a word left, but + # edit.extend_up() = key(shift-up) doesn't work consistently in the + # Slack webapp (sometimes escapes the text box). + actions.edit.extend_word_left() + actions.edit.extend_word_left() + before = actions.edit.selected_text()[:-1] + # Unfortunately, in web Slack, if our selection ends at newline, + # this will go right over the newline. Argh. + actions.edit.right() + if not right: + actions.key("backspace") # remove the peek character + else: + actions.edit.left() # go left before the peek character + # We want to select at least two characters to the right, plus the character + # we inserted, because no_space_before needs two characters in the worst + # case -- for example, inserting before "' hello" we don't want to add + # space, while inserted before "'hello" we do. + # + # We use 2x extend_word_right() because it's fewer keypresses (lower + # latency) than 3x extend_right(). Other options all seem to have + # problems. For instance, extend_line_end() might not select all the way + # to the next newline if text has been wrapped across multiple lines; + # extend_line_down() sometimes escapes the current text box (eg. in a + # browser address bar). 1x extend_word_right() _usually_ works, but on + # Windows in Firefox it doesn't always select enough characters. + actions.edit.extend_word_right() + actions.edit.extend_word_right() + after = actions.edit.selected_text()[1:] + actions.edit.left() + actions.user.delete_right() # remove peek character + return before, after diff --git a/community/core/user_settings.py b/community/core/user_settings.py new file mode 100644 index 0000000..48d1137 --- /dev/null +++ b/community/core/user_settings.py @@ -0,0 +1,132 @@ +import csv +from pathlib import Path +from typing import IO, Callable + +from talon import resource + +# NOTE: This method requires this module to be one folder below the top-level +# community folder. +SETTINGS_DIR = Path(__file__).parents[1] / "settings" +SETTINGS_DIR.mkdir(exist_ok=True) +PRIVATE_DIR = Path(__file__).parents[1] / "private" +PRIVATE_DIR.mkdir(exist_ok=True) + +CallbackT = Callable[[dict[str, str]], None] +DecoratorT = Callable[[CallbackT], CallbackT] + + +def read_csv_list( + f: IO, headers: tuple[str, str], is_spoken_form_first: bool = False +) -> dict[str, str]: + rows = list(csv.reader(f)) + + # print(str(rows)) + mapping = {} + if len(rows) >= 2: + actual_headers = rows[0] + if not actual_headers == list(headers): + print( + f'"{f.name}": Malformed headers - {actual_headers}.' + + f" Should be {list(headers)}. Ignoring row." + ) + for row in rows[1:]: + if len(row) == 0: + # Windows newlines are sometimes read as empty rows. :champagne: + continue + if len(row) == 1: + output = spoken_form = row[0] + else: + if is_spoken_form_first: + spoken_form, output = row[:2] + else: + output, spoken_form = row[:2] + + if len(row) > 2: + print( + f'"{f.name}": More than two values in row: {row}.' + + " Ignoring the extras." + ) + # Leading/trailing whitespace in spoken form can prevent recognition. + spoken_form = spoken_form.strip() + mapping[spoken_form] = output + + return mapping + + +def write_csv_defaults( + path: Path, + headers: tuple[str, str], + default: dict[str, str] = None, + is_spoken_form_first: bool = False, +) -> None: + if not path.is_file() and default is not None: + with open(path, "w", encoding="utf-8", newline="") as file: + writer = csv.writer(file) + writer.writerow(headers) + for key, value in default.items(): + if key == value: + writer.writerow([key]) + elif is_spoken_form_first: + writer.writerow([key, value]) + else: + writer.writerow([value, key]) + + +def track_csv_list( + filename: str, + headers: tuple[str, str], + default: dict[str, str] = None, + is_spoken_form_first: bool = False, + private: bool = False, +) -> DecoratorT: + assert filename.endswith(".csv") + path = (PRIVATE_DIR / filename) if private else (SETTINGS_DIR / filename) + write_csv_defaults(path, headers, default, is_spoken_form_first) + + def decorator(fn: CallbackT) -> CallbackT: + @resource.watch(str(path)) + def on_update(f): + data = read_csv_list(f, headers, is_spoken_form_first) + fn(data) + + return decorator + + +def append_to_csv(filename: str, rows: dict[str, str], private: bool = False): + path = (PRIVATE_DIR / filename) if private else (SETTINGS_DIR / filename) + assert filename.endswith(".csv") + + with open(str(path)) as file: + line = None + for line in file: + pass + needs_newline = line is not None and not line.endswith("\n") + with open(path, "a", encoding="utf-8", newline="") as file: + writer = csv.writer(file) + if needs_newline: + writer.writerow([]) + for key, value in rows.items(): + writer.writerow([key] if key == value else [value, key]) + + +WatchCallbackType = Callable[[IO], None] +WatchDecoratorType = Callable[[WatchCallbackType], WatchCallbackType] + + +def track_file( + filename: str, + default: str = "", + private: bool = False, +) -> WatchDecoratorType: + path = (PRIVATE_DIR / filename) if private else (SETTINGS_DIR / filename) + if not path.is_file(): + path.write_text(default) + + def decorator(fn: WatchCallbackType) -> WatchCallbackType: + @resource.watch(path) + def on_update(f): + fn(f) + + return on_update + + return decorator diff --git a/community/core/vocabulary/edit_vocabulary.talon b/community/core/vocabulary/edit_vocabulary.talon new file mode 100644 index 0000000..f85bae0 --- /dev/null +++ b/community/core/vocabulary/edit_vocabulary.talon @@ -0,0 +1,18 @@ +mode: command +mode: dictation +- +copy to vocab [as ]$: user.add_selection_to_vocabulary(phrase or "") +# Automatically adds possessive form by appending "'s". +copy name to vocab [as ]$: + user.add_selection_to_vocabulary(phrase or "", "name") +# 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 $: + user.add_selection_to_words_to_replace(phrase, "name") +# Automatically adds plural form by simply appending "s". +copy noun to replacements as $: + user.add_selection_to_words_to_replace(phrase, "noun") diff --git a/community/core/vocabulary/vocabulary.py b/community/core/vocabulary/vocabulary.py new file mode 100644 index 0000000..b0c2152 --- /dev/null +++ b/community/core/vocabulary/vocabulary.py @@ -0,0 +1,289 @@ +import logging +import os +import re +from typing import Sequence, Union + +from talon import Context, Module, actions +from talon.grammar import Phrase + +from ..user_settings import append_to_csv, track_csv_list + +mod = Module() +ctx = Context() + +mod.list("vocabulary", desc="additional vocabulary words") + +# Default words that will need to be capitalized. +# DON'T EDIT THIS. Edit settings/words_to_replace.csv instead. +# These defaults and those later in this file are ONLY used when +# auto-creating the corresponding settings/*.csv files. Those csv files +# determine the contents of user.vocabulary and dictate.word_map. Once they +# exist, the contents of the lists/dictionaries below are irrelevant. +_capitalize_defaults = [ + # NB. the lexicon now capitalizes January/February by default, but not the + # others below. Not sure why. + "January", + "February", + # March omitted because it's a regular word too + "April", + # May omitted because it's a regular word too + "June", + "July", + "August", # technically also an adjective but the month is far more common + "September", + "October", + "November", + "December", +] + +# Default words that need to be remapped. +_word_map_defaults = { + # E.g: + # "cash": "cache", + # This is the opposite ordering to words_to_replace.csv (the latter has the target word first) +} +_word_map_defaults.update({word.lower(): word for word in _capitalize_defaults}) +phrases_to_replace = {} + + +class PhraseReplacer: + """Utility for replacing phrases by other phrases inside text or word lists. + + Replacing longer phrases has priority. + + Args: + - phrase_dict: dictionary mapping recognized/spoken forms to written forms + """ + + def __init__(self): + self.phrase_index = {} + + def update(self, phrase_dict: dict[str, str]): + # Index phrases by first word, then number of subsequent words n_next + phrase_index = dict() + for spoken_form, written_form in phrase_dict.items(): + words = spoken_form.split() + if not words: + logging.warning( + "Found empty spoken form for written form" + f"{written_form}, ignored" + ) + continue + first_word, n_next = words[0], len(words) - 1 + phrase_index.setdefault(first_word, {}).setdefault(n_next, {})[ + tuple(words[1:]) + ] = written_form + + # Sort n_next index so longer phrases have priority + self.phrase_index = { + first_word: sorted(same_first_word.items(), key=lambda x: -x[0]) + for first_word, same_first_word in phrase_index.items() + } + + def replace(self, input_words: Sequence[str]) -> Sequence[str]: + input_words = tuple(input_words) # tuple to ensure hashability of slices + output_words = [] + first_word_i = 0 + while first_word_i < len(input_words): + first_word = input_words[first_word_i] + next_word_i = first_word_i + 1 + # Could this word be the first of a phrase we should replace? + for n_next, phrases_n_next in self.phrase_index.get(first_word, []): + # Yes. Perhaps a phrase with n_next subsequent words? + continuation = input_words[next_word_i : next_word_i + n_next] + if continuation in phrases_n_next: + # Found a match! + output_words.append(phrases_n_next[continuation]) + first_word_i += 1 + n_next + break + else: + # No match, just add the word to the result + output_words.append(first_word) + first_word_i += 1 + return output_words + + # Wrapper used for testing. + def replace_string(self, text: str) -> str: + return " ".join(self.replace(text.split())) + + +# Unit tests for PhraseReplacer +rep = PhraseReplacer() +rep.update( + { + "this": "foo", + "that": "bar", + "this is": "stopping early", + "this is a test": "it worked!", + } +) +assert rep.replace_string("gnork") == "gnork" +assert rep.replace_string("this") == "foo" +assert rep.replace_string("this that this") == "foo bar foo" +assert rep.replace_string("this is a test") == "it worked!" +assert rep.replace_string("well this is a test really") == "well it worked! really" +assert rep.replace_string("try this is too") == "try stopping early too" +assert rep.replace_string("this is a tricky one") == "stopping early a tricky one" + +phrase_replacer = PhraseReplacer() + + +# phrases_to_replace is a spoken form -> written form map, used by our +# implementation of `dictate.replace_words` (at bottom of file) to rewrite words +# and phrases Talon recognized. This does not change the priority with which +# Talon recognizes particular phrases over others. +@track_csv_list( + "words_to_replace.csv", + headers=("Replacement", "Original"), + default=_word_map_defaults, +) +def on_word_map(values): + global phrases_to_replace + phrases_to_replace = values + phrase_replacer.update(values) + + # "dictate.word_map" is used by Talon's built-in default implementation of + # `dictate.replace_words`, but supports only single-word replacements. + # Multi-word phrases are ignored. + ctx.settings["dictate.word_map"] = values + + +@ctx.action_class("dictate") +class OverwrittenActions: + def replace_words(words: Sequence[str]) -> Sequence[str]: + try: + return phrase_replacer.replace(words) + except: + # fall back to default implementation for error-robustness + logging.error("phrase replacer failed!") + return actions.next(words) + + +def _create_vocabulary_entries(spoken_form, written_form, type): + """Expands the provided spoken form and written form into multiple variants based on + the provided type, which can be either "name" to add a possessive variant or "noun" + to add plural. + """ + entries = {spoken_form: written_form} + if type == "name": + # Note that we use the spoken form without apostrophe because this seems to generally lead + # to better recognition on Conformer b108. + entries[f"{spoken_form}s"] = f"{written_form}'s" + elif type == "noun": + # Note that we simply append an "s", but we could use something more sophisticated like + # https://github.com/jpvanhal/inflection. The downside is that this is less predictable, + # and this feature is likely to be used in ways that are unlike common English prose, which + # is already included in the lexicon. For example, made up identifiers used in programming. + entries[f"{spoken_form}s"] = f"{written_form}s" + return entries + + +# See https://github.com/wolfmanstout/talon-vocabulary-editor for an experimental version +# of this which tests if the default spoken form can be used instead of the provided phrase. +def _add_selection_to_file( + phrase: Union[Phrase, str], + type: str, + file_name: str, + file_contents: dict[str, str], + skip_identical_replacement: bool, +): + written_form = actions.edit.selected_text().strip() + if phrase: + spoken_form = " ".join(actions.dictate.parse_words(phrase)) + else: + is_acronym = re.fullmatch(r"[A-Z]+", written_form) + spoken_form = " ".join(written_form) if is_acronym else written_form + entries = _create_vocabulary_entries(spoken_form, written_form, type) + added_some_phrases = False + + 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) + elif file_name == "vocabulary.talon-list": + append_to_vocabulary(new_entries) + + if added_some_phrases: + actions.app.notify(f"Added to {file_name}: {new_entries}") + + +def append_to_vocabulary(rows: dict[str, str]): + vocabulary_file_path = actions.user.get_vocabulary_file_path() + with open(str(vocabulary_file_path)) as file: + line = None + for line in file: + pass + needs_newline = line is not None and not line.endswith("\n") + + with open(vocabulary_file_path, "a", encoding="utf-8") as file: + if needs_newline: + file.write("\n") + for key, value in rows.items(): + if key == value: + file.write(f"{key}\n") + else: + if not str.isprintable(value) or "'" in value or '"' in value: + value = repr(value) + file.write(f"{key}: {value}\n") + + +@mod.action_class +class Actions: + # this is implemented as an action so it may be overridden in other contexts + def get_vocabulary_file_path(): + """Returns the path for the active vocabulary file""" + vocabulary_directory = os.path.dirname(os.path.realpath(__file__)) + vocabulary_file_path = os.path.join( + vocabulary_directory, "vocabulary.talon-list" + ) + return vocabulary_file_path + + def add_selection_to_vocabulary(phrase: Union[Phrase, str] = "", type: str = ""): + """Permanently adds the currently selected text to the vocabulary with the provided + spoken form and adds variants based on the type ("noun" or "name"). + """ + _add_selection_to_file( + phrase, + type, + "vocabulary.talon-list", + actions.user.talon_get_active_registry_list("user.vocabulary"), + False, + ) + + def add_selection_to_words_to_replace(phrase: Phrase, type: str = ""): + """Permanently adds the currently selected text as replacement for the provided + original form and adds variants based on the type ("noun" or "name"). + """ + _add_selection_to_file( + phrase, + type, + "words_to_replace.csv", + 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') diff --git a/community/core/vocabulary/vocabulary.talon-list b/community/core/vocabulary/vocabulary.talon-list new file mode 100644 index 0000000..36e6db8 --- /dev/null +++ b/community/core/vocabulary/vocabulary.talon-list @@ -0,0 +1,10 @@ +list: user.vocabulary +- +N map: nmap +under documented: under-documented +nmap +admin +Cisco +VPN +DNS +Minecraft diff --git a/community/core/websites_and_search_engines/search_engine.talon-list b/community/core/websites_and_search_engines/search_engine.talon-list new file mode 100644 index 0000000..8f0ba95 --- /dev/null +++ b/community/core/websites_and_search_engines/search_engine.talon-list @@ -0,0 +1,7 @@ +list: user.search_engine +- +amazon: https://www.amazon.com/s/?field-keywords=%s +google: https://www.google.com/search?q=%s +map: https://maps.google.com/maps?q=%s +scholar: https://scholar.google.com/scholar?q=%s +wiki: https://en.wikipedia.org/w/index.php?search=%s diff --git a/community/core/websites_and_search_engines/website.talon-list b/community/core/websites_and_search_engines/website.talon-list new file mode 100644 index 0000000..cff1c1a --- /dev/null +++ b/community/core/websites_and_search_engines/website.talon-list @@ -0,0 +1,18 @@ +list: user.website +- +talon home page: http://talonvoice.com +talon slack: http://talonvoice.slack.com/messages/help +talon wiki: https://talon.wiki/ +talon practice: https://chaosparrot.github.io/talon_practice/ +talon repository search: https://search.talonvoice.com/search/ +amazon: https://www.amazon.com/ +dropbox: https://dropbox.com/ +google: https://www.google.com/ +google calendar: https://calendar.google.com +google maps: https://maps.google.com/ +google scholar: https://scholar.google.com/ +gmail: https://mail.google.com/ +github: https://github.com/ +gist: https://gist.github.com/ +wikipedia: https://en.wikipedia.org/ +youtube: https://www.youtube.com/ diff --git a/community/core/websites_and_search_engines/websites_and_search_engines.py b/community/core/websites_and_search_engines/websites_and_search_engines.py new file mode 100644 index 0000000..40637ec --- /dev/null +++ b/community/core/websites_and_search_engines/websites_and_search_engines.py @@ -0,0 +1,33 @@ +import webbrowser +from urllib.parse import quote_plus + +from talon import Context, Module + +mod = Module() +mod.list("website", desc="A website.") +mod.list( + "search_engine", + 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: + def open_url(url: str): + """Visit the given URL.""" + webbrowser.open(url) + + 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/community/core/websites_and_search_engines/websites_and_search_engines.talon b/community/core/websites_and_search_engines/websites_and_search_engines.talon new file mode 100644 index 0000000..eab5887 --- /dev/null +++ b/community/core/websites_and_search_engines/websites_and_search_engines.talon @@ -0,0 +1,10 @@ +open {user.website}: user.open_url(website) +open that: user.open_url(edit.selected_text()) +open paste: user.open_url(clip.text()) + +{user.search_engine} hunt $: + user.search_with_search_engine(search_engine, user.text) +{user.search_engine} (that | this): + text = edit.selected_text() + user.search_with_search_engine(search_engine, text) +{user.search_engine} paste: user.search_with_search_engine(search_engine, clip.text()) diff --git a/community/core/windows_and_tabs/README.md b/community/core/windows_and_tabs/README.md new file mode 100644 index 0000000..c3dad45 --- /dev/null +++ b/community/core/windows_and_tabs/README.md @@ -0,0 +1,22 @@ +# Laying Out Windows + +The experimental laying out windows command requires that you first enable a tag. You can find an example in the provided settings.talon file, or you can set it yourself like this: + +``` +tag(): user.experimental_window_layout +``` + +The `layout` command allows you to lay out multiple windows around the screen in prearranged configurations. With a single command you can arrange multiple windows and if you repeat the same command it will rotate them. Here are some example arrangements: + +Half: Split the screen into two halves. The first window goes to the left half and the second goes to the right. +Thirds: Split the screen into thirds, arranging from left to right. +Clock: Arrange one window on the left half, and split the right from top to bottom. + +When arranging windows if you specify nothing it will arrange in order of windows from top to bottom-in other words, the most recent three windows that you have interacted with will be snapped into the arrangement. If you want more control you can specify windows by saying an application name or using an ordinal such as 'second' to refer to the second window from the top of the window manager (the second most recently used window). If you want to skip a particular position when arranging windows, you can use the word 'gap' to skip a position. You can also use the word 'all' to refer to the rest of the windows available filling up all available slots. Here are some examples: + +1. `layout clock`: Arrange the most recent three windows in a clockwise layout +2. `layout halves chrome slack`: Arrange chrome and slack in a split screen. +3. `layout halves gap slack`: Arrange slack on the right (skipping the first placement, which would have been on the left) +4. `layout clock second all`: Move these second from the top window to the first position, rearranging all other windows accordingly. + +If you repeat any of these commands without interacting with the window using the mouse, it will rotate the arrangement. diff --git a/community/core/windows_and_tabs/tabs.py b/community/core/windows_and_tabs/tabs.py new file mode 100644 index 0000000..981b23b --- /dev/null +++ b/community/core/windows_and_tabs/tabs.py @@ -0,0 +1,21 @@ +from talon import Module, actions, app + +mod = Module() + + +@mod.action_class +class TabActions: + def tab_jump(number: int): + """Jumps to the specified tab""" + + def tab_final(): + """Jumps to the final tab""" + + def tab_close_wrapper(): + """Closes the current tab. + Exists so that apps can implement their own delay before running tab_close() to handle repetitions better. + """ + actions.app.tab_close() + + def tab_duplicate(): + """Duplicates the current tab""" diff --git a/community/core/windows_and_tabs/tabs.talon b/community/core/windows_and_tabs/tabs.talon new file mode 100644 index 0000000..fe1d05e --- /dev/null +++ b/community/core/windows_and_tabs/tabs.talon @@ -0,0 +1,10 @@ +tag: user.tabs +- +tab (open | new): app.tab_open() +tab (last | previous): app.tab_previous() +tab next: app.tab_next() +tab close: user.tab_close_wrapper() +tab (reopen | restore): app.tab_reopen() +go tab : user.tab_jump(number) +go tab final: user.tab_final() +tab (duplicate | clone): user.tab_duplicate() diff --git a/community/core/windows_and_tabs/window_layout.py b/community/core/windows_and_tabs/window_layout.py new file mode 100644 index 0000000..00d6fbc --- /dev/null +++ b/community/core/windows_and_tabs/window_layout.py @@ -0,0 +1,291 @@ +import copy +import time +from dataclasses import dataclass +from typing import List, Optional, Union + +from talon import Context, Module, actions, settings, ui +from talon.ui import UIErr, Window + +from .windows_and_tabs import is_window_valid + +"""Tools for laying out windows in an arrangement """ + +SPLIT_POSITIONS = { + # Explicit layout names with only one configuration can be easier to force + # the desired result: + "HALF": ["LEFT", "RIGHT"], + "THIRDS": ["LEFT_THIRD", "CENTER_THIRD", "RIGHT_THIRD"], + "CLOCK": [ + "LEFT", + "TOP_RIGHT", + "BOTTOM_RIGHT", + ], + "COUNTERCLOCK": [ + "RIGHT", + "TOP_LEFT", + "BOTTOM_LEFT", + ], + "GRID": [ + "TOP_LEFT", + "TOP_RIGHT", + "BOTTOM_LEFT", + "BOTTOM_RIGHT", + ], + "BIG_GRID": [ + "TOP_LEFT_THIRD", + "TOP_CENTER_THIRD", + "TOP_RIGHT_THIRD", + "BOTTOM_LEFT_THIRD", + "BOTTOM_CENTER_THIRD", + "BOTTOM_RIGHT_THIRD", + ], +} + +# Keys in `windows_snap.py` `_snap_positions`, ie "TopLeft", "BottomCenterThird", etc. +SnapPosition = str + + +@dataclass +class WindowLayout: + """Represents a layout of windows on a screen""" + + name: str + split_positions: list[SnapPosition] + windows: list[Window] + can_rotate: bool + rotation_count: int + finish_time: float + + +class Gap: + """Users can leave gaps or holes (as in a code snippet) when dictating a layout; + this represents such a gap.""" + + pass + + +# Create a union type for Talon windows and Gaps: +Window = Union[Window, Gap] + +# The current layout being arranged and the last one arranged, if any. +layout_in_progress: Optional[WindowLayout] = None +last_layout: Optional[WindowLayout] = None + + +def snap_next(windows: list[Window], target_layout: SnapPosition) -> Optional[Window]: + """This function snaps a window and returns the window if successful""" + while windows: + window = windows.pop(0) + if isinstance(window, Gap): + return window + try: + actions.user.snap_window_to_position( + target_layout, + window, + ) + + return window + except (UIErr, AttributeError) as e: + print( + f'Failed to snap {window.app.name}\'s "{window.title}" window ({type(e).__name__} {e}); this is normal; continuing to the next' + ) + return Gap() + + +def snap_layout(layout: WindowLayout): + """Split the screen between multiple windows.""" + try: + global layout_in_progress, last_layout + layout_in_progress = layout + + # If called multiple times (and the user hasn't focused a window manually since + # last time), rotate the offset of the existing windows in the arrangement, + # allowing the user to use a repeater to cycle through the windows to get the + # desired result. + if ( + layout.can_rotate + and last_layout is not None + and last_layout.name == layout.name + and layout.windows == last_layout.windows + ): + layout.rotation_count = last_layout.rotation_count + 1 + + # Copy these data structures so we can mutate them: + remaining_windows = [w for w in layout.windows] + split_positions = layout.split_positions.copy() + + snapped_windows = [] + for _ in range(layout.rotation_count): + split_positions.append(split_positions.pop(0)) + + while len(split_positions) > 0: + snapped_window: Window = snap_next( + remaining_windows, split_positions.pop(0) + ) + snapped_windows.insert(0, snapped_window) + + if len(snapped_windows) > 0: + for _ in range(layout.rotation_count): + snapped_windows.append(snapped_windows.pop(0)) + + for window in snapped_windows: + if isinstance(window, Gap): + continue + actions.user.switcher_focus_window(window) + + layout_in_progress.finish_time = time.perf_counter() + last_layout = layout_in_progress + finally: + layout_in_progress = None + + +def filter_nonviable_windows(windows: List[Window]) -> list[Window]: + active_window = ui.active_window() + + # Many invisible non-resizable windows are identifiable because they exist above the current window + # in the z-index + all_windows = ui.windows() + active_window_idx = all_windows.index(active_window) # type: ignore + return list( + filter( + lambda w: (isinstance(w, Gap) or is_window_valid(w)), + windows, + ) + ) + + +mod = Module() +mod.list( + "window_split_positions", + "Predefined window positions when splitting the screen between multiple windows.", +) +mod.tag( + "experimental_window_layout", + desc="Tag to enable experimental window layout commands", +) + +ctx = Context() + + +@mod.capture(rule="all") +def all_candidate_windows(m) -> list[Window]: + return filter_nonviable_windows(ui.windows()) + + +@mod.capture(rule="gap") +def skip_window(m) -> list[Window]: + return [Gap()] + + +@mod.capture(rule="") +def application_windows(m) -> list[Window]: + return filter_nonviable_windows( + [ + window + for app in m.running_applications_list + for window in actions.self.get_running_app(app).windows() + ] + ) + + +@mod.capture( + rule="||" +) +def layout_item(m) -> list[Optional[Window]]: + attributes = [ + "application_windows", + "numbered_windows", + "skip_window", + ] + num_passed = len(list(filter(lambda attrs: hasattr(m, attrs), attributes))) + if num_passed > 1: + raise ValueError( + "Multiple attributes found on 'm'. Only one of 'application_windows', 'numbered_windows', or 'skip_window' should be present." + ) + + # Return the appropriate list based on which attribute is available + if hasattr(m, "application_windows"): + return m.application_windows + elif hasattr(m, "numbered_windows"): + return m.numbered_windows + elif hasattr(m, "skip_window"): + return m.skip_window + else: + return [] + + +@mod.capture(rule="+") +def numbered_windows(m) -> list[Window]: + all_windows = filter_nonviable_windows(ui.windows()) + selected_windows = [ + all_windows[i - 1] for i in m.ordinals_small_list if i - 1 < len(all_windows) + ] + return selected_windows + + +@mod.capture(rule="+ []") +def target_windows(m) -> list[Window]: + windows = [] + if hasattr(m, "layout_item_list"): + windows += [window for sublist in m.layout_item_list for window in sublist] + + if hasattr(m, "all_candidate_windows"): + windows += [w for w in m.all_candidate_windows if w not in windows] + return windows + + +def pick_split_arrangement( + target_windows: Optional[list[Window]], + layout_name: str, +) -> list[SnapPosition]: + return SPLIT_POSITIONS[layout_name] + + +@mod.capture(rule="{user.window_split_positions} []") +def window_layout(m) -> WindowLayout: + global last_layout + layout_name = m.window_split_positions + window_was_specified = hasattr(m, "target_windows") + + target_windows = ( + m.target_windows + if window_was_specified + else filter_nonviable_windows(ui.windows()) + ) + + layout = pick_split_arrangement(target_windows, layout_name) + return WindowLayout( + name=layout_name, + split_positions=layout, + windows=target_windows, + can_rotate=True, + rotation_count=0, + finish_time=0, + ) + + +@mod.action_class +class Actions: + def snap_layout(layout: WindowLayout): + """Split the screen between multiple applications.""" + snap_layout(layout) + + +def focus_callback(_): + global layout_in_progress + global last_layout + + # Running a layout will generate focus events, which we don't consider to be manual + # / user initiated, so skip in that case. + if last_layout is None or layout_in_progress is not None: + return + + # Track if the user has manually focused since layout and clear the state if so; + # this way we won't rotate if that same layout request is made again. + delta = time.perf_counter() - last_layout.finish_time + if delta >= 1: + last_layout = None + + +ui.register("app_activate", focus_callback) +ui.register("win_focus", focus_callback) diff --git a/community/core/windows_and_tabs/window_layout.talon b/community/core/windows_and_tabs/window_layout.talon new file mode 100644 index 0000000..ba3edf5 --- /dev/null +++ b/community/core/windows_and_tabs/window_layout.talon @@ -0,0 +1,3 @@ +tag: user.experimental_window_layout +- +layout : user.snap_layout(window_layout) diff --git a/community/core/windows_and_tabs/window_management.talon b/community/core/windows_and_tabs/window_management.talon new file mode 100644 index 0000000..9179d3d --- /dev/null +++ b/community/core/windows_and_tabs/window_management.talon @@ -0,0 +1,23 @@ +window (new | open): app.window_open() +window next: app.window_next() +window last: app.window_previous() +window close: app.window_close() +window hide: app.window_hide() +app (preferences | prefs | settings): app.preferences() +focus : user.switcher_focus(running_applications) +# following only works on windows. Can't figure out how to make it work for mac. No idea what the equivalent for linux would be. +focus$: user.switcher_menu() +focus last: user.switcher_focus_last() +running list: user.switcher_toggle_running() +running close: user.switcher_hide_running() +launch : user.switcher_launch(launch_applications) + +snap : user.snap_window(window_snap_position) +snap next [screen]: user.move_window_next_screen() +snap last [screen]: user.move_window_previous_screen() +snap screen : user.move_window_to_screen(number) +snap : + user.snap_app(running_applications, window_snap_position) + +snap [screen] : + user.move_app_to_screen(running_applications, number) diff --git a/community/core/windows_and_tabs/window_snap.py b/community/core/windows_and_tabs/window_snap.py new file mode 100644 index 0000000..7582387 --- /dev/null +++ b/community/core/windows_and_tabs/window_snap.py @@ -0,0 +1,354 @@ +"""Tools for voice-driven window management. + +Originally from dweil/talon_community - modified for newapi by jcaw. + +""" + +# TODO: Map keyboard shortcuts to this manager once Talon has key hooks on all +# platforms + +import logging +from typing import Dict, Optional + +from talon import Context, Module, actions, app, registry, settings, ui +from talon.ui import Window + +mod = Module() +mod.list( + "window_snap_positions", + "Predefined window positions for the current window. See `RelativeScreenPos`.", +) +mod.list( + "window_split_positions", + "Predefined window positions when splitting the screen between three applications.", +) +mod.setting( + "window_snap_screen", + type=str, + default="proportional", + desc="""How to position and size windows when snapping across different physical screens. Options: + + "proportional" (default): Preserve the window's relative position and size proportional to the screen. + + "size aware": Preserve position relative to the screen, but keep absolute size the same, except if window is full-height or -width, keep it so. +""", +) + + +def _set_window_pos(window, x, y, width, height): + """Helper to set the window position.""" + window.rect = ui.Rect(round(x), round(y), round(width), round(height)) + + # on occassion, for whatever reason, it fails to + # position correctly on windows the first time + if app.platform == "windows" and "user.experimental_window_layout" in registry.tags: + actions.sleep("100ms") + window.rect = ui.Rect(round(x), round(y), round(width), round(height)) + + +def _bring_forward(window): + current_window = ui.active_window() + try: + window.focus() + current_window.focus() + except Exception as e: + # We don't want to block if this fails. + print(f"Couldn't bring window to front: {e}") + + +def _get_app_window(app_name: str) -> ui.Window: + return actions.self.get_running_app(app_name).active_window + + +def interpolate_interval(w0, w1, s0, s1, d0, d1): + """ + Interpolates an interval (w0, w1) which is within (s0, s1) so that it lies + within (d0, d1). Returns (r0, r1). Tries to preserve absolute interval size, + w1 - w0, while maintaining its relative 'position' within (s0, s1). For + instance, if w0 == s0 then r0 == d0. + + Use-case: fix a window w, a source screen s, and a destination screen d. + Let w0 = w.left, w1 = window.right, s0 = s.left, s1 = s.right, d0 = d.left, d1 = d.right. + """ + wsize, ssize, dsize = w1 - w0, s1 - s0, d1 - d0 + assert wsize > 0 and ssize > 0 and dsize > 0 + before = max(0, (w0 - s0) / ssize) + after = max(0, (s1 - w1) / ssize) + # If we're within 5% of maximized, preserve this. + if before + after <= 0.05: + return (d0, d1) + # If before is 0 (eg. window is left-aligned), we want to preserve before. + # If after is 0 (eg. window is right-aligned), we want to preserve after. + # In between, we linearly interpolate. + beforeness = before / (before + after) + afterness = after / (before + after) + a0, b1 = d0 + before * dsize, d1 - after * dsize + a1, b0 = a0 + wsize, b1 - wsize + r0 = a0 * afterness + b0 * beforeness + r1 = a1 * afterness + b1 * beforeness + return (max(d0, r0), min(d1, r1)) # clamp to destination + + +def _move_to_screen( + window: ui.Window, offset: Optional[int] = None, screen_number: Optional[int] = None +): + """Move a window to a different screen. + + Provide one of `offset` or `screen_number` to specify a target screen. + + Provide `window` to move a specific window, otherwise the current window is + moved. + + """ + assert ( + screen_number or offset and not (screen_number and offset) + ), "Provide exactly one of `screen_number` or `offset`." + + src_screen = window.screen + + if offset: + if offset < 0: + dest_screen = actions.user.screens_get_previous(src_screen) + else: + dest_screen = actions.user.screens_get_next(src_screen) + else: + dest_screen = actions.user.screens_get_by_number(screen_number) + + if src_screen == dest_screen: + return + + 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 + left, right = interpolate_interval( + r.left, r.right, src.left, src.right, dest.left, dest.right + ) + top, bot = interpolate_interval( + r.top, r.bot, src.top, src.bot, dest.top, dest.bot + ) + r.x, r.y = left, top + 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 + # Does the orientation between the screens change? (vertical/horizontal) + if how != "proportional": + logging.warning( + f"Unrecognized 'window_snap_screen' setting: {how!r}. Using default 'proportional'." + ) + if (src.width / src.height > 1) != (dest.width / dest.height > 1): + # Horizontal -> vertical or vertical -> horizontal + # Retain proportional window size, but flip x/y of the vertical monitor to account for the monitors rotation. + if src.width / src.height > 1: + # horizontal -> vertical + width = window.rect.width * dest.height / src.width + height = window.rect.height * dest.width / src.height + else: + # vertical -> horizontal + width = window.rect.width * dest.width / src.height + height = window.rect.height * dest.height / src.width + # Deform window if width or height is bigger than the target monitors while keeping the window area the same. + if width > dest.width: + over = (width - dest.width) * height + width = dest.width + height += over / width + if height > dest.height: + over = (height - dest.height) * width + height = dest.height + width += over / height + # Proportional position: + # Since the window size in respect to the monitor size is not proportional (x/y was flipped), + # the positioning is more complicated than proportionally scaling the x/y coordinates. + # It is computed by keeping the free space to the left of the window proportional to the right + # and respectively for the top/bottom free space. + # The if conditions account for division by 0. TODO: Refactor positioning without division by 0 + if src.height == window.rect.height: + x = dest.left + (dest.width - width) / 2 + else: + x = dest.left + (window.rect.top - src.top) * (dest.width - width) / ( + src.height - window.rect.height + ) + if src.width == window.rect.width: + y = dest.top + (dest.height - height) / 2 + else: + y = dest.top + (window.rect.left - src.left) * (dest.height - height) / ( + src.width - window.rect.width + ) + else: + # Horizontal -> horizontal or vertical -> vertical + # Retain proportional size and position + proportional_width = dest.width / src.width + proportional_height = dest.height / src.height + x = dest.left + (window.rect.left - src.left) * proportional_width + y = dest.top + (window.rect.top - src.top) * proportional_height + width = window.rect.width * proportional_width + height = window.rect.height * proportional_height + _set_window_pos(window, x=x, y=y, width=width, height=height) + if maximized: + window.maximized = True + + +def _snap_window_helper(window, pos): + screen = window.screen.visible_rect + + _set_window_pos( + window, + x=screen.x + (screen.width * pos.left), + y=screen.y + (screen.height * pos.top), + width=screen.width * (pos.right - pos.left), + height=screen.height * (pos.bottom - pos.top), + ) + + +class RelativeScreenPos: + """Represents a window position as a fraction of the screen.""" + + def __init__(self, left, top, right, bottom): + self.left = left + self.top = top + self.bottom = bottom + self.right = right + + 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), + # 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), + # 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), + # Quarters + # .---.---. + # |---|---| + # '---'---' + "TOP_LEFT": RelativeScreenPos(0, 0, 0.5, 0.5), + "TOP_RIGHT": RelativeScreenPos(0.5, 0, 1, 0.5), + "BOTTOM_LEFT": RelativeScreenPos(0, 0.5, 0.5, 1), + "BOTTOM_RIGHT": RelativeScreenPos(0.5, 0.5, 1, 1), + # Sixths + # .--.--.--. + # |--|--|--| + # '--'--'--' + "TOP_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), + # Special + "CENTER": RelativeScreenPos(1 / 8, 1 / 6, 7 / 8, 5 / 6), + "FULL": RelativeScreenPos(0, 0, 1, 1), + "FULLSCREEN": RelativeScreenPos(0, 0, 1, 1), +} + + +@mod.capture(rule="{user.window_snap_positions}") +def window_snap_position(m) -> RelativeScreenPos: + return _snap_positions[m.window_snap_positions] + + +ctx = Context() +ctx.lists["user.window_snap_positions"] = _snap_positions.keys() + + +@mod.action_class +class Actions: + def snap_window( + 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, 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] + 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. + 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.""" + _move_to_screen(ui.active_window(), offset=1) + + def move_window_previous_screen() -> None: + """Move the active window to the previous screen.""" + _move_to_screen(ui.active_window(), offset=-1) + + def move_window_to_screen(screen_number: int) -> None: + """Move the active window leftward by one.""" + _move_to_screen(ui.active_window(), screen_number=screen_number) + + def snap_app(app_name: str, position: RelativeScreenPos): + """Snap a specific application to another screen.""" + window = _get_app_window(app_name) + _bring_forward(window) + _snap_window_helper(window, position) + + def move_app_to_screen(app_name: str, screen_number: int): + """Move a specific application to another screen.""" + window = _get_app_window(app_name) + _bring_forward(window) + _move_to_screen( + window, + screen_number=screen_number, + ) diff --git a/community/core/windows_and_tabs/window_snap_positions.talon-list b/community/core/windows_and_tabs/window_snap_positions.talon-list new file mode 100644 index 0000000..959f055 --- /dev/null +++ b/community/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 diff --git a/community/core/windows_and_tabs/window_split_positions.talon-list b/community/core/windows_and_tabs/window_split_positions.talon-list new file mode 100644 index 0000000..a152328 --- /dev/null +++ b/community/core/windows_and_tabs/window_split_positions.talon-list @@ -0,0 +1,9 @@ +list: user.window_split_positions +- + +half: HALF +thirds: THIRDS +clock: CLOCK +counterclock: COUNTERCLOCK +grid: GRID +big grid: BIG_GRID diff --git a/community/core/windows_and_tabs/windows_and_tabs.py b/community/core/windows_and_tabs/windows_and_tabs.py new file mode 100644 index 0000000..a496fce --- /dev/null +++ b/community/core/windows_and_tabs/windows_and_tabs.py @@ -0,0 +1,45 @@ +from talon import Context, actions, ui + +ctx = Context() + + +@ctx.action_class("app") +class AppActions: + def window_previous(): + cycle_windows(ui.active_app(), -1) + + def window_next(): + cycle_windows(ui.active_app(), 1) + + +def cycle_windows(app: ui.App, diff: int): + """Cycle windows backwards or forwards for the given application""" + active = app.active_window + windows = [w for w in app.windows() if w == active or is_window_valid(w)] + windows.sort(key=lambda w: w.id) + current = windows.index(active) + i = (current + diff) % len(windows) + + while i != current: + try: + actions.user.switcher_focus_window(windows[i]) + break + except Exception: + i = (i + diff) % len(windows) + + +def is_window_valid(window: ui.Window) -> bool: + """Returns true if this window is valid for focusing""" + try: + return ( + not window.hidden + # On Windows, there are many fake windows with empty titles -- this excludes them. + and len(window.title) > 0 + and (window.title not in ("Chrome Legacy Window")) + # This excludes many tiny windows that are not actual windows, and is a rough heuristic. + and window.rect.width > window.screen.dpi + and window.rect.height > window.screen.dpi + ) + except AttributeError: + # Handle case where window.rect might not be accessible + return False diff --git a/community/core/windows_and_tabs/windows_and_tabs_linux.py b/community/core/windows_and_tabs/windows_and_tabs_linux.py new file mode 100644 index 0000000..78b10c1 --- /dev/null +++ b/community/core/windows_and_tabs/windows_and_tabs_linux.py @@ -0,0 +1,44 @@ +# defines the default app actions for linux + +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: linux +""" + + +@ctx.action_class("app") +class AppActions: + def tab_close(): + actions.key("ctrl-w") + + def tab_next(): + actions.key("ctrl-tab") + + def tab_open(): + actions.key("ctrl-t") + + def tab_previous(): + actions.key("ctrl-shift-tab") + + def tab_reopen(): + actions.key("ctrl-shift-t") + + def window_close(): + actions.key("alt-f4") + + def window_hide(): + actions.key("alt-space n") + + def window_hide_others(): + actions.key("win-d alt-tab") + + def window_open(): + actions.key("ctrl-n") + + +@ctx.action_class("user") +class UserActions: + def switcher_focus_last(): + actions.key("alt-tab") diff --git a/community/core/windows_and_tabs/windows_and_tabs_mac.py b/community/core/windows_and_tabs/windows_and_tabs_mac.py new file mode 100644 index 0000000..3fac8ab --- /dev/null +++ b/community/core/windows_and_tabs/windows_and_tabs_mac.py @@ -0,0 +1,51 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: mac +""" + + +@ctx.action_class("app") +class AppActions: + def preferences(): + actions.key("cmd-,") + + def tab_close(): + actions.key("cmd-w") + + def tab_next(): + actions.key("ctrl-tab") + + def tab_open(): + actions.key("cmd-t") + + def tab_previous(): + actions.key("ctrl-shift-tab") + + def tab_reopen(): + actions.key("cmd-shift-t") + + def window_close(): + actions.key("cmd-w") + + def window_hide(): + actions.key("cmd-m") + + def window_hide_others(): + actions.key("cmd-alt-h") + + def window_open(): + actions.key("cmd-n") + + def window_previous(): + actions.key("cmd-shift-`") + + def window_next(): + actions.key("cmd-`") + + +@ctx.action_class("user") +class UserActions: + def switcher_focus_last(): + actions.key("cmd-tab") diff --git a/community/core/windows_and_tabs/windows_and_tabs_win.py b/community/core/windows_and_tabs/windows_and_tabs_win.py new file mode 100644 index 0000000..2070ac2 --- /dev/null +++ b/community/core/windows_and_tabs/windows_and_tabs_win.py @@ -0,0 +1,44 @@ +# defines the default app actions for windows + +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: windows +""" + + +@ctx.action_class("app") +class AppActions: + def tab_close(): + actions.key("ctrl-w") + + def tab_next(): + actions.key("ctrl-tab") + + def tab_open(): + actions.key("ctrl-t") + + def tab_previous(): + actions.key("ctrl-shift-tab") + + def tab_reopen(): + actions.key("ctrl-shift-t") + + def window_close(): + actions.key("alt-f4") + + def window_hide(): + actions.key("alt-space n") + + def window_hide_others(): + actions.key("win-d alt-tab") + + def window_open(): + actions.key("ctrl-n") + + +@ctx.action_class("user") +class UserActions: + def switcher_focus_last(): + actions.key("alt-tab") diff --git a/community/lang/batch/batch.py b/community/lang/batch/batch.py new file mode 100644 index 0000000..e9827af --- /dev/null +++ b/community/lang/batch/batch.py @@ -0,0 +1,12 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +code.language: batch +""" + + +@ctx.action_class("user") +class UserActions: + def code_comment_line_prefix(): + actions.user.insert_snippet_by_name("commentLine") diff --git a/community/lang/batch/batch.talon b/community/lang/batch/batch.talon new file mode 100644 index 0000000..fe66aec --- /dev/null +++ b/community/lang/batch/batch.talon @@ -0,0 +1,16 @@ +code.language: batch +- +tag(): user.code_comment_line + +# exit without killing cmd shell +soft exit: "exit /B 1\n" +# exit with killing cmd shell +hard exit: "exit 1\n" +echo: "echo " +echo off: "@echo off\n" +call: "call " +call shell: "call cmd \\c " +if error: "if errorlevel 1 " +go to: "goto " +delayed expansion: "SETLOCAL EnableDelayedExpansion\n" +arg : "%{number_small}" diff --git a/community/lang/c/c.py b/community/lang/c/c.py new file mode 100644 index 0000000..1a3af71 --- /dev/null +++ b/community/lang/c/c.py @@ -0,0 +1,241 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +mod = Module() + +ctx = Context() +ctx.matches = r""" +code.language: c +""" + +ctx.lists["self.c_pointers"] = { + "pointer": "*", + "pointer to pointer": "**", +} + +ctx.lists["self.stdint_signed"] = { + "signed": "", + "unsigned": "u", + "you": "u", +} + +ctx.lists["self.c_signed"] = { + "signed": "signed", + "unsigned": "unsigned", +} + +ctx.lists["self.c_keywords"] = { + "static": "static", + "volatile": "volatile", + "register": "register", +} + +ctx.lists["self.stdint_types"] = { + "character": "int8_t", + "char": "int8_t", + "short": "int16_t", + "long": "int32_t", + "long long": "int64_t", + "int": "int32_t", + "integer": "int32_t", + "void": "void", + "double": "double", + "struct": "struct", + "struck": "struct", + "num": "enum", + "union": "union", + "float": "float", +} + +ctx.lists["self.c_types"] = { + "character": "char", + "char": "char", + "short": "short", + "long": "long", + "int": "int", + "integer": "int", + "void": "void", + "double": "double", + "struct": "struct", + "struck": "struct", + "num": "enum", + "union": "union", + "float": "float", +} + +ctx.lists["user.code_libraries"] = { + "assert": "assert.h", + "type": "ctype.h", + "error": "errno.h", + "float": "float.h", + "limits": "limits.h", + "locale": "locale.h", + "math": "math.h", + "set jump": "setjmp.h", + "signal": "signal.h", + "arguments": "stdarg.h", + "definition": "stddef.h", + "input": "stdio.h", + "output": "stdio.h", + "library": "stdlib.h", + "string": "string.h", + "time": "time.h", + "standard int": "stdint.h", +} + +mod.list("c_pointers", desc="Common C pointers") +mod.list("c_signed", desc="Common C datatype signed modifiers") +mod.list("c_keywords", desc="C keywords") +mod.list("c_types", desc="Common C types") +mod.list("stdint_types", desc="Common stdint C types") +mod.list("stdint_signed", desc="Common stdint C datatype signed modifiers") + + +@mod.capture(rule="{self.c_pointers}") +def c_pointers(m) -> str: + "Returns a string" + return m.c_pointers + + +@mod.capture(rule="{self.c_signed}") +def c_signed(m) -> str: + "Returns a string" + return m.c_signed + + +@mod.capture(rule="{self.c_keywords}") +def c_keywords(m) -> str: + "Returns a string" + return m.c_keywords + + +@mod.capture(rule="{self.c_types}") +def c_types(m) -> str: + "Returns a string" + return m.c_types + + +@mod.capture(rule="{self.stdint_types}") +def stdint_types(m) -> str: + "Returns a string" + return m.stdint_types + + +@mod.capture(rule="{self.stdint_signed}") +def stdint_signed(m) -> str: + "Returns a string" + return m.stdint_signed + + +@mod.capture(rule="[] [+]") +def c_cast(m) -> str: + "Returns a string" + return "(" + " ".join(list(m)) + ")" + + +@mod.capture(rule="[] [+]") +def stdint_cast(m) -> str: + "Returns a string" + return "(" + "".join(list(m)) + ")" + + +@mod.capture(rule="[] []") +def c_variable(m) -> str: + "Returns a string" + return " ".join(list(m)) + + +operators = Operators( + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + BITWISE_AND=" & ", + BITWISE_OR=" | ", + BITWISE_NOT="~", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_RIGHT_SHIFT=" >> ", + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + 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="!", + POINTER_INDIRECTION="*", + POINTER_ADDRESS_OF="&", + POINTER_STRUCTURE_DEREFERENCE="->", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_insert_null(): + actions.auto_insert("NULL") + + def code_insert_is_null(): + actions.auto_insert(" == NULL ") + + def code_insert_is_not_null(): + actions.auto_insert(" != NULL") + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + def code_insert_function(text: str, selection: str): + if selection: + text = text + f"({selection})" + else: + text = text + "()" + + actions.user.paste(text) + actions.edit.left() + + # TODO - it would be nice that you integrate that types from c_cast + # instead of defaulting to void + def code_private_function(text: str): + """Inserts private function declaration""" + result = "void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_private_static_function(text: str): + """Inserts private static function""" + result = "static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_insert_library(text: str, selection: str): + actions.user.paste(f"#include <{text}>") diff --git a/community/lang/c/c.talon b/community/lang/c/c.talon new file mode 100644 index 0000000..f1d6146 --- /dev/null +++ b/community/lang/c/c.talon @@ -0,0 +1,86 @@ +code.language: c +- +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_functions_common +tag(): user.code_libraries +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_bitwise +tag(): user.code_operators_math +tag(): user.code_operators_pointer + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_protected_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_protected_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +# NOTE: migrated from generic, as they were only used here, though once cpp support is added, perhaps these should be migrated to a tag together with the commands below +state include: user.insert_snippet_by_name("importStatement") +state include system: user.insert_snippet_by_name("includeSystemStatement") +state include local: user.insert_snippet_by_name("includeLocalStatement") +state type deaf: insert("typedef ") +state type deaf struct: user.insert_snippet_by_name("typedefStructDeclaration") + +# XXX - create a preprocessor tag for these, as they will match cpp, etc +state define: user.insert_snippet_by_name("preprocessorDefineStatement") +state (undefine | undeaf): user.insert_snippet_by_name("preprocessorUndefineStatement") +state if (define | deaf): user.insert_snippet_by_name("preprocessorIfDefineStatement") +[state] define $: + user.insert_snippet_by_name_with_phrase("preprocessorDefineStatement", text) +[state] (undefine | undeaf) $: + user.insert_snippet_by_name_with_phrase("preprocessorUndefineStatement", text) +[state] if (define | deaf) $: + user.insert_snippet_by_name_with_phrase("preprocessorIfDefineStatement", text) + +# XXX - preprocessor instead of pre? +state pre if: user.insert_snippet_by_name("preprocessorIfStatement") +state error: user.insert_snippet_by_name("preprocessorErrorStatement") +state pre else if: user.insert_snippet_by_name("preprocessorElseIfStatement") +state pre end: user.insert_snippet_by_name("preprocessorEndIfStatement") +state pragma: user.insert_snippet_by_name("preprocessorPragmaStatement") +state default: "default:\nbreak;" + +#control flow +#best used with a push like command +#the below example may not work in editors that automatically add the closing brace +#if so uncomment the two lines and comment out the rest accordingly +push braces: + edit.line_end() + #insert("{") + #key(enter) + insert("{}") + edit.left() + key(enter) + key(enter) + edit.up() + +# Declare variables or structs etc. +# Ex. * int myList + : + insert("{c_variable} ") + insert(user.formatted_text(phrase, "PRIVATE_CAMEL_CASE,NO_SPACES")) + + : insert("{c_variable} {letter} ") + +# Ex. (int *) +cast to : "{c_cast}" +standard cast to : "{stdint_cast}" +: "{c_types}" +: "{c_pointers}" +: "{c_keywords}" +: "{c_signed} " +standard : "{stdint_types}" +int main: user.insert_between("int main(", ")") + +include : + user.code_insert_library(code_libraries, "") + key(end enter) diff --git a/community/lang/c/code_common_function.talon-list b/community/lang/c/code_common_function.talon-list new file mode 100644 index 0000000..b0901d7 --- /dev/null +++ b/community/lang/c/code_common_function.talon-list @@ -0,0 +1,47 @@ +list: user.code_common_function +code.language: c +- + +alloc ah: alloca +ay to eye: atoi +ef close: fclose +ef open: fopen +ef read: fread +ef write: fwrite +em map: mmap +em un map: munmap +es en print eff: sprintf +es print eff: sprintf +exit +free +get char: getchar +get op: getopt +is digit: isdigit +ma map: mmap +malloc +mem copy: memcpy +mem set: memset +print eff: printf +re alloc: realloc +see alloc: calloc +set jump: setjmp +signal +size of: sizeof +stir cat: strcat +string cat: strcat +stir comp: strcmp +stir copy: strcpy +stir dupe: strdup +string dupe: strdup +stir elle cat: strlcat +stir l cat: strlcat +stir elle copy: strlcpy +stir l copy: strlcpy +stir en cat: strncat +stir en copy: strncpy +stir en comp: strncmp +stir len: strlen +string len: strlen +stir to int: strtoint +stir to unsigned int: strtouint +string char: strchr diff --git a/community/lang/cpp/cpp.py b/community/lang/cpp/cpp.py new file mode 100644 index 0000000..6981815 --- /dev/null +++ b/community/lang/cpp/cpp.py @@ -0,0 +1,16 @@ +from talon import Context, Module + +from ..c.c import operators +from ..tags.operators import Operators + +ctx = Context() + +ctx.matches = r""" +code.language: cpp +""" + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators diff --git a/community/lang/cpp/cpp.talon b/community/lang/cpp/cpp.talon new file mode 100644 index 0000000..8a22e9f --- /dev/null +++ b/community/lang/cpp/cpp.talon @@ -0,0 +1,8 @@ +code.language: cpp +- + +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_bitwise +tag(): user.code_operators_math +tag(): user.code_operators_pointer diff --git a/community/lang/csharp/code_common_function.talon-list b/community/lang/csharp/code_common_function.talon-list new file mode 100644 index 0000000..2b63140 --- /dev/null +++ b/community/lang/csharp/code_common_function.talon-list @@ -0,0 +1,7 @@ +list: user.code_common_function +code.language: csharp +- + +integer: int.TryParse +print: Console.WriteLine +string: .ToString diff --git a/community/lang/csharp/csharp.py b/community/lang/csharp/csharp.py new file mode 100644 index 0000000..12c484d --- /dev/null +++ b/community/lang/csharp/csharp.py @@ -0,0 +1,142 @@ +from talon import Context, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +ctx.matches = r""" +code.language: csharp +""" + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + ASSIGNMENT_INCREMENT="++", + # code_operators_bitwise + BITWISE_NOT="~", + BITWISE_AND=" & ", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_OR=" | ", + BITWISE_RIGHT_SHIFT=" >> ", + # code_operators_lambda + LAMBDA="=>", + # code_operators_pointer + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EQUAL=" == ", + MATH_NOT_EQUAL=" != ", + MATH_OR=" || ", + MATH_AND=" && ", + MATH_NOT="!", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_GREATER_THAN=" > ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_LESS_THAN=" < ", + # code_operators_pointer + POINTER_ADDRESS_OF="&", + POINTER_INDIRECTION="*", + POINTER_STRUCTURE_DEREFERENCE="->", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_self(): + actions.auto_insert("this") + + def code_operator_object_accessor(): + actions.auto_insert(".") + + def code_insert_null(): + actions.auto_insert("null") + + def code_insert_is_null(): + actions.auto_insert(" == null ") + + def code_insert_is_not_null(): + actions.auto_insert(" != null") + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + 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 = "private void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_private_static_function(text: str): + """Inserts private static function""" + result = "private static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_protected_function(text: str): + result = "private void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_protected_static_function(text: str): + result = "protected static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + result = "public void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_public_static_function(text: str): + result = "public static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) diff --git a/community/lang/csharp/csharp.talon b/community/lang/csharp/csharp.talon new file mode 100644 index 0000000..d59ff34 --- /dev/null +++ b/community/lang/csharp/csharp.talon @@ -0,0 +1,26 @@ +code.language: csharp +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +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_functions_common +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 = "PUBLIC_CAMEL_CASE" + user.code_public_function_formatter = "PUBLIC_CAMEL_CASE" + user.code_private_variable_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_variable_formatter = "PUBLIC_CAMEL_CASE" + user.code_public_variable_formatter = "PUBLIC_CAMEL_CASE" diff --git a/community/lang/css/code_common_function.talon-list b/community/lang/css/code_common_function.talon-list new file mode 100644 index 0000000..11602ad --- /dev/null +++ b/community/lang/css/code_common_function.talon-list @@ -0,0 +1,54 @@ +list: user.code_common_function +code.language: css +code.language: scss +- + +# reference +attribute: attr +env +url +var +variable: var +# mathematical +calc +calculate: calc +clamp +max +min +# color +HSL: hsl +hue sat light: hsl +HSLA: hsla +lab +LCH: lch +RGB: rgb +red green blue: rgb +RGBA: rgba +color +# image functions +linear gradient: linear-gradient +# counter functions +counter +counters +symbols +# filter +blur +brightness +contrast +drop shadow: drop-shadow +grayscale +hue rotate: hue-rotate +invert +opacity +saturate +sepia +# grid +fit content: fit-content +min max: minmax +repeat +# transform +matrix +rotate +scale +skew +translate diff --git a/community/lang/css/css.py b/community/lang/css/css.py new file mode 100644 index 0000000..5fafa39 --- /dev/null +++ b/community/lang/css/css.py @@ -0,0 +1,81 @@ +from talon import Context, Module, actions + +from ..tags.operators import Operators + +mod = Module() +global_ctx = Context() +ctx = Context() +ctx.matches = """ +code.language: css +code.language: scss +""" + +mod.list("css_at_rule", desc="List of CSS @rules") +mod.list("css_unit", desc="List of CSS units") +mod.list("css_global_value", desc="CSS-wide values") + +global_ctx.lists["self.css_unit"] = { + # distance (length) + "char": "ch", + "em": "em", + "rem": "rem", + "pixels": "px", + "points": "pt", + "view height": "vh", + "view width": "vw", + # angle + "degrees": "deg", + "radians": "rad", + "turn": "turn", + # duration (time) + "seconds": "s", + "millis": "ms", + # resolution + "dots per pixel": "dppx", + # flexible length (flex) - grid + "fraction": "fr", +} + +global_ctx.lists["self.css_at_rule"] = { + # regular + "charset": "charset", + "import": "import", + "namespace": "namespace", + # conditional group + "media": "media", + "supports": "supports", + # other nested + "page": "page", + "font face": "font-face", + "keyframes": "keyframes", + # CSS Modules + "value": "value", +} + +global_ctx.lists["self.css_global_value"] = ["initial", "inherit", "unset", "revert"] + + +operators = Operators( + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_AND=" and ", + MATH_OR=" or ", + MATH_GREATER_THAN=" > ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN=" < ", + MATH_LESS_THAN_OR_EQUAL=" <= ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_insert_function(text: str, selection: str): + substitutions = {"1": text} + if selection: + substitutions["0"] = selection + actions.user.insert_snippet_by_name("functionCall", substitutions) diff --git a/community/lang/css/css.talon b/community/lang/css/css.talon new file mode 100644 index 0000000..d6b8352 --- /dev/null +++ b/community/lang/css/css.talon @@ -0,0 +1,45 @@ +code.language: css +code.language: scss +- + +tag(): user.code_comment_block_c_like +tag(): user.code_functions_common +tag(): user.code_libraries +tag(): user.code_operators_math + +settings(): + user.code_public_variable_formatter = "DASH_SEPARATED" + +block: user.code_block() + +attribute []: + name = user.formatted_text(text or "", "DASH_SEPARATED") + user.insert_between("[{name}", "]") + +prop : + name = user.formatted_text(text, "DASH_SEPARATED") + user.insert_between("{name}: ", ";") + +# for media/supports queries, or if you don't like prop +rule : + name = user.formatted_text(text, "DASH_SEPARATED") + insert("{name}: ") + +value [{user.css_unit}]: "{number_string}{css_unit or ''}" +value point [{user.css_unit}]: + "{number_string}.{digit_string}{css_unit or ''}" + +(value | state) {user.css_global_value}: "{css_global_value}" +value : user.insert_formatted(text, "DASH_SEPARATED") + +variable : + name = user.formatted_text(text, "DASH_SEPARATED") + insert("var(--{name})") + +op var: user.insert_between("var(--", ")") + +at {user.css_at_rule}: "@{css_at_rule} " +unit {user.css_unit}: insert(css_unit) + +[value] current color: "currentColor" +op important: " !important" diff --git a/community/lang/elixir/elixir.py b/community/lang/elixir/elixir.py new file mode 100644 index 0000000..dfdc5fc --- /dev/null +++ b/community/lang/elixir/elixir.py @@ -0,0 +1,129 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +mod = Module() +ctx.matches = r""" +code.language: elixir +""" + +# Elixir keywords and constructs +ctx.lists["user.code_keyword"] = { + "def": "def ", + "def p": "defp ", + "def module": "defmodule ", + "do": "do ", + "end": "end", + "if": "if ", + "else": "else ", + "cond": "cond ", + "case": "case ", + "when": "when ", + "f n": "fn ", + "receive": "receive ", + "after": "after ", + "try": "try ", + "catch": "catch ", + "rescue": "rescue ", + "raise": "raise ", + "with": "with ", + "unless": "unless ", + "import": "import ", + "alias": "alias ", + "require": "require ", + "use": "use ", +} + +operators = Operators( + LAMBDA="->", + ASSIGNMENT=" = ", + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_EQUAL=" === ", + MATH_NOT_EQUAL=" !== ", + MATH_WEAK_EQUAL=" == ", + MATH_WEAK_NOT_EQUAL=" != ", + MATH_WEAK_AND=" && ", + MATH_WEAK_OR=" || ", + MATH_WEAK_NOT="!", + MATH_GREATER_THAN=" > ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN=" < ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_AND=" and ", + MATH_OR=" or ", + MATH_NOT="not ", + MATH_IN=" in ", + MATH_NOT_IN=" not in ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_self(): + actions.auto_insert("self") + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + 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_else_if(): + actions.user.insert_between("else if ", " do\nend") + + def code_insert_function(text: str, selection: str): + text += f"({selection or ''})" + actions.user.paste(text) + actions.edit.left() + + def code_default_function(text: str): + result = "def {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + actions.user.code_default_function(text) + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "defp {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + actions.user.code_insert_function(result, None) + + def code_import_module(text: str): + actions.auto_insert("import ") + actions.insert(text) + + def code_alias_module(text: str): + actions.auto_insert("alias ") + actions.insert(text) + + def code_require_module(text: str): + actions.auto_insert("require ") + actions.insert(text) + + def code_use_module(text: str): + actions.auto_insert("use ") + actions.insert(text) diff --git a/community/lang/elixir/elixir.talon b/community/lang/elixir/elixir.talon new file mode 100644 index 0000000..31927ef --- /dev/null +++ b/community/lang/elixir/elixir.talon @@ -0,0 +1,40 @@ +code.language: elixir +- +tag(): user.code_functional +tag(): user.code_concurrent + +tag(): user.code_comment_line +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_functions +tag(): user.code_keywords +tag(): user.code_libraries +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_math +tag(): user.code_operators_lambda + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +# Elixir-specific grammars +state def: "def " +state defp: "defp " +state if: "if " +state else: "else" +state case: "case " +state cond: "cond do" +state try: "try do" +state rescue: "rescue" +state after: "after" +state end: "end" + +op pipe: " |> " + +# Elixir-specific keywords and symbols +[state] raise {user.elixir_exception}: user.insert_between("raise ", "") + +[state] rescue {user.elixir_exception}: "rescue {elixir_exception}" diff --git a/community/lang/go/code_common_function.talon-list b/community/lang/go/code_common_function.talon-list new file mode 100644 index 0000000..aae1db2 --- /dev/null +++ b/community/lang/go/code_common_function.talon-list @@ -0,0 +1,26 @@ +list: user.code_common_function +code.language: go +- + +# golang builtin functions +append +length: len +make +# formatting +format print: fmt.Printf +format print eff: fmt.Printf +format sprint: fmt.Sprintf +format es print eff: 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 diff --git a/community/lang/go/go.py b/community/lang/go/go.py new file mode 100644 index 0000000..9c3a1d5 --- /dev/null +++ b/community/lang/go/go.py @@ -0,0 +1,132 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +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", +} + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + 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_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EQUAL=" == ", + MATH_NOT_EQUAL=" != ", + MATH_OR=" || ", + MATH_AND=" && ", + MATH_EXPONENT=" ^ ", + MATH_GREATER_THAN=" > ", + MATH_LESS_THAN=" < ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + # code_operators_pointer + POINTER_ADDRESS_OF="&", + POINTER_INDIRECTION="*", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + 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_insert_true(): + actions.insert("true") + + def code_insert_false(): + actions.insert("false") + + 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/community/lang/go/go.talon b/community/lang/go/go.talon new file mode 100644 index 0000000..1b5d668 --- /dev/null +++ b/community/lang/go/go.talon @@ -0,0 +1,38 @@ +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_functions_common +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") diff --git a/community/lang/html/html.talon b/community/lang/html/html.talon new file mode 100644 index 0000000..d3096fb --- /dev/null +++ b/community/lang/html/html.talon @@ -0,0 +1,4 @@ +code.language: html +code.language: javascriptreact +code.language: typescriptreact +- diff --git a/community/lang/java/code_keyword.talon-list b/community/lang/java/code_keyword.talon-list new file mode 100644 index 0000000..b1fa5b4 --- /dev/null +++ b/community/lang/java/code_keyword.talon-list @@ -0,0 +1,64 @@ +list: user.code_keyword +code.language: java +- + +abstract: "abstract " +assert: "assert " +boolean: "boolean " +break +byte: "byte " +case: "case " +catch +char: "char " +class: "class " +continue +default +do: "do " +double: "double " +else +enum: "enum " +exports: "exports " +extends: " extends " +final: "final " +finally: "finally " +float: "float " +for: "for " +if: "if " +implements: " implements " +import: "import " +instance of: "instanceof " +int: "int " +interface: "interface " +long: "long " +module: "module " +native: "native " +new: "new " +open: "open " +opens: "opens " +package: "package " +private: "private " +protected: "protected " +provides: "provides " +public: "public " +requires: "requires " +return: "return " +short: "short " +static: "static " +strict eff pee: "strictfp " +super +switch: "switch " +synchronized: "synchronized " +this +throw: "throw " +throws: "throws " +to: " to " +transient: "transient " +transitive: "transitive " +try: "try " +uses: "uses " +var: "var " +void: "void " +volatile: "volatile " +while: "while " +with: " with " +yield: "yield " diff --git a/community/lang/java/java.py b/community/lang/java/java.py new file mode 100644 index 0000000..667a962 --- /dev/null +++ b/community/lang/java/java.py @@ -0,0 +1,218 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +mod = Module() +ctx.matches = r""" +code.language: java +""" + +# Primitive Types +java_primitive_types = { + "boolean": "boolean", + "int": "int", + "float": "float", + "byte": "byte", + "double": "double", + "short": "short", + "long": "long", + "char": "char", + "void": "void", +} + +# Java Boxed Types +java_boxed_types = { + "Byte": "Byte", + "Integer": "Integer", + "Double": "Double", + "Short": "Short", + "Float": "Float", + "Long": "Long", + "Boolean": "Boolean", + "Character": "Character", + "Void": "Void", +} + +mod.list("java_boxed_type", desc="Java Boxed Types") +ctx.lists["self.java_boxed_type"] = java_boxed_types + +# Common Classes +java_common_classes = { + "Object": "Object", + "string": "String", + "thread": "Thread", + "exception": "Exception", +} + +mod.list("java_common_class", desc="Java Common Classes") +ctx.lists["self.java_common_class"] = java_common_classes + + +# Java Generic Data Structures +java_generic_data_structures = { + # Interfaces + "set": "Set", + "list": "List", + "queue": "Queue", + "deque": "Deque", + "map": "Map", + # Classes + "hash set": "HashSet", + "array list": "ArrayList", + "hash map": "HashMap", +} + +unboxed_types = java_primitive_types.copy() +unboxed_types.update(java_common_classes) +unboxed_types.update(java_generic_data_structures) + +ctx.lists["user.code_type"] = unboxed_types + +mod.list("java_generic_data_structure", desc="Java Generic Data Structures") +ctx.lists["self.java_generic_data_structure"] = java_generic_data_structures + +# Java Modifies +java_modifiers = { + "public": "public", + "private": "private", + "protected": "protected", + "static": "static", + "synchronized": "synchronized", + "volatile": "volatile", + "transient": "transient", + "abstract": "abstract", + "interface": "interface", + "final": "final", +} + +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_get_operators() -> Operators: + return operators + + def code_self(): + actions.insert("this") + + def code_operator_object_accessor(): + actions.insert(".") + + def code_insert_null(): + actions.insert("null") + + def code_insert_is_null(): + actions.insert(" == null") + + def code_insert_is_not_null(): + actions.insert(" != null") + + def code_insert_true(): + actions.insert("true") + + def code_insert_false(): + actions.insert("false") + + 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 = "private void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_private_static_function(text: str): + """Inserts private static function""" + result = "private static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_protected_function(text: str): + result = "void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_protected_static_function(text: str): + result = "static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + result = "public void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_public_static_function(text: str): + result = "public static void {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) diff --git a/community/lang/java/java.talon b/community/lang/java/java.talon new file mode 100644 index 0000000..6f76738 --- /dev/null +++ b/community/lang/java/java.talon @@ -0,0 +1,44 @@ +code.language: java +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +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_keywords + +settings(): + user.code_private_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_function_formatter = "PRIVATE_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" + +state var: "var " + +# Types Commands +boxed [type] {user.java_boxed_type}: insert(user.java_boxed_type + " ") + +generic [type] {user.java_generic_data_structure}: + user.insert_between(java_generic_data_structure + "<", ">") + +# Arrays +type {user.code_type} array: + insert(user.code_type) + user.code_operator_subscript() + +[state] {user.java_modifier}: insert(user.java_modifier + " ") + +op array: user.code_operator_subscript() + +op new: insert("new ") diff --git a/community/lang/javascript/code_common_function.talon-list b/community/lang/javascript/code_common_function.talon-list new file mode 100644 index 0000000..7213835 --- /dev/null +++ b/community/lang/javascript/code_common_function.talon-list @@ -0,0 +1,19 @@ +list: user.code_common_function +code.language: javascript +code.language: typescript +code.language: javascriptreact +code.language: typescriptreact +- + +abs: Math.abs +entries: Object.entries +fetch: fetch +floor: Math.floor +from entries: Object.fromEntries +keys: Object.keys +log: console.log +max: Math.max +min: Math.min +print: console.log +round: Math.round +values: Object.values diff --git a/community/lang/javascript/javascript.py b/community/lang/javascript/javascript.py new file mode 100644 index 0000000..9f9c0d5 --- /dev/null +++ b/community/lang/javascript/javascript.py @@ -0,0 +1,203 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +mod = Module() +ctx = Context() +ctx.matches = r""" +code.language: javascript +code.language: typescript +code.language: javascriptreact +code.language: typescriptreact +""" + +mod.list("code_common_member_function", "Function to use in a dotted chain, eg .foo()") + +ctx.lists["user.code_common_member_function"] = { + "catch": "catch", + "concat": "concat", + "filter": "filter", + "finally": "finally", + "find": "find", + "flat map": "flatMap", + "for each": "forEach", + "join": "join", + "includes": "includes", + "map": "map", + "pop": "pop", + "push": "push", + "reduce": "reduce", + "slice": "slice", + "some": "some", + "split": "split", + "substring": "substring", + "then": "then", +} + +ctx.lists["user.code_keyword"] = { + "a sink": "async ", + "await": "await ", + "break": "break", + "class": "class ", + "const": "const ", + "continue": "continue", + "default": "default ", + "export": "export ", + "false": "false", + "function": "function ", + "import": "import ", + "let": "let ", + "new": "new ", + "null": "null", + "private": "private ", + "protected": "protected ", + "public": "public ", + "return": "return ", + "throw": "throw ", + "true": "true", + "try": "try ", + "undefined": "undefined", + "yield": "yield ", +} + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_OR=" ||= ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + 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_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EXPONENT=" ** ", + MATH_EQUAL=" === ", + MATH_NOT_EQUAL=" !== ", + MATH_OR=" || ", + MATH_AND=" && ", + MATH_GREATER_THAN=" > ", + MATH_LESS_THAN=" < ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_WEAK_EQUAL=" == ", + MATH_WEAK_NOT_EQUAL=" != ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_insert_is_not_null(): + actions.auto_insert(" !== null") + + def code_insert_is_null(): + actions.auto_insert(" === null") + + def code_self(): + actions.auto_insert("this") + + def code_operator_object_accessor(): + actions.auto_insert(".") + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + def code_insert_null(): + actions.auto_insert("null") + + def code_insert_function(text: str, selection: str): + text += f"({selection or ''})" + actions.user.paste(text) + actions.edit.left() + + def code_default_function(text: str): + """Inserts function declaration without modifiers""" + result = "function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + # def code_private_static_function(text: str): + # """Inserts private static function""" + # result = "private static void {}".format( + # actions.user.formatted_text( + # text, settings.get("user.code_private_function_formatter") + # ) + # ) + + # actions.user.code_insert_function(result, None) + + def code_protected_function(text: str): + result = "function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + # def code_protected_static_function(text: str): + # result = "protected static void {}".format( + # actions.user.formatted_text( + # text, settings.get("user.code_protected_function_formatter") + # ) + # ) + + # actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + result = "function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + # def code_public_static_function(text: str): + # result = "public static void {}".format( + # actions.user.formatted_text( + # text, settings.get("user.code_public_function_formatter") + # ) + # ) + + # actions.user.code_insert_function(result, None) diff --git a/community/lang/javascript/javascript.talon b/community/lang/javascript/javascript.talon new file mode 100644 index 0000000..db88e68 --- /dev/null +++ b/community/lang/javascript/javascript.talon @@ -0,0 +1,62 @@ +code.language: javascript +code.language: typescript +code.language: javascriptreact +code.language: typescriptreact +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +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_functions_common +tag(): user.code_keywords +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 + +settings(): + user.code_private_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_function_formatter = "PRIVATE_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" + +(op | is) strict equal: + user.deprecate_command("2025-03-4", "(op | is) strict equal", "is equal") + user.code_operator("MATH_EQUAL") + +(op | is) strict not equal: + user.deprecate_command("2025-03-4", "(op | is) strict not equal", "is not equal") + user.code_operator("MATH_NOT_EQUAL") + +op null else: " ?? " + +state const: "const " + +state let: "let " + +state var: "var " + +state export: "export " + +state async: "async " + +state await: "await " + +dot {user.code_common_member_function}: + user.insert_between(".{code_common_member_function}(", ")") + +state map: app.notify('ERROR: Command deprecated; please use "dot map"') +state filter: app.notify('ERROR: Command deprecated; please use "dot filter"') +state reduce: app.notify('ERROR: Command deprecated; please use "dot reduce"') + +state spread: "..." + +from import: user.insert_between(' from "', '"') diff --git a/community/lang/kotlin/kotlin.py b/community/lang/kotlin/kotlin.py new file mode 100644 index 0000000..576ccd2 --- /dev/null +++ b/community/lang/kotlin/kotlin.py @@ -0,0 +1,125 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +mod = Module() +ctx.matches = r""" +code.language: kotlin +""" + +ctx.lists["user.code_keyword"] = { + "var": "var ", + "val": "val ", + "lateinit": "lateinit ", + "public": "public ", + "private": "private ", + "protected": "protected ", + "companion object": "companion object ", + "synchronized": "synchronized ", + "volatile": "volatile ", + "transient": "transient ", + "abstract": "abstract ", + "interface": "interface ", + "final": "final ", + "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", +} + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + 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_bitwise + 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_get_operators() -> Operators: + return operators + + def code_self(): + actions.auto_insert("this") + + def code_insert_null(): + actions.insert("null") + + def code_insert_is_null(): + actions.insert(" == null") + + def code_insert_is_not_null(): + actions.insert(" != null") + + def code_insert_function(text: str, selection: str): + text += f"({selection or ''})" + actions.user.paste(text) + actions.edit.left() + + def code_default_function(text: str): + result = "fun {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + actions.user.code_default_function(text) + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "private fun {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + actions.user.code_insert_function(result, None) diff --git a/community/lang/kotlin/kotlin.talon b/community/lang/kotlin/kotlin.talon new file mode 100644 index 0000000..fb5601a --- /dev/null +++ b/community/lang/kotlin/kotlin.talon @@ -0,0 +1,25 @@ +code.language: kotlin +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +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_keywords + +settings(): + user.code_private_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_function_formatter = "PRIVATE_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" diff --git a/community/lang/lua/code_common_function.talon-list b/community/lang/lua/code_common_function.talon-list new file mode 100644 index 0000000..4f457d7 --- /dev/null +++ b/community/lang/lua/code_common_function.talon-list @@ -0,0 +1,47 @@ +list: user.code_common_function +code.language: lua +- + +to number: "tonumber" +I pairs: "ipairs" +print: "print" +print F: "printf" +type: "type" +assert: "assert" +get meta table: "getmetatable" +set meta table: "setmetatable" +# io +I O write: "io.write" +I O read: "io.read" +I O open: "io.open" +# string +format: "string.format" +string G find: "string.gfind" +string find: "string.strfind" +string len: "string.strlen" +string upper: "string.strupper" +string lower: "string.strlower" +string sub: "string.strsub" +string G sub: "string.gsub" +string match: "string.match" +string G match: "string.gmatch" +# table +table unpack: "table.unpack" +table insert: "table.insert" +tabel get N: "table.getn" +tabel sort: "table.sort" +# math +math max: "math.max" +# json +jason parse: "json.parse" +# http +H T T P get: "http.get" +web get: "http.get" +# os +O S date: "os.date" +O S time: "os.time" +O S clock: "os.clock" +O S rename: "os.rename" +O S remove: "os.remove" +O S getenv: "os.getenv" +O S execute: "os.execute" diff --git a/community/lang/lua/lua.py b/community/lang/lua/lua.py new file mode 100644 index 0000000..8f6f0e2 --- /dev/null +++ b/community/lang/lua/lua.py @@ -0,0 +1,197 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +mod = Module() +ctx = Context() +ctx.matches = r""" +code.language: lua +""" + +mod.setting( + "lua_version", + type=float, + default=5.1, + desc="The default lua version to use. Dictates certain operators", +) +mod.tag("stylua", desc="Tag for stylua linting commands") + +ctx.lists["user.code_libraries"] = { + "bit": "bit", + "I O": "io", + "string": "string", + "U T F eight": "utf8", + "table": "table", + "math": "math", + "O S": "os", + "debug": "debug", + "L F S": "lfs", + "socket": "socket", + "H T T P": "http", + "web": "http", + "jason": "json", +} + + +@mod.capture(rule="{self.lua_functions}") +def lua_functions(m) -> str: + "Returns a string" + return m.lua_functions + + +### +# code_operators_bitwise +### + + +# NOTE: < 5.3 assumes Lua BitOp usage +# > 5.2 assumes native bitwise operators +# TODO: Possibly add settings to define which library to use, as 5.2 +# includes bit32. Neovim uses luajit, which uses Lua BitOp +def code_operator_bitwise_and(): + if settings.get("user.lua_version") > 5.2: + actions.insert(" & ") + else: + actions.insert(" bit.band() ") + + +def code_operator_bitwise_or(): + if settings.get("user.lua_version") > 5.2: + actions.insert(" | ") + else: + actions.insert(" bit.bor() ") + + +def code_operator_bitwise_exclusive_or(): + if settings.get("user.lua_version") > 5.2: + actions.insert(" ~ ") + else: + actions.insert(" bit.xor() ") + + +def code_operator_bitwise_left_shift(): + if settings.get("user.lua_version") > 5.2: + actions.insert(" << ") + else: + actions.insert(" bit.lshift() ") + + +def code_operator_bitwise_right_shift(): + if settings.get("user.lua_version") > 5.2: + actions.insert(" >> ") + else: + actions.insert(" bit.rshift() ") + + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + # code_operators_bitwise + BITWISE_AND=code_operator_bitwise_and, + BITWISE_OR=code_operator_bitwise_or, + BITWISE_EXCLUSIVE_OR=code_operator_bitwise_exclusive_or, + BITWISE_LEFT_SHIFT=code_operator_bitwise_left_shift, + BITWISE_RIGHT_SHIFT=code_operator_bitwise_right_shift, + # code_operators_assignment + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_INTEGER_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=" and ", + MATH_OR=" or ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + # tag-related actions listed first, indicated by comment. corresponds to + # the tag(): user.code_imperative style declaration in the language .talon + # file + + ## + # code_comment_block + ## + def code_comment_block_prefix(): + actions.insert("--[[") + + def code_comment_block_suffix(): + actions.insert("--]]") + + ## + # code_data_bool + ## + def code_insert_true(): + actions.insert("true") + + def code_insert_false(): + actions.insert("false") + + ## + # code_data_null + ## + def code_insert_null(): + actions.insert("nil") + + def code_insert_is_null(): + actions.insert(" == nil") + + def code_insert_is_not_null(): + actions.insert(" ~= nil") + + ## + # code_functions + ## + def code_private_function(text: str): + """Inserts private function declaration""" + result = "local function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.insert("\n\nend") + actions.key("up:2") + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + result = "function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.insert("\n\nend") + actions.key("up:2") + actions.user.code_insert_function(result, None) + + def code_insert_function(text: str, selection: str): + if selection: + text = text + f"({selection})" + else: + text = text + "()" + + actions.user.paste(text) + actions.edit.left() + + ## + # code_libraries + ## + def code_insert_library(text: str, selection: str): + substitutions = {"1": selection, "0": selection} + actions.user.insert_snippet_by_name("importStatement", substitutions) + + # non-tag related actions diff --git a/community/lang/lua/lua.talon b/community/lang/lua/lua.talon new file mode 100644 index 0000000..4276d0b --- /dev/null +++ b/community/lang/lua/lua.talon @@ -0,0 +1,67 @@ +code.language: lua +- + +tag(): user.code_imperative + +tag(): user.code_comment_line +tag(): user.code_comment_block +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_functions +tag(): user.code_functions_common +tag(): user.code_libraries +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_bitwise +tag(): user.code_operators_math + +# Use this tag if you use the stylua linter +#tag(): user.stylua + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +state local: "local" +state end: "end" +state then: "then" +state repeat: "repeat" +state until: "until" +state return (null | nil): "return nil" +state return true: "return true" +state return false: "return false" +state return table: user.insert_between("return {", "}") +state append string: " .. " + +state label : + insert("::") + user.insert_formatted(text, "SNAKE_CASE") + insert("::") + +require : + user.code_insert_library("", code_libraries) + key(end enter) + +state (variable | var) [] [over]: user.code_public_variable_formatter(text) + +state local (variable | var) [] [over]: + insert("local ") + user.code_private_variable_formatter(text) + +# for built in object methods, ex: foo:gsub() +method : + insert(":") + user.code_public_function_formatter(text) + insert("()") + edit.left() + +self dot: "self." + +index : '["{word}"]' +index (var | variable) : + var = user.formatted_text(text, "SNAKE_CASE") + insert("[{var}]") + +state return dick: user.insert_between("return {", "}") diff --git a/community/lang/lua/stylua.talon b/community/lang/lua/stylua.talon new file mode 100644 index 0000000..4dae9a4 --- /dev/null +++ b/community/lang/lua/stylua.talon @@ -0,0 +1,6 @@ +tag: user.stylua +- + +lint ignore: "-- stylua: ignore" +lint ignore start: "-- stylua: ignore start" +lint ignore end: "-- stylua: ignore end" diff --git a/community/lang/markdown/markdown.py b/community/lang/markdown/markdown.py new file mode 100644 index 0000000..b9f8149 --- /dev/null +++ b/community/lang/markdown/markdown.py @@ -0,0 +1,21 @@ +from talon import Context, Module + +mod = Module() +ctx = Context() + +ctx.matches = r""" +code.language: markdown +""" + +mod.list("markdown_code_block_language", desc="Languages for code blocks") +ctx.lists["user.markdown_code_block_language"] = { + "typescript": "typescript", + "python": "python", + "code": "", + "ruby": "ruby", + "shell": "shell", + "bash": "bash", + "json": "json", + "are": "r", + "markdown": "markdown", +} diff --git a/community/lang/markdown/markdown.talon b/community/lang/markdown/markdown.talon new file mode 100644 index 0000000..7f614ba --- /dev/null +++ b/community/lang/markdown/markdown.talon @@ -0,0 +1,44 @@ +code.language: markdown +- +(level | heading | header) one: + edit.line_start() + "# " +(level | heading | header) two: + edit.line_start() + "## " +(level | heading | header) three: + edit.line_start() + "### " +(level | heading | header) four: + edit.line_start() + "#### " +(level | heading | header) five: + edit.line_start() + "##### " +(level | heading | header) six: + edit.line_start() + "###### " + +list [one]: + edit.line_start() + "- " +list two: + edit.line_start() + " - " +list three: + edit.line_start() + " - " +list four: + edit.line_start() + " - " +list five: + edit.line_start() + " - " +list six: + edit.line_start() + " - " + +{user.markdown_code_block_language} block: + user.insert_snippet("```{markdown_code_block_language}\n$0\n```") + +link: user.insert_snippet_by_name("link") diff --git a/community/lang/php/php.py b/community/lang/php/php.py new file mode 100644 index 0000000..1772ab3 --- /dev/null +++ b/community/lang/php/php.py @@ -0,0 +1,160 @@ +from talon import Context, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +ctx.matches = r""" +code.language: php +""" + +ctx.lists["user.code_type"] = { + "int": "int", + "float": "float", + "string": "string", + "bool": "bool", + "array": "array", + "null": "null", + "void": "void", +} + +operators = Operators( + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + # code_operators_bitwise + BITWISE_AND=" & ", + BITWISE_OR=" | ", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_RIGHT_SHIFT=" >> ", + BITWISE_NOT="~", + # code_operators_math + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EXPONENT=" ** ", + MATH_EQUAL=" === ", + MATH_NOT_EQUAL=" !== ", + MATH_WEAK_EQUAL=" == ", + MATH_WEAK_NOT_EQUAL=" != ", + MATH_GREATER_THAN=" > ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN=" < ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_NOT="!", + MATH_AND=" && ", + MATH_OR=" || ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_self(): + actions.auto_insert("$this") + + def code_operator_object_accessor(): + actions.auto_insert("->") + + def code_comment_block_prefix(): + actions.auto_insert("/*") + + def code_comment_block_suffix(): + actions.auto_insert("*/") + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + def code_insert_null(): + actions.auto_insert("null") + + def code_insert_is_null(): + actions.auto_insert("is_null()") + actions.edit.left() + + def code_insert_is_not_null(): + actions.auto_insert("isset()") + actions.edit.left() + + def code_default_function(text: str): + actions.user.code_public_function(text) + + def code_protected_function(text: str): + """Inserts protected function declaration""" + result = "protected function {}()".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + + def code_public_function(text: str): + """Inserts public function declaration""" + result = "public function {}()".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "private function {}()".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + + def code_private_static_function(text: str): + """Inserts private static function declaration""" + result = "private static function {}()".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + + def code_protected_static_function(text: str): + """Inserts protected static function declaration""" + result = "protected static function {}()".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + + def code_public_static_function(text: str): + """Inserts public static function declaration""" + result = "public static function {}()".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + + def code_insert_return_type(type: str): + actions.insert(f": {type}") diff --git a/community/lang/php/php.talon b/community/lang/php/php.talon new file mode 100644 index 0000000..52d2f2c --- /dev/null +++ b/community/lang/php/php.talon @@ -0,0 +1,36 @@ +code.language: php +- +tag(): user.code_imperative +tag(): user.code_object_oriented +tag(): user.code_libraries + +tag(): user.code_comment_line +tag(): user.code_comment_block +tag(): user.code_comment_documentation +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_operators_assignment +tag(): user.code_operators_math +tag(): user.code_functions + +settings(): + user.code_private_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_function_formatter = "PRIVATE_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" + +(op | is) loosely equal: + user.deprecate_command("2025-03-20", "(op | is) loosely equal", "is weak equal") + insert(" == ") +(op | is) loosely not equal: + user.deprecate_command("2025-03-20", "(op | is) loosely not equal", "is weak not equal") + insert(" != ") + +state try: user.insert_snippet_by_name("tryStatement") +state catch: user.insert_snippet_by_name("catchStatement") + +var [over]: + insert("$") + insert(user.formatted_text(phrase, "PRIVATE_CAMEL_CASE")) diff --git a/community/lang/proto/proto.py b/community/lang/proto/proto.py new file mode 100644 index 0000000..a193efe --- /dev/null +++ b/community/lang/proto/proto.py @@ -0,0 +1,26 @@ +from talon import Context, Module + +mod = Module() + +ctx = Context() +ctx.matches = r""" +code.language: protobuf +""" + +ctx.lists["user.code_type"] = { + "string": "string", + "bytes": "bytes", + "you sixty four": "uint64", + "you thirty two": "uint32", + "eye sixty four": "int64", + "eye thirty two": "int32", + "sin sixty four": "sint64", + "sin thirty two": "sint32", + "fixed sixty four": "fixed64", + "fixed thirty two": "fixed32", + "as fixed sixty four": "sfixed64", + "as fixed thirty two": "sfixed32", + "boolean": "bool", + "double": "double", + "float": "float", +} diff --git a/community/lang/proto/proto.talon b/community/lang/proto/proto.talon new file mode 100644 index 0000000..c5eb2d0 --- /dev/null +++ b/community/lang/proto/proto.talon @@ -0,0 +1,18 @@ +code.language: protobuf +- + +# this is pretty bare-bones, further contributions welcome +block: user.code_block() + +state message: "message " +state package: "package " +state reserved: "reserved " +state enum: "enum " +op equals: " = " +state import: "import " +state import public: "import public " +state option: "option " +state repeated: "repeated " + +type {user.code_type}: "{code_type}" +repeated type {user.code_type}: "repeated {code_type}" diff --git a/community/lang/python/code_common_function.talon-list b/community/lang/python/code_common_function.talon-list new file mode 100644 index 0000000..398c546 --- /dev/null +++ b/community/lang/python/code_common_function.talon-list @@ -0,0 +1,14 @@ +list: user.code_common_function +code.language: python +- + +enumerate +integer: "int" +length: "len" +list +print +range +set +split +string: "str" +update diff --git a/community/lang/python/code_keyword.talon-list b/community/lang/python/code_keyword.talon-list new file mode 100644 index 0000000..0f94b41 --- /dev/null +++ b/community/lang/python/code_keyword.talon-list @@ -0,0 +1,41 @@ +list: user.code_keyword +code.language: python +- + +and: " and " +as: " as " +assert: "assert " +async: "async " +await: "await " +break: "break" +case: "case " +class: "class " +deaf: "def " +define: "def " +dell: "del " +elif: "elif " +else: "else " +except: "except " +false: "False" +for: "for " +from: "from " +global: "global " +if: "if " +import: "import " +in: " in " +is: " is " +lambda: "lambda " +match: "match " +none: "None" +not: "not " +null: "None" +or: " or " +pass: "pass" +raise: "raise " +return: "return " +true: "True" +try: "try" +type: "type" +while: "while " +with: "with " +yield: "yield " diff --git a/community/lang/python/code_keyword_bare.talon-list b/community/lang/python/code_keyword_bare.talon-list new file mode 100644 index 0000000..e4e21ee --- /dev/null +++ b/community/lang/python/code_keyword_bare.talon-list @@ -0,0 +1,7 @@ +list: user.code_keyword_unprefixed +code.language: python +- + +continue: "continue" +finally: "finally" +nonlocal: "nonlocal " diff --git a/community/lang/python/python.py b/community/lang/python/python.py new file mode 100644 index 0000000..9ff3907 --- /dev/null +++ b/community/lang/python/python.py @@ -0,0 +1,231 @@ +import re + +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +mod = Module() +ctx = Context() +ctx.matches = r""" +code.language: python +""" + +"""a set of fields used in python docstrings that will follow the +reStructuredText format""" +docstring_fields = { + "class": ":class:", + "function": ":func:", + "parameter": ":param:", + "raise": ":raise:", + "returns": ":return:", + "type": ":type:", + "return type": ":rtype:", + # these are sphinx-specific + "see also": ".. seealso:: ", + "notes": ".. notes:: ", + "warning": ".. warning:: ", + "todo": ".. todo:: ", +} + +mod.list("python_docstring_fields", desc="python docstring fields") +ctx.lists["user.python_docstring_fields"] = docstring_fields + +ctx.lists["user.code_type"] = { + "boolean": "bool", + "integer": "int", + "string": "str", + "none": "None", + "dick": "Dict", + "float": "float", + "any": "Any", + "tuple": "Tuple", + "union": "UnionAny", + "iterable": "Iterable", + "vector": "Vector", + "bytes": "bytes", + "sequence": "Sequence", + "callable": "Callable", + "list": "List", + "no return": "NoReturn", +} + +exception_list = [ + "BaseException", + "SystemExit", + "KeyboardInterrupt", + "GeneratorExit", + "Exception", + "StopIteration", + "StopAsyncIteration", + "ArithmeticError", + "FloatingPointError", + "OverflowError", + "ZeroDivisionError", + "AssertionError", + "AttributeError", + "BufferError", + "EOFError", + "ImportError", + "ModuleNotFoundError", + "LookupError", + "IndexError", + "KeyError", + "MemoryError", + "NameError", + "UnboundLocalError", + "OSError", + "BlockingIOError", + "ChildProcessError", + "ConnectionError", + "BrokenPipeError", + "ConnectionAbortedError", + "ConnectionRefusedError", + "ConnectionResetError", + "FileExistsError", + "FileNotFoundError", + "InterruptedError", + "IsADirectoryError", + "NotADirectoryError", + "PermissionError", + "ProcessLookupError", + "TimeoutError", + "ReferenceError", + "RuntimeError", + "NotImplementedError", + "RecursionError", + "SyntaxError", + "IndentationError", + "TabError", + "SystemError", + "TypeError", + "ValueError", + "UnicodeError", + "UnicodeDecodeError", + "UnicodeEncodeError", + "UnicodeTranslateError", + "Warning", + "DeprecationWarning", + "PendingDeprecationWarning", + "RuntimeWarning", + "SyntaxWarning", + "UserWarning", + "FutureWarning", + "ImportWarning", + "UnicodeWarning", + "BytesWarning", + "ResourceWarning", +] +mod.list("python_exception", desc="python exceptions") +ctx.lists["user.python_exception"] = { + " ".join(re.findall("[A-Z][^A-Z]*", exception)).lower(): exception + for exception in exception_list +} + +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=" += 1", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + # code_operators_bitwise + BITWISE_NOT="~", + BITWISE_AND=" & ", + BITWISE_OR=" | ", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_RIGHT_SHIFT=" >> ", + # code_operators_lambda + LAMBDA=lambda: actions.user.insert_between("lambda ", ": "), + # code_operators_math + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_INTEGER_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=" and ", + MATH_OR=" or ", + MATH_NOT=" not ", + MATH_IN=" in ", + MATH_NOT_IN=" not in ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_self(): + actions.auto_insert("self") + + def code_operator_object_accessor(): + actions.auto_insert(".") + + def code_insert_null(): + actions.auto_insert("None") + + def code_insert_is_null(): + actions.auto_insert(" is None") + + def code_insert_is_not_null(): + actions.auto_insert(" is not None") + + def code_insert_true(): + actions.auto_insert("True") + + def code_insert_false(): + actions.auto_insert("False") + + def code_insert_function(text: str, selection: str): + text += f"({selection or ''})" + actions.user.paste(text) + actions.edit.left() + + def code_default_function(text: str): + actions.user.code_public_function(text) + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "def _{}():".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.paste(result) + actions.edit.left() + actions.edit.left() + + def code_public_function(text: str): + result = "def {}():".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.left() + actions.edit.left() + + def code_insert_type_annotation(type: str): + actions.insert(f": {type}") + + def code_insert_return_type(type: str): + actions.insert(f" -> {type}") diff --git a/community/lang/python/python.talon b/community/lang/python/python.talon new file mode 100644 index 0000000..9d40492 --- /dev/null +++ b/community/lang/python/python.talon @@ -0,0 +1,53 @@ +code.language: python +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +tag(): user.code_comment_documentation +tag(): user.code_comment_line +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_functions +tag(): user.code_functions_common +tag(): user.code_keywords +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 + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_protected_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_protected_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +#python-specific grammars +dunder in it: "__init__" +state (def | deaf | deft): "def " +state try: "try:\n" +state except: "except " +state raise: "raise " +self taught: "self." +pie test: "pytest" +state past: "pass" + +[state] raise {user.python_exception}: + user.insert_between("raise {python_exception}(", ")") +[state] except {user.python_exception}: "except {python_exception}:" + +dock string: user.code_comment_documentation() +dock {user.python_docstring_fields}: + insert("{python_docstring_fields}") + edit.left() +dock type {user.code_type}: user.insert_between(":type ", ": {code_type}") +dock returns type {user.code_type}: user.insert_between(":rtype ", ": {code_type}") + +import : + user.code_insert_library(code_libraries, "") + key(end enter) + +from import: user.insert_snippet_by_name("importFromStatement") diff --git a/community/lang/r/code_common_function.talon-list b/community/lang/r/code_common_function.talon-list new file mode 100644 index 0000000..95217fe --- /dev/null +++ b/community/lang/r/code_common_function.talon-list @@ -0,0 +1,157 @@ +list: user.code_common_function +code.language: r +- + +# base R +as character: "as.character" +as data frame: "as.data.frame" +as date: "as.Date" +as double: "as.double" +as factor: "as.factor" +as integer: "as.integer" +as numeric: "as.numeric" +base read RDS: "readRDS" +base save RDS: "saveRDS" +cable: "kable" +correlation: "cor" +count: "count" +covariance: "cov" +describe: "describe" +eigen: "eigen" +ex table: "xtable" +get working directory: "getwd" +head: "head" +if else: "ifelse" +install packages: "install.packages" +is NA: "is.na" +is not NA: "!is.na" +length: "length" +library: "library" +list files: "list.files" +list: "list" +lm: "lm" +log: "log" +make directory: "dir.create" +margins: "margins" +max: "max" +mean: "mean" +min: "min" +names: "names" +paste: "paste0" +print: "print" +reorder: "reorder" +repeat: "rep" +scale: "scale" +sequence along: "seq_along" +sequence length: "seq_len" +sequence: "seq" +set working directory: "setwd" +sort: "sort" +subset: "subset" +sum: "sum" +summary: "summary" +tail: "tail" +tidy: "tidy" +trim white space: "trimws" +type: "typeof" +unique: "unique" +vector: "c" +vee table: "vtable" +view: "View" +# dplyr +anti join: "anti_join" +arrange: "arrange" +as tibble: "as_tibble" +bind rows: "bind_rows" +case when: "case_when" +distinct: "distinct" +everything: "everything" +filter: "filter" +full join: "full_join" +glimpse: "glimpse" +group by: "group_by" +inner join: "inner_join" +left join: "left_join" +mutate: "mutate" +pull: "pull" +rename all: "rename_all" +rename: "rename" +right join: "right_join" +select all: "select_all" +select: "select" +semi join: "semi_join" +starts with: "starts_with" +summarise: "summarise" +tibble: "tibble" +ungroup: "ungroup" +# ggplot2 +coord cartesian: "coor_cartesian" +element text: "element_text" +element blank: "element_blank" +facet grid: "facet_grid" +facet wrap: "facet_wrap" +geom A B line: "geom_abline" +geom area: "geom_area" +geom bar: "geom_bar" +geom boxplot: "geom_boxplot" +geom histogram: "geom_histogram" +geom horizontal line: "geom_hline" +geom line: "geom_line" +geom point: "geom_point" +geom pointrange: "geom_pointrange" +geom polygon: "geom_polygon" +geom ribbon: "geom_ribbon" +geom segment: "geom_segment" +geom smooth: "geom_smooth" +geom vertical line: "geom_vline" +geom violin: "geom_violin" +labs: "labs" +scale colour manual: "scale_colour_manual" +scale fill manual: "scale_fill_manual" +scale fill viridis: "scale_fill_viridis_c" +scale colour viridis: "scale_colour_viridis_c" +theme set: "theme_set" +# purrr +map character: "map_chr" +map data frame: "map_dfr" +map double: "map_dbl" +map: "map" +P map: "pmap" +# stringr +string contains: "str_detect" +string detect: "str_detect" +string replace all: "str_replace_all" +string replace: "str_replace" +# tidyr +drop NA: "drop_na" +gather: "gather" +nest: "nest" +pivot longer: "pivot_longer" +pivot wider: "pivot_wider" +spread: "spread" +un nest: "unnest" +# readr readxl and other non-base R reading/writing +read E views: "readEViews" +read CSV: "read_csv" +read RDS: "read_rds" +read excel: "read_xlsx" +write CSV: "write_csv" +write RDS: "write_rds" +# Shiny +shine ui: "shinyUI" +title panel: "titlePanel" +main panel: "mainPanel" +tab panel: "tabPanel" +navigation list panel: "navlistPanel" +conditional panel: "conditionalPanel" +input panel: "inputPanel" +ui output: "uiOutput" +text output: "textOutput" +table output: "tableOutput" +data table output: "dataTableOutput" +select size input: "selectizeInput" +action button: "actionButton" +download button: "downloadButton" +render ui: "renderUI" +observe event: "observeEvent" +# Base diff --git a/community/lang/r/r.py b/community/lang/r/r.py new file mode 100644 index 0000000..1e63af7 --- /dev/null +++ b/community/lang/r/r.py @@ -0,0 +1,151 @@ +from talon import Context, actions, settings + +from ..tags.operators import Operators + +ctx = Context() + +ctx.matches = r""" +code.language: r +""" + + +ctx.lists["user.code_libraries"] = { + "bayes plot": "bayesplot", + "BRMS": "brms", + "cable": "kable", + "car": "car", + "D plier": "dplyr", + "dev tools": "devtools", + "future": "future", + "furr": "furrr", + "gap minder": "gapminder", + "gee animate": "gganimate", + "gee highlight": "gghighlight", + "gee map": "ggmap", + "gee repel": "ggrepel", + "grid extra": "gridExtra", + "gee gee plot": "ggplot2", + "GLMM TMB": "glmmTMB", + "here": "here", + "knitter": "knitr", + "LME four": "lme4", + "LM test": "lmtest", + "lubridate": "lubridate", + "margins": "margins", + "inla": "INLA", + "NLME": "nlme", + "psych": "psych", + "purr": "purrr", + "R markdown": "rmarkdown", + "R stan": "rstan", + "R stan arm": "rstanarm", + "R color brewer": "RColorBrewer", + "read R": "readr", + "stargazer": "stargazer", + "tidy verse": "tidyverse", + "tidier": "tidyr", + "tidy bayes": "tidybayes", + "TMB": "TMB", + "vee table": "vtable", + "viridis": "viridis", + "viridis light": "viridisLite", + "shiny alert": "shinyalert", +} + +ctx.lists["user.code_parameter_name"] = { + "alpha": "alpha", + "breaks": "breaks", + "colour": "colour", + "data": "data", + "fill": "fill", + "H just": "hjust", + "keep": ".keep", + "label": "label", + "labels": "labels", + "log": "log", + "main": "main", + "mapping": "mapping", + "method": "method", + "NA remove": "na.rm", + "path": "path", + "position": "position", + "plex label": "xlab", + "plex limit": "xlim", + "scales": "scales", + "size": "size", + "show legend": "show.legend", + "sort": "sort", + "title": "title", + "type": "type", + "vee just": "vjust", + "width": "width", + "with ties": "with_ties", + "why label": "ylab", + "why limit": "ylim", + "why max": "ymax", + "why min": "ymin", +} + +operators = Operators( + # code_operators_assignment + ASSIGNMENT=" <- ", + # code_operators_bitwise + BITWISE_AND=" & ", + # code_operators_math: + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_EXPONENT=" ** ", + MATH_DIVIDE=" / ", + MATH_MODULO=" %% ", + 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_IN=" %in% ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_insert_null(): + actions.auto_insert("NULL") + + def code_insert_true(): + actions.auto_insert("TRUE") + + def code_insert_false(): + actions.auto_insert("FALSE") + + 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): + result = "{} <- function () {{\n\n}}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.paste(result) + actions.edit.up() + actions.edit.up() + actions.edit.line_end() + actions.edit.left() + actions.edit.left() + actions.edit.left() + + def code_insert_library(text: str, selection: str): + actions.user.insert_snippet_by_name("importStatement", {"0": text + selection}) + + def code_insert_named_argument(parameter_name: str): + actions.insert(f"{parameter_name} = ") diff --git a/community/lang/r/r.talon b/community/lang/r/r.talon new file mode 100644 index 0000000..6f37187 --- /dev/null +++ b/community/lang/r/r.talon @@ -0,0 +1,38 @@ +code.language: r +- +tag(): user.code_imperative + +tag(): user.code_comment_line +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_functions +tag(): user.code_functions_common +tag(): user.code_libraries +tag(): user.code_operators_assignment +tag(): user.code_operators_bitwise +tag(): user.code_operators_math + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_protected_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_protected_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +library : + user.code_insert_library(code_libraries, "") + key(end enter) + +# R specific commands +(chain | pipe that): + key(end) + " %>%" + key(enter) +state na: insert("NA") + +# TODO: migrate to function tag +^function define $: user.code_private_function(text) + +named arg {user.code_parameter_name}: + user.code_insert_named_argument(code_parameter_name) diff --git a/community/lang/ruby/ruby.py b/community/lang/ruby/ruby.py new file mode 100644 index 0000000..0c23f65 --- /dev/null +++ b/community/lang/ruby/ruby.py @@ -0,0 +1,95 @@ +from talon import Context, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +ctx.matches = r""" +code.language: ruby +""" +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_OR=" ||= ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT=" += 1", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + # code_operators_bitwise + BITWISE_AND=" & ", + BITWISE_OR=" | ", + BITWISE_NOT="~", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_RIGHT_SHIFT=" >> ", + # code_operators_lambda + LAMBDA="->", + # code_operators_math + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_EXPONENT=" ** ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EQUAL=" == ", + MATH_NOT_EQUAL=" != ", + MATH_GREATER_THAN=" > ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN=" < ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_NOT="!", + MATH_AND=" && ", + MATH_OR=" || ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_self(): + actions.auto_insert("self") + + def code_operator_object_accessor(): + actions.auto_insert(".") + + def code_insert_null(): + actions.auto_insert("nil") + + def code_insert_is_null(): + actions.auto_insert(".nil?") + + # Technically .present? is provided by Rails + def code_insert_is_not_null(): + actions.auto_insert(".present?") + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + def code_comment_documentation(): + actions.insert("##") + actions.key("enter") + actions.key("space") + ### Extra non-standard things + + def code_default_function(text: str): + """Inserts function definition""" + + result = "def {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + actions.user.paste(result) diff --git a/community/lang/ruby/ruby.talon b/community/lang/ruby/ruby.talon new file mode 100644 index 0000000..af8dcf0 --- /dev/null +++ b/community/lang/ruby/ruby.talon @@ -0,0 +1,38 @@ +code.language: ruby +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +tag(): user.code_comment_line +tag(): user.code_comment_documentation +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 + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_protected_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_protected_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +args pipe: user.insert_between("|", "|") + +# NOTE: this command is created for backward compatibility, but the documentation comments are not actually strings in Ruby. +dock string: user.code_comment_documentation() + +state end: "end" +state begin: "begin" +state rescue: "rescue " +state module: "module " + +^instance $: + insert("@") + user.code_public_variable_formatter(text) diff --git a/community/lang/rust/rust.py b/community/lang/rust/rust.py new file mode 100644 index 0000000..cdc5f1b --- /dev/null +++ b/community/lang/rust/rust.py @@ -0,0 +1,392 @@ +from typing import Any, Callable, TypeVar + +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +mod = Module() +# rust specific grammar +mod.list("code_type_modifier", desc="List of type modifiers for active language") +mod.list("code_macros", desc="List of macros for active language") +mod.list("code_trait", desc="List of traits for active language") + + +@mod.action_class +class Actions: + def code_state_implements(): + """Inserts implements block, positioning the cursor appropriately""" + + def code_insert_macro(text: str, selection: str): + """Inserts a macro and positions the cursor appropriately""" + + def code_insert_macro_array(text: str, selection: str): + """Inserts a macro array and positions the cursor appropriately""" + + def code_insert_macro_block(text: str, selection: str): + """Inserts a macro block and positions the cursor appropriately""" + + def code_state_unsafe(): + """Inserts an unsafe block and positions the cursor appropriately""" + + def code_comment_documentation_block(): + """Inserts a block document comment and positions the cursor appropriately""" + + def code_comment_documentation_inner(): + """Inserts an inner document comment and positions the cursor appropriately""" + + def code_comment_documentation_block_inner(): + """Inserts an inner block document comment and positions the cursor appropriately""" + + +ctx = Context() +ctx.matches = r""" +code.language: rust +""" + +scalar_types = { + "eye eight": "i8", + "you eight": "u8", + "bytes": "u8", + "eye sixteen": "i16", + "you sixteen": "u16", + "eye thirty two": "i32", + "you thirty two": "u32", + "eye sixty four": "i64", + "you sixty four": "u64", + "eye one hundred and twenty eight": "i128", + "you one hundred and twenty eight": "u128", + "eye size": "isize", + "you size": "usize", + "float thirty two": "f32", + "float sixty four": "f64", + "boolean": "bool", + "character": "char", +} + +compound_types = { + "tuple": "()", + "array": "[]", +} + +standard_library_types = { + "box": "Box", + "vector": "Vec", + "string": "String", + "string slice": "&str", + "os string": "OsString", + "os string slice": "&OsStr", + "see string": "CString", + "see string slice": "&CStr", + "option": "Option", + "result": "Result", + "hashmap": "HashMap", + "hash set": "HashSet", + "reference count": "Rc", +} + +standard_sync_types = { + "arc": "Arc", + "barrier": "Barrier", + "condition variable": "Condvar", + "mutex": "Mutex", + "once": "Once", + "read write lock": "RwLock", + "receiver": "Receiver", + "sender": "Sender", + "sink sender": "SyncSender", +} + +all_types = { + **scalar_types, + **compound_types, + **standard_library_types, + **standard_sync_types, +} + +standard_function_macros = { + "panic": "panic!", + "format": "format!", + "concatenate": "concat!", + "print": "print!", + "print line": "println!", + "error print line": "eprintln!", + "to do": "todo!", +} + +standard_array_macros = { + "vector": "vec!", +} + +standard_block_macros = { + "macro rules": "macro_rules!", +} + +logging_macros = { + "debug": "debug!", + "info": "info!", + "warning": "warn!", + "error": "error!", +} + +testing_macros = { + "assert": "assert!", + "assert equal": "assert_eq!", + "assert not equal": "assert_ne!", +} + +all_function_macros = { + **standard_function_macros, + **logging_macros, + **testing_macros, +} + +all_array_macros = { + **standard_array_macros, +} + +all_block_macros = { + **standard_block_macros, +} + +all_macros = { + **all_function_macros, + **all_array_macros, + **all_block_macros, +} + +all_function_macro_values = set(all_function_macros.values()) +all_array_macro_values = set(all_array_macros.values()) +all_block_macro_values = set(all_block_macros.values()) + +closure_traits = { + "closure": "Fn", + "closure once": "FnOnce", + "closure mutable": "FnMut", +} + +conversion_traits = { + "into": "Into", + "from": "From", +} + +iterator_traits = { + "iterator": "Iterator", +} + +all_traits = { + **closure_traits, + **conversion_traits, + **iterator_traits, +} + + +# tag: libraries +ctx.lists["user.code_libraries"] = { + "eye oh": "std::io", + "file system": "std::fs", + "envy": "std::env", + "collections": "std::collections", +} + +# tag: functions_common +ctx.lists["user.code_common_function"] = { + "drop": "drop", + "catch unwind": "catch_unwind", + "iterator": "iter", + "into iterator": "into_iter", + "from iterator": "from_iter", + **all_macros, +} + +# tag: functions +ctx.lists["user.code_type"] = all_types + +# rust specific grammar +ctx.lists["user.code_type_modifier"] = { + "mutable": "mut ", + "mute": "mut ", + "borrowed": "&", + "borrowed mutable": "&mut ", + "borrowed mute": "&mut ", + "mutable borrowed": "&mut ", + "mute borrowed": "&mut ", +} + + +@ctx.capture("user.code_type", rule="[{user.code_type_modifier}] {user.code_type}") +def code_type(m) -> str: + """Returns a macro name""" + return "".join(m) + + +ctx.lists["user.code_macros"] = all_macros + +ctx.lists["user.code_trait"] = all_traits + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + 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_math + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EXPONENT=lambda: actions.user.insert_between(".pow(", ")"), + 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=" || ", + ASSIGNMENT_INCREMENT=" += 1", + # code_operators_pointer + POINTER_INDIRECTION="*", + POINTER_ADDRESS_OF="&", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + # tag: imperative + + # tag: object_oriented + + def code_operator_object_accessor(): + actions.auto_insert(".") + + def code_self(): + actions.auto_insert("self") + + def code_define_class(): + actions.user.insert_snippet_by_name("structDeclaration") + + # tag: data_bool + + def code_insert_true(): + actions.auto_insert("true") + + def code_insert_false(): + actions.auto_insert("false") + + # tag: data_null + + def code_insert_null(): + actions.auto_insert("None") + + def code_insert_is_null(): + actions.auto_insert(".is_none()") + + def code_insert_is_not_null(): + actions.auto_insert(".is_some()") + + # tag: functions + + def code_default_function(text: str): + actions.user.code_private_function(text) + + def code_private_function(text: str): + actions.auto_insert("fn ") + formatter = settings.get("user.code_private_function_formatter") + function_name = actions.user.formatted_text(text, formatter) + actions.user.code_insert_function(function_name, None) + + def code_protected_function(text: str): + actions.auto_insert("pub(crate) fn ") + formatter = settings.get("user.code_protected_function_formatter") + function_name = actions.user.formatted_text(text, formatter) + actions.user.code_insert_function(function_name, None) + + def code_public_function(text: str): + actions.auto_insert("pub fn ") + formatter = settings.get("user.code_public_function_formatter") + function_name = actions.user.formatted_text(text, formatter) + actions.user.code_insert_function(function_name, None) + + def code_insert_type_annotation(type: str): + actions.auto_insert(f": {type}") + + def code_insert_return_type(type: str): + actions.auto_insert(f" -> {type}") + + # tag: functions_gui + + def code_insert_function(text: str, selection: str): + code_insert_function_or_macro(text, selection, "(", ")") + + # tag: libraries + + def code_insert_library(text: str, selection: str): + actions.user.insert_snippet_by_name("importStatement", {"0": text}) + + # rust specific grammar + + def code_state_implements(): + actions.user.insert_snippet_by_name("implementsStruct") + + def code_insert_macro(text: str, selection: str): + if text in all_array_macro_values: + code_insert_function_or_macro(text, selection, "[", "]") + elif text in all_block_macro_values: + code_insert_function_or_macro(text, selection, "{", "}") + else: + code_insert_function_or_macro(text, selection, "(", ")") + + def code_state_unsafe(): + actions.user.insert_snippet_by_name("unsafeBlock") + + def code_comment_documentation_block(): + actions.user.insert_between("/**", "*/") + actions.key("enter") + + def code_comment_documentation_inner(): + actions.auto_insert("//! ") + + def code_comment_documentation_block_inner(): + actions.user.insert_between("/*!", "*/") + actions.key("enter") + + +def code_insert_function_or_macro( + text: str, + selection: str, + left_delim: str, + right_delim: str, +): + if selection: + out_text = text + f"{left_delim}{selection}{right_delim}" + else: + out_text = text + f"{left_delim}{right_delim}" + actions.user.paste(out_text) + actions.edit.left() + + +RT = TypeVar("RT") # return type + + +def repeat_call(n: int, f: Callable[..., RT], *args: Any, **kwargs: Any): + for i in range(n): + f(*args, **kwargs) diff --git a/community/lang/rust/rust.talon b/community/lang/rust/rust.talon new file mode 100644 index 0000000..17db4c4 --- /dev/null +++ b/community/lang/rust/rust.talon @@ -0,0 +1,102 @@ +code.language: rust +- +tag(): user.code_comment_line +tag(): user.code_comment_block_c_like +tag(): user.code_comment_documentation + +tag(): user.code_imperative +tag(): user.code_object_oriented + +tag(): user.code_data_bool +tag(): user.code_data_null + +tag(): user.code_functions +tag(): user.code_functions_common +tag(): user.code_libraries + +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_bitwise +tag(): user.code_operators_math +tag(): user.code_operators_pointer + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_protected_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_protected_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +# rust-specific grammars + +## for unsafe rust +state unsafe: "unsafe " +unsafe block: user.code_state_unsafe() + +## rust centric struct and enum definitions +state (struct | structure) : + insert("struct ") + insert(user.formatted_text(text, "PUBLIC_CAMEL_CASE")) + +state enum : + insert("enum ") + insert(user.formatted_text(text, "PUBLIC_CAMEL_CASE")) + +## Simple aliases +borrow: "&" +borrow mutable: "&mut " +state (a sink | async | asynchronous): "async " +state (pub | public): "pub " +state (pub | public) crate: "pub(crate) " +state (dyn | dynamic): "dyn " +state constant: "const " +state (funk | func | function): "fn " +state (imp | implements): "impl " +state let mute: "let mut " +state let: "let " +state (mute | mutable): "mut " +state (mod | module): "mod " +state ref (mute | mutable): "ref mut " +state ref: "ref " +state trait: "trait " +state (some | sum): "Some" +state static: "static " +self taught: "self." +state use: user.code_import() + +use : + user.code_insert_library(code_libraries, "") + key(enter) + +## specialist flow control +state if let some: user.insert_between("if let Some(", ")") +state if let (ok | okay): user.insert_between("if let Ok(", ")") +state if let error: user.insert_between("if let Err(", ")") + +## rust centric synonyms +is some: user.code_insert_is_not_null() + +## for implementing +implement (struct | structure): user.code_state_implements() + +## for annotating function parameters +is implemented trait {user.code_trait}: ": impl {code_trait}" +is implemented trait: ": impl " +returns implemented trait {user.code_trait}: " -> impl {code_trait}" +returns implemented trait: " -> impl " + +## for generic reference of traits +trait {user.code_trait}: insert("{code_trait}") +implemented trait {user.code_trait}: insert("impl {code_trait}") +dynamic trait {user.code_trait}: insert("dyn {code_trait}") + +## for generic reference of macro +macro {user.code_macros}: user.code_insert_macro(code_macros, "") +macro wrap {user.code_macros}: + user.code_insert_macro(code_macros, edit.selected_text()) + +## rust specific document comments +block dock comment: user.code_comment_documentation_block() +inner dock comment: user.code_comment_documentation_inner() +inner block dock comment: user.code_comment_documentation_block_inner() diff --git a/community/lang/scala/scala.py b/community/lang/scala/scala.py new file mode 100644 index 0000000..fbff19e --- /dev/null +++ b/community/lang/scala/scala.py @@ -0,0 +1,209 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +ctx = Context() +mod = Module() +ctx.matches = r""" +code.language: scala +""" + +# Scala Common Types +scala_common_types = { + "boolean": "Boolean", + "int": "Int", + "float": "Float", + "byte": "Byte", + "double": "Double", + "short": "Short", + "long": "Long", + "char": "Char", + "unit": "Unit", + "any": "Any", + "any val": "AnyVal", + "string": "String", + "thread": "Thread", + "exception": "Exception", + "throwable": "Throwable", + "none": "None", + "success": "Success", + "failure": "Failure", +} + +# Scala Common Generic Types +scala_common_generic_types = { + "array": "Array", + "deck": "Deque", + "future": "Future", + "list": "List", + "map": "Map", + "nil": "Nil", + "option": "Option", + "queue": "Queue", + "seek": "Seq", + "set": "Set", + "some": "Some", + "stack": "Stack", + "try": "Try", +} + +scala_types = scala_common_types.copy() +scala_types.update(scala_common_generic_types) +ctx.lists["user.code_type"] = scala_types + +# Scala Modifies +scala_modifiers = { + "public": "public", + "private": "private", + "protected": "protected", +} + +mod.list("scala_modifier", desc="Scala Modifiers") +ctx.lists["user.scala_modifier"] = scala_modifiers + +scala_keywords = { + "abstract": "abstract", + "case class": "case class", + "def": "def", + "extends": "extends", + "implicit": "implicit", + "lazy val": "lazy val", + "new": "new", + "object": "object", + "override": "override", + "package": "package", + "sealed": "sealed", + "throw": "throw", + "trait": "trait", + "type": "type", + "val": "val", + "var": "var", + "with": "with", + "yield": "yield", +} + +mod.list("scala_keyword", desc="Scala Keywords") +ctx.lists["user.scala_keyword"] = scala_keywords + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("(", ")"), + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + ASSIGNMENT_MODULO=" %= ", + ASSIGNMENT_INCREMENT="++", + ASSIGNMENT_BITWISE_AND=" &= ", + ASSIGNMENT_BITWISE_OR=" |= ", + ASSIGNMENT_BITWISE_EXCLUSIVE_OR=" ^= ", + ASSIGNMENT_BITWISE_LEFT_SHIFT=" <<= ", + ASSIGNMENT_BITWISE_RIGHT_SHIFT=" >>= ", + # code_operators_bitwise + BITWISE_NOT="~", + BITWISE_AND=" & ", + BITWISE_OR=" | ", + BITWISE_EXCLUSIVE_OR=" ^ ", + BITWISE_LEFT_SHIFT=" << ", + BITWISE_RIGHT_SHIFT=" >> ", + # code_operators_lambda + LAMBDA=" => ", + # code_operators_math + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + MATH_EQUAL=" == ", + MATH_NOT_EQUAL=" != ", + MATH_NOT="!", + MATH_OR=" || ", + MATH_AND=" && ", + MATH_EXPONENT=" ^ ", + MATH_GREATER_THAN=" > ", + MATH_LESS_THAN=" < ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN_OR_EQUAL=" <= ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_self(): + actions.insert("this") + + def code_insert_null(): + actions.insert("null") + + def code_insert_is_null(): + actions.insert(" == null") + + def code_insert_is_not_null(): + actions.insert(" != null") + + def code_insert_true(): + actions.insert("true") + + def code_insert_false(): + actions.insert("false") + + def code_comment_block_prefix(): + actions.insert("/*") + + def code_comment_block_suffix(): + actions.insert("*/") + + def code_insert_type_annotation(type: str): + actions.insert(f": {type}") + + def code_insert_return_type(type: str): + actions.insert(f": {type}") + + def code_operator_object_accessor(): + actions.insert(".") + + def code_default_function(text: str): + """Inserts function declaration""" + actions.user.code_public_function(text) + + def code_insert_function(text: str, selection: str): + if selection: + text = text + f"({selection})" + else: + text = text + "()" + + actions.user.paste(text) + actions.edit.left() + + def code_private_function(text: str): + """Inserts private function declaration""" + result = "private def {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_protected_function(text: str): + result = "protected def {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + result = "def {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) diff --git a/community/lang/scala/scala.talon b/community/lang/scala/scala.talon new file mode 100644 index 0000000..4cd0d2d --- /dev/null +++ b/community/lang/scala/scala.talon @@ -0,0 +1,37 @@ +code.language: scala +- +tag(): user.code_imperative +tag(): user.code_object_oriented + +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 + +settings(): + user.code_private_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_protected_function_formatter = "PRIVATE_CAMEL_CASE" + user.code_public_function_formatter = "PRIVATE_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" + +state {user.scala_modifier}: insert("{user.scala_modifier} ") + +state {user.scala_keyword}: insert("{scala_keyword} ") + +op right arrow: " -> " +op left arrow: " <- " +op plus plus: " ++ " +op subtype: " <: " + +block string: + insert('""""""') + key("left left left") diff --git a/community/lang/sql/code_common_function.talon-list b/community/lang/sql/code_common_function.talon-list new file mode 100644 index 0000000..581f4ba --- /dev/null +++ b/community/lang/sql/code_common_function.talon-list @@ -0,0 +1,8 @@ +list: user.code_common_function +code.language: sql +- + +# these vary by dialect +count: Count +min: Min +max: Max diff --git a/community/lang/sql/sql.py b/community/lang/sql/sql.py new file mode 100644 index 0000000..246100f --- /dev/null +++ b/community/lang/sql/sql.py @@ -0,0 +1,46 @@ +from talon import Context, actions + +from ..tags.operators import Operators + +ctx = Context() +ctx.matches = r""" +code.language: sql +""" + +operators = Operators( + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_EQUAL=" = ", + MATH_NOT_EQUAL=" <> ", + MATH_GREATER_THAN=" > ", + MATH_GREATER_THAN_OR_EQUAL=" >= ", + MATH_LESS_THAN=" < ", + MATH_LESS_THAN_OR_EQUAL=" <= ", + MATH_IN=lambda: actions.user.insert_between(" IN (", ")"), + MATH_NOT_IN=lambda: actions.user.insert_between(" NOT IN (", ")"), + MATH_AND=" AND ", + MATH_OR=" OR ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_insert_null(): + actions.auto_insert("NULL") + + def code_insert_is_null(): + actions.auto_insert(" IS NULL") + + def code_insert_is_not_null(): + actions.auto_insert(" IS NOT NULL") + + def code_insert_function(text: str, selection: str): + substitutions = {"1": text} + if selection: + substitutions["0"] = selection + actions.user.insert_snippet_by_name("functionCall", substitutions) diff --git a/community/lang/sql/sql.talon b/community/lang/sql/sql.talon new file mode 100644 index 0000000..070d0e5 --- /dev/null +++ b/community/lang/sql/sql.talon @@ -0,0 +1,33 @@ +code.language: sql +- +tag(): user.code_operators_math +tag(): user.code_comment_line +tag(): user.code_comment_block_c_like +tag(): user.code_data_null +tag(): user.code_functions_common + +select: "SELECT " +distinct: "DISTINCT " +from: "FROM " +select star from: "SELECT *\nFROM " +where: "WHERE " +order by: "ORDER BY " +group by: "GROUP BY " +having: "HAVING " +descending: " DESC" +ascending: " ASC" +dot i d: ".id" +inner join: user.insert_between("INNER JOIN ", " ON ") +inner join using: user.insert_between("INNER JOIN ", " USING ") +left outer join: user.insert_between("LEFT OUTER JOIN ", " ON ") +right outer join: user.insert_between("RIGHT OUTER JOIN ", " ON ") + +with: user.insert_snippet_by_name("withStatement") + +column: + key(return) + ", " + +count: user.code_insert_function("Count", "") + +date: user.insert_between("DATE '", "'") diff --git a/community/lang/stata/code_common_function.talon-list b/community/lang/stata/code_common_function.talon-list new file mode 100644 index 0000000..6d394d0 --- /dev/null +++ b/community/lang/stata/code_common_function.talon-list @@ -0,0 +1,15 @@ +list: user.code_common_function +code.language: stata +- + +# base stata +global +local +reg +regress: reg +# packages +estadd +estout +estpost +eststo +esttab diff --git a/community/lang/stata/stata.py b/community/lang/stata/stata.py new file mode 100644 index 0000000..351f71a --- /dev/null +++ b/community/lang/stata/stata.py @@ -0,0 +1,78 @@ +from talon import Context, actions, settings + +from ..tags.operators import Operators + +ctx = Context() + +ctx.matches = r""" +code.language: stata +""" + +# functions.py +ctx.lists["user.code_parameter_name"] = { + # regressions + "V C E cluster": "vce(cluster)", + "V C E robust": "vce(robust)", +} + +# libraries.py +ctx.lists["user.code_libraries"] = { + "estout": "estout", +} + +operators = Operators( + # code_operators_array + SUBSCRIPT=lambda: actions.user.insert_between("[", "]"), + # code_operators_assignment + ASSIGNMENT=" = ", + # code_operators_math + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=lambda: actions.user.insert_between("mod(", ")"), + 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=" | ", +) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + # functions.py + def code_private_function(text: str): + result = "program {} \n\nend".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + actions.user.paste(result) + actions.edit.up() + actions.key("tab") + + def code_default_function(text: str): + actions.user.code_private_function(text) + + def code_insert_named_argument(parameter_name: str): + actions.insert(f"{parameter_name} ") + + # functions_common.py + def code_insert_function(text: str, selection: str): + substitutions = {"1": text} + if selection: + substitutions["0"] = selection + actions.user.insert_snippet_by_name("functionCall", substitutions) + + # libraries.py + def code_insert_library(text: str, selection: str): + library_text = text + selection + actions.user.insert_snippet_by_name("importStatement", {"0": library_text}) diff --git a/community/lang/stata/stata.talon b/community/lang/stata/stata.talon new file mode 100644 index 0000000..cf19843 --- /dev/null +++ b/community/lang/stata/stata.talon @@ -0,0 +1,25 @@ +code.language: stata +- +tag(): user.code_imperative + +tag(): user.code_comment_block_c_like +tag(): user.code_comment_block +tag(): user.code_comment_line +tag(): user.code_functions +tag(): user.code_functions_common +tag(): user.code_libraries +tag(): user.code_operators_array +tag(): user.code_operators_assignment +tag(): user.code_operators_math + +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + +arg {user.code_parameter_name}: user.code_insert_named_argument(code_parameter_name) + +state for val: user.insert_snippet_by_name("forLoopStatement") + +# alternative to saying ""state import"" +s s c install: user.code_import() + +s s c install : user.code_insert_library(code_libraries, "") diff --git a/community/lang/tags/comment_block.py b/community/lang/tags/comment_block.py new file mode 100644 index 0000000..8c1d84c --- /dev/null +++ b/community/lang/tags/comment_block.py @@ -0,0 +1,38 @@ +from talon import Context, Module, actions + +c_like_ctx = Context() +mod = Module() + +mod.tag("code_comment_block", desc="Tag for enabling generic block comment commands") +mod.tag("code_comment_block_c_like", desc="Denotes usage of C-style block comments") + +c_like_ctx.matches = """ +tag: user.code_comment_block_c_like +""" +c_like_ctx.tags = ["user.code_comment_block"] + + +@mod.action_class +class Actions: + def code_comment_block(): + """Block comment""" + actions.user.insert_snippet_by_name("commentBlock") + + def code_comment_block_prefix(): + """Block comment start syntax""" + + def code_comment_block_suffix(): + """Block comment end syntax""" + + +@c_like_ctx.action_class("user") +class CActions: + def code_comment_block(): + actions.insert("/*\n\n*/") + actions.edit.up() + + def code_comment_block_prefix(): + actions.auto_insert("/*") + + def code_comment_block_suffix(): + actions.auto_insert("*/") diff --git a/community/lang/tags/comment_block.talon b/community/lang/tags/comment_block.talon new file mode 100644 index 0000000..9a28a1f --- /dev/null +++ b/community/lang/tags/comment_block.talon @@ -0,0 +1,49 @@ +tag: user.code_comment_block +- +block comment: user.code_comment_block() +block comment line: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_start() + user.code_comment_block_prefix() + key(space) + edit.line_end() + key(space) + user.code_comment_block_suffix() +#adds comment to the start of the line +block comment line over: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_start() + user.code_comment_block() + insert(user.text) +block comment over: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + user.code_comment_block() + insert(user.text) +block comment $: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + user.code_comment_block() + insert(user.text) +(line | inline) block comment over: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_end() + user.code_comment_block_prefix() + key(space) + insert(user.text) + key(space) + user.code_comment_block_suffix() +(line | inline) block comment $: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_end() + user.code_comment_block_prefix() + key(space) + insert(user.text) + key(space) + user.code_comment_block_suffix() +open block comment: user.code_comment_block_prefix() +close block comment: user.code_comment_block_suffix() diff --git a/community/lang/tags/comment_documentation.py b/community/lang/tags/comment_documentation.py new file mode 100644 index 0000000..39c45c8 --- /dev/null +++ b/community/lang/tags/comment_documentation.py @@ -0,0 +1,15 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.tag( + "code_comment_documentation", desc="Tag for enabling generic documentation commands" +) + + +@mod.action_class +class Actions: + def code_comment_documentation(): + """Inserts a document comment and positions the cursor appropriately""" + actions.user.insert_snippet_by_name("commentDocumentation") diff --git a/community/lang/tags/comment_documentation.talon b/community/lang/tags/comment_documentation.talon new file mode 100644 index 0000000..7c57601 --- /dev/null +++ b/community/lang/tags/comment_documentation.talon @@ -0,0 +1,3 @@ +tag: user.code_comment_documentation +- +dock comment: user.code_comment_documentation() diff --git a/community/lang/tags/comment_line.py b/community/lang/tags/comment_line.py new file mode 100644 index 0000000..c6ee870 --- /dev/null +++ b/community/lang/tags/comment_line.py @@ -0,0 +1,13 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.tag("code_comment_line", desc="Tag for enabling generic line comment commands") + + +@mod.action_class +class Actions: + def code_comment_line_prefix(): + """Inserts line comment prefix at current cursor location""" + actions.user.insert_snippet_by_name("commentLine") diff --git a/community/lang/tags/comment_line.talon b/community/lang/tags/comment_line.talon new file mode 100644 index 0000000..921b8bb --- /dev/null +++ b/community/lang/tags/comment_line.talon @@ -0,0 +1,38 @@ +tag: user.code_comment_line +- +comment: user.code_comment_line_prefix() +comment line: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_start() + user.code_comment_line_prefix() +#adds comment to the start of the line +comment line over: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_start() + user.code_comment_line_prefix() + insert(user.text) + insert(" ") +comment over: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + user.code_comment_line_prefix() + insert(user.text) +comment $: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + user.code_comment_line_prefix() + insert(user.text) +(line | inline) comment over: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_end() + user.code_comment_line_prefix() + insert(user.text) +(line | inline) comment $: + #todo: this should probably be a single function once + #.talon supports implementing actions with parameters? + edit.line_end() + user.code_comment_line_prefix() + insert(user.text) diff --git a/community/lang/tags/data_bool.py b/community/lang/tags/data_bool.py new file mode 100644 index 0000000..27e9fae --- /dev/null +++ b/community/lang/tags/data_bool.py @@ -0,0 +1,15 @@ +from talon import Context, Module + +ctx = Context() +mod = Module() + +mod.tag("code_data_bool", desc="Tag for enabling commands for inserting Boolean data") + + +@mod.action_class +class Actions: + def code_insert_true(): + """Insert True value""" + + def code_insert_false(): + """Insert False value""" diff --git a/community/lang/tags/data_bool.talon b/community/lang/tags/data_bool.talon new file mode 100644 index 0000000..a291e5e --- /dev/null +++ b/community/lang/tags/data_bool.talon @@ -0,0 +1,4 @@ +tag: user.code_data_bool +- +state true: user.code_insert_true() +state false: user.code_insert_false() diff --git a/community/lang/tags/data_null.py b/community/lang/tags/data_null.py new file mode 100644 index 0000000..33a5d6a --- /dev/null +++ b/community/lang/tags/data_null.py @@ -0,0 +1,18 @@ +from talon import Context, Module + +ctx = Context() +mod = Module() + +mod.tag("code_data_null", desc="Tag for enabling commands relating to null") + + +@mod.action_class +class Actions: + def code_insert_null(): + """Inserts null""" + + def code_insert_is_null(): + """Inserts check for null""" + + def code_insert_is_not_null(): + """Inserts check for non-null""" diff --git a/community/lang/tags/data_null.talon b/community/lang/tags/data_null.talon new file mode 100644 index 0000000..c8f5f2f --- /dev/null +++ b/community/lang/tags/data_null.talon @@ -0,0 +1,5 @@ +tag: user.code_data_null +- +state (no | none | nil | null): user.code_insert_null() +is not (none | null): user.code_insert_is_not_null() +is (none | null): user.code_insert_is_null() diff --git a/community/lang/tags/functions.py b/community/lang/tags/functions.py new file mode 100644 index 0000000..f25fee0 --- /dev/null +++ b/community/lang/tags/functions.py @@ -0,0 +1,149 @@ +from typing import Union + +from talon import Context, Module, actions, settings + +ctx = Context() +mod = Module() + +# TODO: abstract visibilities using a list (#663) + +mod.tag("code_functions", desc="Tag for enabling commands for functions") + +mod.list("code_type", desc="List of types for active language") +mod.list( + "code_parameter_name", desc="List of common parameter names for active language" +) +mod.list( + "code_function_modifier", + desc="List of function modifiers (e.g. private, async, static)", +) + +ctx.lists["user.code_function_modifier"] = { + "pub": "public", + "pro": "protected", + "private": "private", + "static": "static", +} + + +@mod.capture(rule="{user.code_type}") +def code_type(m) -> str: + """Returns a macro name""" + return m.code_type + + +mod.setting("code_private_function_formatter", str) +mod.setting("code_protected_function_formatter", str) +mod.setting("code_public_function_formatter", str) +mod.setting("code_private_variable_formatter", str) +mod.setting("code_protected_variable_formatter", str) +mod.setting("code_public_variable_formatter", str) + + +@mod.action_class +class Actions: + def code_modified_function(modifiers: Union[list[str], int], text: str): + """ + Inserts function declaration with the given modifiers. modifiers == 0 + implies no modifiers (.talon files don't have empty list literal + syntax) + """ + mods = {} if modifiers == 0 else set(modifiers) + + if mods == {}: + return actions.user.code_default_function(text) + elif mods == {"static"}: + return actions.user.code_private_static_function(text) + elif mods == {"private"}: + return actions.user.code_private_function(text) + elif mods == {"private", "static"}: + return actions.user.code_private_static_function(text) + elif mods == {"protected"}: + return actions.user.code_protected_function(text) + elif mods == {"protected", "static"}: + return actions.user.code_protected_static_function(text) + elif mods == {"public"}: + return actions.user.code_public_function(text) + elif mods == {"public", "static"}: + return actions.user.code_public_static_function(text) + else: + raise RuntimeError(f"Unhandled modifier set: {mods}") + + def code_default_function(text: str): + """Inserts function declaration""" + actions.user.code_private_function(text) + + def code_private_function(text: str): + """Inserts private function declaration""" + + def code_private_static_function(text: str): + """Inserts private static function""" + + def code_protected_function(text: str): + """Inserts protected function declaration""" + + def code_protected_static_function(text: str): + """Inserts public function""" + + def code_public_function(text: str): + """Inserts public function""" + + def code_public_static_function(text: str): + """Inserts public function""" + + def code_private_function_formatter(name: str): + """Inserts private function name with formatter""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_private_function_formatter") + ) + ) + + def code_protected_function_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_protected_function_formatter") + ) + ) + + def code_public_function_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_public_function_formatter") + ) + ) + + def code_private_variable_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_private_variable_formatter") + ) + ) + + def code_protected_variable_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_protected_variable_formatter") + ) + ) + + def code_public_variable_formatter(name: str): + """inserts properly formatted private function name""" + actions.insert( + actions.user.formatted_text( + name, settings.get("user.code_public_variable_formatter") + ) + ) + + def code_insert_type_annotation(type: str): + """Inserts a type annotation""" + + def code_insert_return_type(type: str): + """Inserts a return type""" + + def code_insert_named_argument(parameter_name: str): + """Inserts a named argument""" diff --git a/community/lang/tags/functions.talon b/community/lang/tags/functions.talon new file mode 100644 index 0000000..f8979e5 --- /dev/null +++ b/community/lang/tags/functions.talon @@ -0,0 +1,26 @@ +tag: user.code_functions +- +# Default implementation of capture listens for the following keywords in any +# order: private pro pub static +# +# The default action implementation looks for the token combination on the left +# (funky is added here for searchability) and calls the function on the right: +# +# * funky -> code_default_function +# * private funky -> code_private_function +# * pro funky -> code_protected_function +# * pub funky -> code_public_function +# * static funky -> code_private_static_function +# * private static funky -> code_private_static_function +# * pro static funky -> code_protected_static_function +# * pub static funky -> code_public_static_function +# +^{user.code_function_modifier}* funky $: + user.code_modified_function(code_function_modifier_list or 0, text) + +# for annotating function parameters +is type : user.code_insert_type_annotation(code_type) +returns [type] : user.code_insert_return_type(code_type) + +# for generic reference of types +type : insert(code_type) diff --git a/community/lang/tags/functions_common.py b/community/lang/tags/functions_common.py new file mode 100644 index 0000000..19ce6c9 --- /dev/null +++ b/community/lang/tags/functions_common.py @@ -0,0 +1,91 @@ +from talon import Context, Module, actions, imgui, registry + +ctx = Context() +mod = Module() + +mod.list("code_common_function", desc="List of common functions for active language") + +# global +function_list = [] + + +@mod.capture(rule="{user.code_common_function}") +def code_common_function(m) -> str: + """Returns a function name""" + return m.code_common_function + + +mod.tag("code_functions_common", desc="Tag for enabling support for common functions") +mod.tag( + "code_functions_common_gui_active", + desc="Active when the function picker GUI is showing", +) + + +@mod.action_class +class Actions: + def code_toggle_functions(): + """GUI: List functions for active language""" + global function_list + if gui_functions.showing: + function_list = [] + gui_functions.hide() + ctx.tags = [] + else: + update_function_list_and_freeze() + + def code_select_function(number: int, selection: str): + """Inserts the selected function when the imgui is open""" + if gui_functions.showing and number < len(function_list): + talon_list = actions.user.talon_get_active_registry_list( + "user.code_common_function" + ) + actions.user.code_insert_function( + talon_list[function_list[number]], + selection, + ) + + # TODO: clarify the relation between `code_insert_function` + # and the various functions declared in the functions + + def code_insert_function(text: str, selection: str): + """Inserts a function and positions the cursor appropriately""" + + +def update_function_list_and_freeze(): + global function_list + if "user.code_common_function" in registry.lists: + talon_list = actions.user.talon_get_active_registry_list( + "user.code_common_function" + ) + function_list = sorted(talon_list.keys()) + else: + function_list = [] + + gui_functions.show() + ctx.tags = ["user.code_functions_common_gui_active"] + + +@imgui.open() +def gui_functions(gui: imgui.GUI): + gui.text("Functions") + gui.line() + + for i, entry in enumerate(function_list, 1): + 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)"): + actions.user.code_toggle_functions() + + +def commands_updated(_): + if gui_functions.showing: + update_function_list_and_freeze() + + +registry.register("update_commands", commands_updated) diff --git a/community/lang/tags/functions_common.talon b/community/lang/tags/functions_common.talon new file mode 100644 index 0000000..7e24f99 --- /dev/null +++ b/community/lang/tags/functions_common.talon @@ -0,0 +1,8 @@ +tag: user.code_functions_common +- +toggle funk: user.code_toggle_functions() +funk : user.code_insert_function(code_common_function, "") +funk cell : user.code_select_function(number - 1, "") +funk wrap : + user.code_insert_function(code_common_function, edit.selected_text()) +funk wrap : user.code_select_function(number - 1, edit.selected_text()) diff --git a/community/lang/tags/functions_common_gui_active.talon b/community/lang/tags/functions_common_gui_active.talon new file mode 100644 index 0000000..c598564 --- /dev/null +++ b/community/lang/tags/functions_common_gui_active.talon @@ -0,0 +1,4 @@ +tag: user.code_functions_common_gui_active +- +# Toggle prefix to be mentally similar to the 'toggle funk' show command +toggle funk: user.code_toggle_functions() diff --git a/community/lang/tags/imperative.py b/community/lang/tags/imperative.py new file mode 100644 index 0000000..b8b9b75 --- /dev/null +++ b/community/lang/tags/imperative.py @@ -0,0 +1,150 @@ +from talon import Context, Module, actions + +mod = Module() + +mod.tag( + "code_imperative", + desc="Tag for enabling basic imperative programming commands (loops, functions, etc)", +) + + +@mod.action_class +class Actions: + def code_block(): + """Inserts equivalent of {\n} for the active language, and places the cursor appropriately""" + actions.user.insert_snippet_by_name("codeBlock") + + def code_state_if(): + """Inserts if statement""" + actions.user.insert_snippet_by_name("ifStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_if", + 'user.insert_snippet_by_name("ifStatement")', + ) + + def code_state_else_if(): + """Inserts else if statement""" + actions.user.insert_snippet_by_name("elseIfStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_else_if", + 'user.insert_snippet_by_name("elseIfStatement")', + ) + + def code_state_else(): + """Inserts else statement""" + actions.user.insert_snippet_by_name("elseStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_else", + 'user.insert_snippet_by_name("elseStatement")', + ) + + def code_state_do(): + """Inserts do statement""" + actions.user.insert_snippet_by_name("doWhileLoopStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_do", + 'user.insert_snippet_by_name("doWhileLoopStatement")', + ) + + def code_state_switch(): + """Inserts switch statement""" + actions.user.insert_snippet_by_name("switchStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_switch", + 'user.insert_snippet_by_name("switchStatement")', + ) + + def code_state_case(): + """Inserts case statement""" + actions.user.insert_snippet_by_name("caseStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_case", + 'user.insert_snippet_by_name("caseStatement")', + ) + + def code_state_for(): + """Inserts for statement""" + actions.user.insert_snippet_by_name("forLoopStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_for", + 'user.insert_snippet_by_name("forLoopStatement")', + ) + + def code_state_for_each(): + """Inserts for each equivalent statement""" + actions.user.insert_snippet_by_name("forEachStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_for_each", + 'user.insert_snippet_by_name("forEachStatement")', + ) + + def code_state_go_to(): + """inserts go-to statement""" + actions.user.insert_snippet_by_name("goToStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_go_to", + 'user.insert_snippet_by_name("goToStatement")', + ) + + def code_state_while(): + """Inserts while statement""" + actions.user.insert_snippet_by_name("whileLoopStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_while", + 'user.insert_snippet_by_name("whileLoopStatement")', + ) + + def code_state_infinite_loop(): + """Inserts infinite loop statement""" + actions.user.insert_snippet_by_name("infiniteLoopStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_infinite_loop", + 'user.insert_snippet_by_name("infiniteLoopStatement")', + ) + + def code_state_return(): + """Inserts return statement""" + actions.user.insert_snippet_by_name("returnStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_state_return", + 'user.insert_snippet_by_name("returnStatement")', + ) + + def code_break(): + """Inserts break statement""" + actions.user.insert_snippet_by_name("breakStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_break", + 'user.insert_snippet_by_name("breakStatement")', + ) + + def code_next(): + """Inserts next/continue statement""" + actions.user.insert_snippet_by_name("continueStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_next", + 'user.insert_snippet_by_name("continueStatement")', + ) + + def code_try_catch(): + """Inserts try/catch. If selection is true, does so around the selection""" + actions.user.insert_snippet_by_name("tryCatchStatement") + actions.user.deprecate_action( + "2025-06-24", + "user.code_try_catch", + 'user.insert_snippet_by_name("tryCatchStatement")', + ) diff --git a/community/lang/tags/imperative.talon b/community/lang/tags/imperative.talon new file mode 100644 index 0000000..57c173a --- /dev/null +++ b/community/lang/tags/imperative.talon @@ -0,0 +1,17 @@ +tag: user.code_imperative +- +block: user.code_block() +state if: user.insert_snippet_by_name("ifStatement") +state else if: user.insert_snippet_by_name("elseIfStatement") +state else: user.insert_snippet_by_name("elseStatement") +state while: user.insert_snippet_by_name("whileLoopStatement") +state loop: user.insert_snippet_by_name("infiniteLoopStatement") +state for: user.insert_snippet_by_name("forLoopStatement") +state for in: user.insert_snippet_by_name("forEachStatement") +state (switch | match): user.insert_snippet_by_name("switchStatement") +state case: user.insert_snippet_by_name("caseStatement") +state do: user.insert_snippet_by_name("doWhileLoopStatement") +state goto: user.insert_snippet_by_name("goToStatement") +state return: user.insert_snippet_by_name("returnStatement") +state break: user.insert_snippet_by_name("breakStatement") +state (continue | next): user.insert_snippet_by_name("continueStatement") diff --git a/community/lang/tags/keywords.py b/community/lang/tags/keywords.py new file mode 100644 index 0000000..d6c4826 --- /dev/null +++ b/community/lang/tags/keywords.py @@ -0,0 +1,35 @@ +from talon import Module, actions + +mod = Module() + +mod.tag("code_keywords", desc="Tag for enabling commands for keywords") + +mod.list("code_keyword", desc="List of keywords for active language") +mod.list( + "code_keyword_unprefixed", + desc="List of keywords for active language that can be dictated by name alone or the put (name) command", +) + + +@mod.capture(rule=("{user.code_keyword}|{user.code_keyword_unprefixed}")) +def code_keyword(m) -> str: + return str(m) + + +@mod.action_class +class Actions: + def code_keyword(keywords: list[str]): + """Adds keywords""" + if len(keywords) == 1: + actions.insert(keywords[0]) + else: + # every keyword is separated by a space + # the spacing before the first keyword and after the last keyword is kept + leading_word = keywords[0].rstrip() + trailing_word = keywords[-1].lstrip() + stripped_words = ( + [leading_word] + + [keyword.strip() for keyword in keywords[1:-1]] + + [trailing_word] + ) + actions.insert(" ".join(stripped_words)) diff --git a/community/lang/tags/keywords.talon b/community/lang/tags/keywords.talon new file mode 100644 index 0000000..24b814d --- /dev/null +++ b/community/lang/tags/keywords.talon @@ -0,0 +1,4 @@ +tag: user.code_keywords +- +(keyword | put) (+): user.code_keyword(code_keyword_list) +{user.code_keyword_unprefixed}+: user.code_keyword(code_keyword_unprefixed_list) diff --git a/community/lang/tags/libraries.py b/community/lang/tags/libraries.py new file mode 100644 index 0000000..67375e8 --- /dev/null +++ b/community/lang/tags/libraries.py @@ -0,0 +1,27 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.tag( + "code_libraries", + 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""" + actions.user.insert_snippet_by_name("importStatement") + + def code_insert_library(text: str, selection: str): + """Inserts a library and positions the cursor appropriately""" diff --git a/community/lang/tags/libraries.talon b/community/lang/tags/libraries.talon new file mode 100644 index 0000000..91d9acc --- /dev/null +++ b/community/lang/tags/libraries.talon @@ -0,0 +1,3 @@ +tag: user.code_libraries +- +state import: user.code_import() diff --git a/community/lang/tags/object_oriented.py b/community/lang/tags/object_oriented.py new file mode 100644 index 0000000..6d4abc5 --- /dev/null +++ b/community/lang/tags/object_oriented.py @@ -0,0 +1,22 @@ +from talon import Context, Module, actions + +ctx = Context() +mod = Module() + +mod.tag( + "code_object_oriented", + desc="Tag for enabling basic object oriented programming commands (objects, classes, etc)", +) + + +@mod.action_class +class Actions: + def code_operator_object_accessor(): + """Inserts the object accessor operator (e.g., Java's "." or PHP's "->)""" + + def code_self(): + """Inserts a reference to the current object (e.g., C++ "this" or Python's "self")""" + + def code_define_class(): + """Starts a class definition (e.g., Java's "class" keyword)""" + actions.user.insert_snippet_by_name("classDeclaration") diff --git a/community/lang/tags/object_oriented.talon b/community/lang/tags/object_oriented.talon new file mode 100644 index 0000000..150b886 --- /dev/null +++ b/community/lang/tags/object_oriented.talon @@ -0,0 +1,10 @@ +tag: user.code_object_oriented +- + +self dot: + user.code_self() + user.code_operator_object_accessor() + +state self: user.code_self() + +state class: user.code_define_class() diff --git a/community/lang/tags/operators.py b/community/lang/tags/operators.py new file mode 100644 index 0000000..44022a2 --- /dev/null +++ b/community/lang/tags/operators.py @@ -0,0 +1,216 @@ +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_math_comparison", + desc="List of code operators for mathematical comparison 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_INTEGER_DIVIDE: Operator + MATH_MODULO: Operator + MATH_EXPONENT: Operator + MATH_EQUAL: Operator + MATH_NOT_EQUAL: Operator + # For weak comparison operators. Strict comparison should use standard operators. + MATH_WEAK_EQUAL: Operator + MATH_WEAK_NOT_EQUAL: Operator + MATH_WEAK_AND: Operator + MATH_WEAK_OR: Operator + MATH_WEAK_NOT: 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/community/lang/tags/operators.talon b/community/lang/tags/operators.talon new file mode 100644 index 0000000..9c30801 --- /dev/null +++ b/community/lang/tags/operators.talon @@ -0,0 +1,10 @@ +# Note: the "help operators" command will currently display "op" and "is" regardless of what the commands are +# so changing those commands will make the "help operators" command display the wrong prefixes +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_pointer}: user.code_operator(code_operators_pointer) +op {user.code_operators_math}: user.code_operator(code_operators_math) +is {user.code_operators_math_comparison}: + user.code_operator(code_operators_math_comparison) diff --git a/community/lang/tags/operators_array.py b/community/lang/tags/operators_array.py new file mode 100644 index 0000000..f33f3bc --- /dev/null +++ b/community/lang/tags/operators_array.py @@ -0,0 +1,9 @@ +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def code_operator_subscript(): + """code_operator_subscript (e.g., C++ [])""" diff --git a/community/lang/tags/operators_array.talon-list b/community/lang/tags/operators_array.talon-list new file mode 100644 index 0000000..8404f45 --- /dev/null +++ b/community/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/community/lang/tags/operators_assignment.py b/community/lang/tags/operators_assignment.py new file mode 100644 index 0000000..1dda36c --- /dev/null +++ b/community/lang/tags/operators_assignment.py @@ -0,0 +1,45 @@ +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def code_operator_assignment(): + """code_operator_assignment""" + + def code_or_operator_assignment(): + """code_operator_assignment""" + + def code_operator_subtraction_assignment(): + """code_operator_subtraction_assignment""" + + def code_operator_addition_assignment(): + """code_operator_addition_assignment""" + + def code_operator_increment(): + """code_operator_increment""" + + def code_operator_multiplication_assignment(): + """code_operator_multiplication_assignment""" + + def code_operator_division_assignment(): + """code_operator_division_assignment""" + + def code_operator_modulo_assignment(): + """code_operator_modulo_assignment""" + + def code_operator_bitwise_and_assignment(): + """code_operator_and_assignment""" + + def code_operator_bitwise_or_assignment(): + """code_operator_or_assignment""" + + def code_operator_bitwise_exclusive_or_assignment(): + """code_operator_bitwise_exclusive_or_assignment""" + + def code_operator_bitwise_left_shift_assignment(): + """code_operator_bitwise_left_shift_assigment""" + + def code_operator_bitwise_right_shift_assignment(): + """code_operator_bitwise_right_shift_assignment""" diff --git a/community/lang/tags/operators_assignment.talon-list b/community/lang/tags/operators_assignment.talon-list new file mode 100644 index 0000000..d4ce8e4 --- /dev/null +++ b/community/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/community/lang/tags/operators_assignment_deprecations.talon b/community/lang/tags/operators_assignment_deprecations.talon new file mode 100644 index 0000000..1f0c15c --- /dev/null +++ b/community/lang/tags/operators_assignment_deprecations.talon @@ -0,0 +1,47 @@ +tag: user.code_operators_assignment +- +tag(): user.code_operators_math +tag(): user.code_operators_bitwise + +# assignment +op assign: + user.deprecate_command("2025-01-19", "op assign", "op equals") + user.code_operator("ASSIGNMENT") + +# combined computation and assignment +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.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.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.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.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/community/lang/tags/operators_bitwise.py b/community/lang/tags/operators_bitwise.py new file mode 100644 index 0000000..82fdf89 --- /dev/null +++ b/community/lang/tags/operators_bitwise.py @@ -0,0 +1,24 @@ +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def code_operator_bitwise_and(): + """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""" + + def code_operator_bitwise_left_shift(): + """code_operator_bitwise_left_shift""" + + def code_operator_bitwise_right_shift(): + """code_operator_bitwise_right_shift""" diff --git a/community/lang/tags/operators_bitwise.talon-list b/community/lang/tags/operators_bitwise.talon-list new file mode 100644 index 0000000..f621be8 --- /dev/null +++ b/community/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/community/lang/tags/operators_bitwise_deprecations.talon b/community/lang/tags/operators_bitwise_deprecations.talon new file mode 100644 index 0000000..4498626 --- /dev/null +++ b/community/lang/tags/operators_bitwise_deprecations.talon @@ -0,0 +1,27 @@ +tag: user.code_operators_bitwise +- + +#bitwise operators +bitwise and: + user.deprecate_command("2025-01-19", "bitwise and", "op bitwise and") + user.code_operator("BITWISE_AND") + +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) (left shift | shift left): + 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.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/community/lang/tags/operators_lambda.py b/community/lang/tags/operators_lambda.py new file mode 100644 index 0000000..2855379 --- /dev/null +++ b/community/lang/tags/operators_lambda.py @@ -0,0 +1,9 @@ +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def code_operator_lambda(): + """code_operator_lambda""" diff --git a/community/lang/tags/operators_lambda.talon-list b/community/lang/tags/operators_lambda.talon-list new file mode 100644 index 0000000..6f302e4 --- /dev/null +++ b/community/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/community/lang/tags/operators_math.py b/community/lang/tags/operators_math.py new file mode 100644 index 0000000..53cb900 --- /dev/null +++ b/community/lang/tags/operators_math.py @@ -0,0 +1,57 @@ +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def code_operator_subtraction(): + """code_operator_subtraction""" + + def code_operator_addition(): + """code_operator_addition""" + + def code_operator_multiplication(): + """code_operator_multiplication""" + + def code_operator_exponent(): + """code_operator_exponent""" + + def code_operator_division(): + """code_operator_division""" + + def code_operator_modulo(): + """code_operator_modulo""" + + def code_operator_equal(): + """code_operator_equal""" + + def code_operator_not_equal(): + """code_operator_not_equal""" + + def code_operator_greater_than(): + """code_operator_greater_than""" + + def code_operator_greater_than_or_equal_to(): + """code_operator_greater_than_or_equal_to""" + + def code_operator_less_than(): + """code_operator_less_than""" + + def code_operator_less_than_or_equal_to(): + """code_operator_less_than_or_equal_to""" + + def code_operator_and(): + """code_operator_and""" + + def code_operator_or(): + """code_operator_or""" + + def code_operator_not(): + """code_operator_not""" + + def code_operator_in(): + """code_operator_in""" + + def code_operator_not_in(): + """code_operator_not_in""" diff --git a/community/lang/tags/operators_math.talon-list b/community/lang/tags/operators_math.talon-list new file mode 100644 index 0000000..a0b82fc --- /dev/null +++ b/community/lang/tags/operators_math.talon-list @@ -0,0 +1,20 @@ +list: user.code_operators_math +tag: user.code_operators_math +- + +# Math operators +minus: MATH_SUBTRACT +plus: MATH_ADD +times: MATH_MULTIPLY +divide: MATH_DIVIDE +int divide: MATH_INTEGER_DIVIDE +mod: MATH_MODULO +power: MATH_EXPONENT + +# logical operators +and: MATH_AND +or: MATH_OR +not: MATH_NOT +weak and: MATH_WEAK_AND +weak or: MATH_WEAK_OR +weak not: MATH_WEAK_NOT diff --git a/community/lang/tags/operators_math_comparison.talon-list b/community/lang/tags/operators_math_comparison.talon-list new file mode 100644 index 0000000..ae86a6e --- /dev/null +++ b/community/lang/tags/operators_math_comparison.talon-list @@ -0,0 +1,17 @@ +list: user.code_operators_math_comparison +tag: user.code_operators_math +- + +# Comparison operators +equal: MATH_EQUAL +not equal: MATH_NOT_EQUAL +weak equal: MATH_WEAK_EQUAL +weak not equal: MATH_WEAK_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 + +# Set operators +in: MATH_IN +not in: MATH_NOT_IN diff --git a/community/lang/tags/operators_math_deprecations.talon b/community/lang/tags/operators_math_deprecations.talon new file mode 100644 index 0000000..a6ce914 --- /dev/null +++ b/community/lang/tags/operators_math_deprecations.talon @@ -0,0 +1,53 @@ +tag: user.code_operators_math +- + +# math operators +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 +is more: + user.deprecate_command("2025-01-19", "is more", "is greater") + user.code_operator("MATH_GREATER_THAN") + +is below [than]: + user.deprecate_command("2025-01-19", "is below [than]", "is less") + user.code_operator("MATH_LESS_THAN") + +is greater than or equal: + user.deprecate_command("2025-01-19", "is greater than or equal", "is 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", "is less or equal") + user.code_operator("MATH_LESS_THAN_OR_EQUAL") + +# logical operators +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") + +op colon: + user.deprecate_command("2025-01-19", "op colon", "pad colon") + insert(" : ") diff --git a/community/lang/tags/operators_pointer.py b/community/lang/tags/operators_pointer.py new file mode 100644 index 0000000..1f4b826 --- /dev/null +++ b/community/lang/tags/operators_pointer.py @@ -0,0 +1,15 @@ +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def code_operator_indirection(): + """code_operator_indirection""" + + def code_operator_address_of(): + """code_operator_address_of (e.g., C++ & op)""" + + def code_operator_structure_dereference(): + """code_operator_structure_dereference (e.g., C++ -> op)""" diff --git a/community/lang/tags/operators_pointer.talon-list b/community/lang/tags/operators_pointer.talon-list new file mode 100644 index 0000000..7f5d724 --- /dev/null +++ b/community/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 diff --git a/community/lang/talon/talon-list.talon b/community/lang/talon/talon-list.talon new file mode 100644 index 0000000..14e2d4d --- /dev/null +++ b/community/lang/talon/talon-list.talon @@ -0,0 +1,5 @@ +code.language: talonlist +- +# requires user.talon_populate_lists tag. do not use with dragon +list [require] {user.talon_lists}: "list: {talon_lists}" +list [require]: "list: " diff --git a/community/lang/talon/talon.py b/community/lang/talon/talon.py new file mode 100644 index 0000000..5a1cfd0 --- /dev/null +++ b/community/lang/talon/talon.py @@ -0,0 +1,140 @@ +from talon import Context, Module, actions, app, registry + +from ..tags.operators import Operators + +mod = Module() +ctx_talon = Context() +ctx_talon_python = Context() +ctx_talon_lists = Context() + +# restrict all the talon_* lists to when the user.talon_populate_lists tag +# is active to prevent them from being active in contexts where they are not wanted. +# Do not enable this tag with dragon, as it will be unusable. +# with conformer, the latency increase may also be unacceptable depending on your cpu +# see https://github.com/talonhub/community/issues/600 +ctx_talon_lists.matches = r""" +tag: user.talon_populate_lists +""" + +mod.tag("talon_python", "Tag to activate talon-specific python commands") +mod.tag( + "talon_populate_lists", + "Tag to activate talon-specific lists of actions, scopes, modes etcetera. Do not use this tag with dragon", +) +mod.list("talon_actions") +mod.list("talon_lists") +mod.list("talon_captures") +mod.list("talon_apps") +mod.list("talon_tags") +mod.list("talon_modes") +mod.list("talon_settings") +mod.list("talon_scopes") +mod.list("talon_modes") + +ctx_talon.matches = r""" +code.language: talon +""" + +ctx_talon_python.matches = r""" +tag: user.talon_python +""" + + +def on_update_decls(decls): + # todo modes? + for thing in [ + "actions", + "lists", + "captures", + "tags", + "apps", + "settings", + "scopes", + "modes", + ]: + l = getattr(decls, thing) + ctx_talon_lists.lists[f"user.talon_{thing}"] = ( + actions.user.create_spoken_forms_from_list( + l.keys(), generate_subsequences=False + ) + ) + # print( + # "List: {} \n {}".format(thing, str(ctx_talon_lists.lists[f"user.talon_{thing}"])) + # ) + + +def on_ready(): + # print("on_ready") + on_update_decls(registry.decls) + registry.register("update_decls", on_update_decls) + + +app.register("ready", on_ready) + + +@mod.action_class +class Actions: + def talon_code_insert_action_call(text: str, selection: str): + """inserts talon-specific action call""" + actions.user.code_insert_function(text, selection) + + def talon_code_enable_tag(tag: str): + """enables tag in either python or talon files""" + + def talon_code_enable_setting(setting: str): + """asserts setting in either python or talon files""" + + +@ctx_talon.action_class("user") +class TalonActions: + def talon_code_enable_tag(tag: str): + """enables tag in either python or talon files""" + actions.user.paste(f"tag(): {tag}") + + def talon_code_enable_setting(setting: str): + """asserts setting in either python or talon files""" + actions.user.paste(f"{setting} = ") + + +@ctx_talon_python.action_class("user") +class TalonPythonActions: + def talon_code_insert_action_call(text: str, selection: str): + text = f"actions.{text}({selection or ''})" + actions.user.paste(text) + actions.edit.left() + + def talon_code_enable_tag(tag: str): + """enables tag in either python or talon files""" + actions.user.paste(f'ctx.tags = ["{tag}"]') + if not tag: + actions.edit.left() + actions.edit.left() + + def talon_code_enable_setting(setting: str): + """asserts setting in either python or talon files""" + if not setting: + actions.user.insert_between('ctx.settings["', '"] = ') + else: + actions.user.paste(f'ctx.settings["{setting}"] = ') + + +operators = Operators( + MATH_AND=" and ", + MATH_OR=" or ", + MATH_SUBTRACT=" - ", + MATH_ADD=" + ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + ASSIGNMENT=" = ", +) + + +@ctx_talon.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_insert_function(text: str, selection: str): + text += f"({selection or ''})" + actions.user.paste(text) + actions.edit.left() diff --git a/community/lang/talon/talon.talon b/community/lang/talon/talon.talon new file mode 100644 index 0000000..df2b7e4 --- /dev/null +++ b/community/lang/talon/talon.talon @@ -0,0 +1,14 @@ +code.language: talon +- +tag(): user.code_operators_math +tag(): user.code_operators_assignment +tag(): user.code_comment_line +tag(): user.code_functions_common +# uncomment user.talon_populate_lists tag to activate talon-specific lists of actions, scopes, modes etcetera. +# Do not enable this tag with dragon, as it will be unusable. +# with conformer, the latency increase may also be unacceptable depending on your cpu +# see https://github.com/talonhub/community/issues/600 +# tag(): user.talon_populate_lists + +#defintion blocks for the context +setting block: insert("settings():\n\t") diff --git a/community/lang/talon/talon_code_common_function.talon-list b/community/lang/talon/talon_code_common_function.talon-list new file mode 100644 index 0000000..addc97d --- /dev/null +++ b/community/lang/talon/talon_code_common_function.talon-list @@ -0,0 +1,7 @@ +list: user.code_common_function +code.language: talon +- +insert +key +print +repeat diff --git a/community/lang/talon/talon_common.talon b/community/lang/talon/talon_common.talon new file mode 100644 index 0000000..3bcd4cb --- /dev/null +++ b/community/lang/talon/talon_common.talon @@ -0,0 +1,22 @@ +#Defines commands common to both python and talon files +code.language: talon +code.language: python +and tag: user.talon_python +- +tag set [{user.talon_tags}]: + tag = talon_tags or "" + user.talon_code_enable_tag(tag) + +# requires user.talon_populate_lists tag. do not use with dragon +list {user.talon_lists}: "{{{talon_lists}}}" +# requires user.talon_populate_lists tag. do not use with dragon +capture {user.talon_captures}: "<{talon_captures}>" + +setting {user.talon_settings}: user.talon_code_enable_setting(talon_settings) + +#commands for dictating key combos +key over: "{keys}" +key over: "{modifiers}" + +action {user.talon_actions}: + user.talon_code_insert_action_call(talon_actions, edit.selected_text()) diff --git a/community/lang/talon/talon_context.talon b/community/lang/talon/talon_context.talon new file mode 100644 index 0000000..d001cba --- /dev/null +++ b/community/lang/talon/talon_context.talon @@ -0,0 +1,22 @@ +code.language: talon +code.language: talonlist +code.language: python +and tag: user.talon_python +- +#context requirements +win require: insert("os: windows\n") +mac require: insert("os: mac\n") +linux require: insert("os: linux\n") +title require: insert("win.title: ") +application [require] [{user.talon_apps}]: + app = "{talon_apps}\n" or "" + insert("app: {app}") +mode require [{user.talon_modes}]: + mode = "{talon_modes}\n" or "" + insert("mode: {mode}") +tag require [{user.talon_tags}]: + tag = "{talon_tags}\n" or "" + insert("tag: {tag}") +host require: + hostname = user.talon_get_hostname() + insert("hostname: {hostname}\n") diff --git a/community/lang/talon/talon_python_activator.talon b/community/lang/talon/talon_python_activator.talon new file mode 100644 index 0000000..3f82ea7 --- /dev/null +++ b/community/lang/talon/talon_python_activator.talon @@ -0,0 +1,14 @@ +# This file activates talon-specific python commands +# by default, it simply looks for the python tag to be active +# lines 7-11 provide examples to make the activation more specific +# which may be preferred by people who code in other python projects +# app: vscode +# Mac VSCode uses an em-dash +# win.title: /— user/ +# win.title: /— community/ +# windows VSCode uses an en-dash +# win.title: / - user - Visual Studio Code/ +# win.title: / - community - Visual Studio Code/ +code.language: python +- +tag(): user.talon_python diff --git a/community/lang/terraform/terraform.py b/community/lang/terraform/terraform.py new file mode 100644 index 0000000..f5495e4 --- /dev/null +++ b/community/lang/terraform/terraform.py @@ -0,0 +1,117 @@ +from talon import Context, Module, actions + +from ..tags.operators import Operators + +ctx = Context() +mod = Module() +ctx.matches = r""" +code.language: terraform +""" + +types = { + "string": "string", + "number": "number", + "bool": "bool", + "list": "list", + "map": "map", + "null": "null", +} + +ctx.lists["user.code_type"] = types + +common_properties = { + "name": "name", + "type": "type", + "description": "description", + "default": "default", + "for each": "for_each", + "count": "count", + "prevent destroy": "prevent_destroy", + "nullable": "nullable", + "sensitive": "sensitive", + "depends on": "depends_on", + "provider": "provider", + "source": "source", +} + +mod.list("terraform_common_property", desc="Terraform Modifier") +ctx.lists["self.terraform_common_property"] = common_properties + +module_blocks = { + "variable": "variable", + "output": "output", + "provider": "provider", + "module": "module", +} + +mod.list("terraform_module_block", desc="Simple Terraform Block") +ctx.lists["self.terraform_module_block"] = module_blocks + +operators = Operators( + # code_operators_assignment + ASSIGNMENT=" = ", + # code_operators_lambda + LAMBDA=" => ", + # code_operators_math + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", + MATH_MODULO=" % ", + 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=" || ", +) + + +@mod.action_class +class Actions: + def code_terraform_module_block(text: str): + """Inserts a new module-related block of a given type (e.g. variable, output, provider...)""" + + def code_terraform_resource(text: str): + """Inserts a new resource block with given name""" + + def code_terraform_data_source(text: str): + """Inserts a new data block with given name""" + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_terraform_module_block(text: str): + actions.user.insert_between(text + ' "', '"') + + def code_terraform_resource(text: str): + result = f"resource \"{actions.user.formatted_text(text, 'SNAKE_CASE')}\" \"\"" + + actions.insert(result) + actions.key("left") + + def code_terraform_data_source(text: str): + result = f"data \"{actions.user.formatted_text(text, 'SNAKE_CASE')}\" \"\"" + + actions.insert(result) + actions.key("left") + + def code_insert_true(): + actions.insert("true") + + def code_insert_false(): + actions.insert("false") + + def code_insert_null(): + actions.insert("null") + + def code_insert_is_null(): + actions.insert(" == null") + + def code_insert_is_not_null(): + actions.insert(" != null") diff --git a/community/lang/terraform/terraform.talon b/community/lang/terraform/terraform.talon new file mode 100644 index 0000000..939f669 --- /dev/null +++ b/community/lang/terraform/terraform.talon @@ -0,0 +1,26 @@ +code.language: terraform +- + +tag(): user.code_comment_block_c_like +tag(): user.code_comment_line +tag(): user.code_data_bool +tag(): user.code_data_null +tag(): user.code_imperative +tag(): user.code_operators_assignment +tag(): user.code_operators_lambda +tag(): user.code_operators_math + +block: user.code_block() + +state {user.terraform_module_block}: + user.code_terraform_module_block(user.terraform_module_block) + +resource : user.code_terraform_resource(text) + +data [source] : user.code_terraform_data_source(text) + +[state] prop {user.terraform_common_property}: + insert(user.terraform_common_property) + user.code_operator_assignment() + +type {user.code_type}: insert("{code_type}") diff --git a/community/lang/typescript/typescript.py b/community/lang/typescript/typescript.py new file mode 100644 index 0000000..18c8069 --- /dev/null +++ b/community/lang/typescript/typescript.py @@ -0,0 +1,56 @@ +from talon import Context, actions, settings + +ctx = Context() +ctx.matches = r""" +code.language: typescript +code.language: typescriptreact +# Make typescript win over javascript +mode: command +""" + +ctx.lists["user.code_type"] = { + "boolean": "boolean", + "integer": "int", + "string": "string", + "null": "null", + "undefined": "undefined", + "number": "number", + "any": "any", +} + + +@ctx.action_class("user") +class UserActions: + def code_private_function(text: str): + """Inserts private function declaration""" + result = "private function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_private_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_protected_function(text: str): + result = "protected function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_protected_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_public_function(text: str): + result = "public function {}".format( + actions.user.formatted_text( + text, settings.get("user.code_public_function_formatter") + ) + ) + + actions.user.code_insert_function(result, None) + + def code_insert_type_annotation(type: str): + actions.insert(f": {type}") + + def code_insert_return_type(type: str): + actions.insert(f": {type}") diff --git a/community/lang/typescript/typescript.talon b/community/lang/typescript/typescript.talon new file mode 100644 index 0000000..9e7f314 --- /dev/null +++ b/community/lang/typescript/typescript.talon @@ -0,0 +1,10 @@ +code.language: typescript +code.language: typescriptreact +- + +type union []: " | {code_type or ''}" +type intersect []: " & {code_type or ''}" + +state type: user.insert_between("type ", " = ") + +as const: " as const" diff --git a/community/lang/vimscript/vimscript.py b/community/lang/vimscript/vimscript.py new file mode 100644 index 0000000..9b4965f --- /dev/null +++ b/community/lang/vimscript/vimscript.py @@ -0,0 +1,80 @@ +from talon import Context, Module, actions, settings + +from ..tags.operators import Operators + +mod = Module() +ctx = Context() +ctx.matches = r""" +code.language: vimscript +""" + +ctx.lists["self.vimscript_functions"] = { + "string len": "strlen", + "get line": "getline", + "set line": "setline", + "length": "len", +} + +ctx.lists["self.vimscript_scope"] = { + "argument": "a:", + "arg": "a:", + "buffer": "b:", + "buf": "b:", + "window": "w:", + "win": "w:", + "tab": "t:", + "special": "v:", + "global": "g:", + "local": "l:", + "script local": "s:", +} + +mod.list("vimscript_functions", desc="Standard built-in vimscript functions") +mod.list("vimscript_scope", desc="vimscript scoping types for functions and variables") + + +@mod.capture(rule="{self.vimscript_functions}") +def vimscript_functions(m) -> str: + "Returns a string" + return m.vimscript_functions + + +@mod.capture(rule="{self.vimscript_scope}") +def vimscript_scope(m) -> str: + "Returns a string" + return m.vimscript_scope + + +operators = Operators( + # code_operators_assignment + ASSIGNMENT=" = ", + ASSIGNMENT_ADDITION=" += ", + ASSIGNMENT_SUBTRACTION=" -= ", + ASSIGNMENT_MULTIPLICATION=" *= ", + ASSIGNMENT_DIVISION=" /= ", + # code_operators_math + MATH_ADD=" + ", + MATH_SUBTRACT=" - ", + MATH_MULTIPLY=" * ", + MATH_DIVIDE=" / ", +) + + +def code_function_declaration(text: str, formatter: str): + formatted_text = actions.user.formatted_text(text, settings.get(formatter)) + actions.user.insert_snippet_by_name("functionDeclaration", {"1": formatted_text}) + + +@ctx.action_class("user") +class UserActions: + def code_get_operators() -> Operators: + return operators + + def code_private_function(text: str): + code_function_declaration(text, "user.code_private_function_formatter") + + def code_protected_function(text: str): + code_function_declaration(text, "user.code_protected_function_formatter") + + def code_public_function(text: str): + code_function_declaration(text, "user.code_public_function_formatter") diff --git a/community/lang/vimscript/vimscript.talon b/community/lang/vimscript/vimscript.talon new file mode 100644 index 0000000..03e3fa8 --- /dev/null +++ b/community/lang/vimscript/vimscript.talon @@ -0,0 +1,38 @@ +code.language: vimscript +- +tag(): user.code_imperative +tag(): user.code_operators_assignment +tag(): user.code_operators_math +tag(): user.code_comment_line + +# XXX - revisit these +settings(): + user.code_private_function_formatter = "SNAKE_CASE" + user.code_protected_function_formatter = "SNAKE_CASE" + user.code_public_function_formatter = "SNAKE_CASE" + user.code_private_variable_formatter = "SNAKE_CASE" + user.code_protected_variable_formatter = "SNAKE_CASE" + user.code_public_variable_formatter = "SNAKE_CASE" + +### +# VIM Script Specific +### +assign [] (variable | var) [] [over]: + insert("let ") + insert(vimscript_scope or "") + user.code_private_variable_formatter(text) + +[] (variable | var) [] [over]: + insert(vimscript_scope or "") + user.code_private_variable_formatter(text) + +# see lang/vimscript/vimscript.py for list +: insert("{vimscript_functions} ") + +# XXX - possibly overlap with some programming.talon +state command: "command! " +state end if: "endif" +state end for: "endfor" +state end while: "endwhile" +state end function: "endfunction" +state continue: "continue" diff --git a/community/migration_helpers/migration_helpers.py b/community/migration_helpers/migration_helpers.py new file mode 100644 index 0000000..0c32f82 --- /dev/null +++ b/community/migration_helpers/migration_helpers.py @@ -0,0 +1,271 @@ +import csv +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Union + +from talon import Module, actions, app + +mod = Module() + + +@dataclass +class CSVData: + """Class to track CSV-related data necessary for conversion to .talon-list""" + + # name of the list + name: str + # Path to the CSV file + path: str + # path to the generated .talon-list + newpath: Union[str, callable] = None + # Indicates whether the first line of the CSV file is a header + # that should be ignored + is_first_line_header: bool = True + # Indicates whether the spoken form or value is first in the CSV file + is_spoken_form_first: bool = False + # An optional callable for generating a custom header for + # generated .talon-list + custom_header: callable = None + # An optional callable for custom processing of the value for + # generated .talon-list + custom_value_converter: callable = None + + +# Note: homophones, emacs_commands, file_extensions, words_to_replace, abbreviations, and app name overrides +# are intentionally omitted, as their use cases are not compatible with .talon-list conversions +supported_csv_files = [ + CSVData( + "user.git_argument", + os.path.join("apps", "git", "git_arguments.csv"), + os.path.join("apps", "git", "git_argument.talon-list"), + ), + CSVData( + "user.git_command", + os.path.join("apps", "git", "git_commands.csv"), + os.path.join("apps", "git", "git_command.talon-list"), + ), + CSVData( + "user.vocabulary", + os.path.join("settings", "additional_words.csv"), + os.path.join("core", "vocabulary", "vocabulary.talon-list"), + ), + CSVData( + "user.letter", + os.path.join("settings", "alphabet.csv"), + os.path.join("core", "keys", "letter.talon-list"), + ), + CSVData( + "user.system_paths", + os.path.join("settings", "system_paths.csv"), + lambda: os.path.join( + "core", f"system_paths-{actions.user.talon_get_hostname()}.talon-list" + ), + custom_header=(lambda: f"hostname: {actions.user.talon_get_hostname()}"), + ), + CSVData( + "user.search_engine", + os.path.join("settings", "search_engines.csv"), + os.path.join("core", "websites_and_search_engines", "search_engine.talon-list"), + ), + CSVData( + "user.unix_utility", + os.path.join("settings", "unix_utilities.csv"), + os.path.join("tags", "terminal", "unix_utility.talon-list"), + ), + CSVData( + "user.website", + os.path.join("settings", "websites.csv"), + os.path.join("core", "websites_and_search_engines", "website.talon-list"), + ), + CSVData( + "user.emoji", + os.path.join("tags", "emoji", "emoji.csv"), + os.path.join("tags", "emoji", "emoji.talon-list"), + is_first_line_header=False, + is_spoken_form_first=True, + ), + CSVData( + "user.emoticon", + os.path.join("tags", "emoji", "emoticon.csv"), + os.path.join("tags", "emoji", "emoticon.talon-list"), + is_first_line_header=False, + is_spoken_form_first=True, + ), + CSVData( + "user.kaomoji", + os.path.join("tags", "emoji", "kaomoji.csv"), + os.path.join("tags", "emoji", "kaomoji.talon-list"), + is_first_line_header=False, + is_spoken_form_first=True, + ), +] + + +def convert_csv_to_talonlist(input_csv: csv.reader, config: CSVData): + """ + Convert a 1 or 2 column CSV into a talon list. + Empty lines, lines containing only whitespace or starting with a # are skipped. + + Args: + - input_csv: A csv.reader instance + - config: A CSVData instance + + Returns: + - str: The contents of a talon list file + + Raises: + - ValueError: If any line in the input CSV contains more than 2 columns. + """ + rows = list(input_csv) + + is_spoken_form_first = config.is_spoken_form_first + is_first_line_header = config.is_first_line_header + start_index = 1 if is_first_line_header else 0 + output = [] + + output.append(f"list: {config.name}") + if config.custom_header and callable(config.custom_header): + output.append(config.custom_header()) + + output.append("-") + + for row in rows[start_index:]: + # Remove trailing whitespace for each cell + row = [col.rstrip() for col in row] + cols = len(row) + + # Check columns + if cols > 2: + raise ValueError("Expected only 1 or 2 columns, got {cols}:", row) + + # Exclude empty or comment rows + if cols == 0 or (cols == 1 and row[0] == "") or row[0].startswith("#"): + continue + + if cols == 2: + if is_spoken_form_first: + spoken_form, value = row + else: + value, spoken_form = row + + if config.custom_value_converter: + value = config.custom_value_converter(value) + + else: + spoken_form = value = row[0] + + if spoken_form != value: + if not str.isprintable(value) or "'" in value or '"' in value: + value = repr(value) + + output.append(f"{spoken_form}: {value}") + else: + output.append(f"{spoken_form}") + + # Terminate file in newline + output.append("") + return "\n".join(output) + + +def convert_files(csv_files_list): + known_csv_files = {str(item.path): item for item in csv_files_list} + + conversion_count = 0 + base_path = Path(__file__).resolve().parent.parent + + for csv_path in base_path.rglob("*.csv"): + csv_relative_path = csv_path.relative_to(base_path) + migrated_csv_path = csv_path.with_suffix(".csv-converted-to-talon-list") + + config = known_csv_files.get(str(csv_relative_path)) + if not config: + continue + + if callable(config.newpath): + talonlist_relative_path = config.newpath() + else: + talonlist_relative_path = config.newpath + + talonlist_path = base_path / talonlist_relative_path + + if talonlist_path.is_file() and not csv_path.is_file(): + print(f"Skipping existing Talon list file {talonlist_relative_path}") + continue + + if migrated_csv_path.is_file(): + print(f"Skipping existing renamed CSV {migrated_csv_path}") + continue + + print( + f"Converting CSV {csv_relative_path} to Talon list {talonlist_relative_path}" + ) + + conversion_count += 1 + with open(csv_path, newline="") as csv_file: + csv_reader = csv.reader(csv_file, skipinitialspace=True) + talonlist_content = convert_csv_to_talonlist(csv_reader, config) + + print( + f"Renaming converted CSV to {migrated_csv_path.name}. This file may be deleted if no longer needed; it's preserved in case there's an issue with conversion." + ) + if talonlist_path.is_file(): + backup_path = talonlist_path.with_suffix(".bak") + print( + f"Migration target {talonlist_relative_path} already exists; backing up to {backup_path}" + ) + talonlist_path.rename(backup_path) + + with open(talonlist_path, "w") as talonlist_file: + talonlist_file.write(talonlist_content) + csv_path.rename(migrated_csv_path) + + return conversion_count + + +@mod.action_class +class Actions: + def migrate_known_csv_files(): + """Migrate known CSV files to .talon-list""" + conversion_count = convert_files(supported_csv_files) + if conversion_count > 0: + notification_text = f"migration_helpers.py converted {conversion_count} CSVs. See Talon log for more details.\n" + print(notification_text) + actions.app.notify(notification_text) + + def migrate_custom_csv( + path: str, + new_path: str, + list_name: str, + is_first_line_header: bool, + spoken_form_first: bool, + ): + """Migrate a custom CSV file""" + csv_file = CSVData( + list_name, + path, + new_path, + is_first_line_header, + spoken_form_first, + None, + None, + ) + convert_files([csv_file]) + + +def on_ready(): + try: + actions.user.migrate_known_csv_files() + except KeyError: + # Due to a core Talon bug, the above action may not be available when a ready callback is invoked. + # (see https://github.com/talonhub/community/pull/1268#issuecomment-2325721706) + notification = ( + "Unable to migrate CSVs to Talon lists.", + "Please quit and restart Talon.", + ) + app.notify(*notification) + print(*notification) + + +app.register("ready", on_ready) diff --git a/community/plugin/README.md b/community/plugin/README.md new file mode 100644 index 0000000..dc6a5f4 --- /dev/null +++ b/community/plugin/README.md @@ -0,0 +1,21 @@ +# plugin + +The plugin folder has several other subfolders containing various commands: + +- `cancel` contains commands to make talon ignore a command +- `command_history` has commands to see previous commands +- `datetimeinsert` has commands to automatically write the current date and time +- `desktops` has commands to navigate between the different computer desktops +- `draft_editor` has some of the commands to open and use a built-in pop-up text editor +- `dropdown` has commands to select an option from a dropdown menu +- `macro` has commands to use macros +- `media` has commands for video and volume control +- `microphone_selection` has commands for selecting a microphone to use +- `mode_indicator` does not have commands, but has settings for enabling a graphical mode indicator +- `mouse` has commands to click, drag, scroll, and use an eye tracker +- `repeater` has commands for repeating other commands, described briefly in the top level [README](https://github.com/talonhub/community?tab=readme-ov-file#repeating-commands) +- `screenshot` has commands for taking screenshots +- `symbols` has commands for inserting certain symbols, like pairs of parentheses or quotation marks +- `talon_draft_window` has the rest of the commands for using the draft editor window +- `talon_helpers` has commands helpful for debugging, opening the talon directory, and getting updates +- `text_navigation` has commands for navigating the cursor in text diff --git a/community/plugin/are_you_sure/README.md b/community/plugin/are_you_sure/README.md new file mode 100644 index 0000000..4a0c743 --- /dev/null +++ b/community/plugin/are_you_sure/README.md @@ -0,0 +1,28 @@ +# Are You Sure Dialog + +This lets you require confirmation before executing an action, which can be useful for potentially destructive commands like shutting down your computer or exiting talon. + +To require confirmation for an action, you use the user.are_you_sure_set_on_confirmation_action function that receives a message to display for the dialogue and the action to perform on confirmation. An optional action to perform on cancelling the action can be provided as the third argument. As this is intended to work with particularly destructive actions, this only supports executing a single action at a time and does not work with chaining. + +You confirm an action by saying "yes I am sure" and cancel it by saying "cancel". + +## Example + +```python +from talon import actions, Module, app + +mod = Module() +@mod.action_class +class Actions: + def test_are_you_sure(): + '''A simple test for the are you sure dialog''' + def on_confirm(): + app.notify('Confirmed') + def on_cancel(): + app.notify('Cancelled') + actions.user.are_you_sure_set_on_confirmation_action('Would you like to receive the on confirm message?', on_confirm, on_cancel) +``` + +```talon +test are you sure: user.test_are_you_sure() +``` diff --git a/community/plugin/are_you_sure/are_you_sure.py b/community/plugin/are_you_sure/are_you_sure.py new file mode 100644 index 0000000..ca1e442 --- /dev/null +++ b/community/plugin/are_you_sure/are_you_sure.py @@ -0,0 +1,73 @@ +from typing import Callable + +from talon import Context, Module, actions, imgui + +mod = Module() +mod.tag("are_you_sure", desc="Activates are you sure commands") + + +class ConfirmationState: + def __init__(self): + self.context = Context() + + def request_confirmation(self, message: str, on_confirmation, on_disconfirmation): + self.on_confirmation = on_confirmation + self.on_cancel = on_disconfirmation + self.message = message + self.context.tags = ["user.are_you_sure"] + gui.show() + + def confirm(self): + self.on_confirmation() + self.cleanup() + + def cancel(self): + if self.on_cancel: + self.on_cancel() + self.cleanup() + + def cleanup(self): + self.context.tags = [] + self.on_confirmation = None + self.on_cancel = None + self.message = None + gui.hide() + + def get_message(self) -> str: + return self.message + + +confirmation = ConfirmationState() + + +@imgui.open(y=0) +def gui(gui: imgui.GUI): + gui.text(confirmation.get_message()) + gui.line() + if gui.button("Yes I am sure"): + actions.user.are_you_sure_confirm() + if gui.button("Cancel"): + actions.user.are_you_sure_cancel() + + +@mod.action_class +class Actions: + def are_you_sure_confirm(): + """Performs the registered are you sure action""" + confirmation.confirm() + + def are_you_sure_cancel(): + """Cancels the registered are you sure action""" + confirmation.cancel() + + def are_you_sure_set_on_confirmation_action( + message: str, on_confirmation: Callable, on_cancel: Callable = None + ): + """Sets the action to be performed on user confirmation. + message: the message to display to the user + on_confirmation: the action to perform if the user confirms + on_cancel: (optional) the action to perform if the user cancels + This only supports working with a single action at a time and + does not work with chaining as it is intended to be used with particularly destructive actions. + """ + confirmation.request_confirmation(message, on_confirmation, on_cancel) diff --git a/community/plugin/are_you_sure/are_you_sure.talon b/community/plugin/are_you_sure/are_you_sure.talon new file mode 100644 index 0000000..84c79b0 --- /dev/null +++ b/community/plugin/are_you_sure/are_you_sure.talon @@ -0,0 +1,4 @@ +tag: user.are_you_sure +- +yes I am sure: user.are_you_sure_confirm() +cancel: user.are_you_sure_cancel() diff --git a/community/plugin/cancel/cancel.py b/community/plugin/cancel/cancel.py new file mode 100644 index 0000000..1533aad --- /dev/null +++ b/community/plugin/cancel/cancel.py @@ -0,0 +1,73 @@ +# to disable command cancellation, comment out this entire file. +# you may also wish to adjust the commands in misc/cancel.talon. + +import time + +from talon import Context, Module, actions, speech_system +from talon.grammar import Phrase + +# To change the phrase used to cancel commands, you must also adjust cancel.talon +cancel_phrase = "cancel cancel".split() + +mod = Module() +ctx = Context() + +ts_threshold: float = 0 + + +@ctx.action_class("speech") +class SpeechActions: + # When Talon wakes we set the timestamp threshold. On the next command we + # will compare the phrase timestamp to the threshold and cancel any phrase + # started before wakeup. This is to prevent speech said before wake-up to + # be interpreted as a command if the user wakes Talon using a noise or + # keypress. + def enable(): + actions.user.cancel_current_phrase() + actions.next() + + +@mod.action_class +class Actions: + def cancel_current_phrase(): + """Cancel/abort current spoken phrase""" + global ts_threshold + ts_threshold = time.perf_counter() + + +def pre_phrase(phrase: Phrase): + global ts_threshold + + words = phrase["phrase"] + + if not words: + return + + # 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 phrase.get("_ts", ts_threshold) + phrase_starts_before_threshold = start < ts_threshold + ts_threshold = 0 + # Start of phrase is before threshold timestamp + if phrase_starts_before_threshold: + print(f"Canceled phrase: {' '.join(words)}") + cancel_entire_phrase(phrase) + return + + # Check if the phrase is a cancel command + n = len(cancel_phrase) + before, after = words[:-n], words[-n:] + if after == cancel_phrase: + actions.app.notify(f"Command canceled: {' '.join(before)!r}") + cancel_entire_phrase(phrase) + return + + +def cancel_entire_phrase(phrase: Phrase): + phrase["phrase"] = [] + if "parsed" in phrase: + phrase["parsed"]._sequence = [] + + +speech_system.register("pre:phrase", pre_phrase) diff --git a/community/plugin/cancel/cancel.talon b/community/plugin/cancel/cancel.talon new file mode 100644 index 0000000..79e4149 --- /dev/null +++ b/community/plugin/cancel/cancel.talon @@ -0,0 +1,6 @@ +# allows you to prevent a command executing by ending it with "cancel cancel" +cancel cancel$: skip() +# the actual behavior of "cancel cancel" is implemented in cancel.py; if you want to use a different phrase you must also change cancel_phrase there. + +# allows you to say something (eg to a human) that you don't want talon to hear, eg "ignore hey Jerry" +ignore []$: app.notify("Command ignored") diff --git a/community/plugin/command_history/command_history.py b/community/plugin/command_history/command_history.py new file mode 100644 index 0000000..14cdf25 --- /dev/null +++ b/community/plugin/command_history/command_history.py @@ -0,0 +1,90 @@ +from typing import Optional + +from talon import Module, actions, imgui, settings, speech_system + +from ..subtitles.on_phrase import skip_phrase + +# We keep command_history_size lines of history, but by default display only +# command_history_display of them. +mod = Module() +mod.setting("command_history_size", type=int, default=50) +mod.setting("command_history_display", type=int, default=10) + +hist_more: bool = False +history: list[str] = [] + + +def on_phrase(j): + global history + if skip_phrase(j): + return + + words = j.get("phrase") + text = actions.user.history_transform_phrase_text(words) + if text is not None: + history.append(text) + history = history[-settings.get("user.command_history_size") :] + + +# todo: dynamic rect? +@imgui.open(y=0) +def gui(gui: imgui.GUI): + global history + gui.text("Command History") + gui.line() + text = ( + history[:] + if hist_more + else history[-settings.get("user.command_history_display") :] + ) + for line in text: + gui.text(line) + + gui.spacer() + if gui.button("Command history close"): + actions.user.history_disable() + + +speech_system.register("phrase", on_phrase) + + +@mod.action_class +class Actions: + def history_toggle(): + """Toggles viewing the history""" + if gui.showing: + gui.hide() + else: + gui.show() + + def history_enable(): + """Enables the history""" + gui.show() + + def history_disable(): + """Disables the history""" + gui.hide() + + def history_clear(): + """Clear the history""" + global history + history = [] + + def history_more(): + """Show more history""" + global hist_more + hist_more = True + + def history_less(): + """Show less history""" + global hist_more + hist_more = False + + def history_get(number: int) -> str: + """returns the history entry at the specified index""" + num = (0 - number) - 1 + return history[num] + + def history_transform_phrase_text(words: list[str]) -> Optional[str]: + """Transforms phrase text for presentation in history. Return `None` to omit from history""" + return " ".join(words) if words else None diff --git a/community/plugin/command_history/command_history.talon b/community/plugin/command_history/command_history.talon new file mode 100644 index 0000000..7289894 --- /dev/null +++ b/community/plugin/command_history/command_history.talon @@ -0,0 +1,5 @@ +command history: user.history_toggle() +command history close: user.history_disable() +command history clear: user.history_clear() +command history less: user.history_less() +command history more: user.history_more() diff --git a/community/plugin/datetimeinsert/datetimeinsert.py b/community/plugin/datetimeinsert/datetimeinsert.py new file mode 100644 index 0000000..6a7adbc --- /dev/null +++ b/community/plugin/datetimeinsert/datetimeinsert.py @@ -0,0 +1,24 @@ +import datetime + +from talon import Module + +mod = Module() + + +@mod.action_class +class Actions: + def time_format(fmt: str = None) -> str: + """Return the current time, formatted. + fmt: strftime()-style format string, defaults to ISO format.""" + now = datetime.datetime.now() + if fmt is None: + return now.isoformat() + return now.strftime(fmt) + + def time_format_utc(fmt: str = None) -> str: + """Return the current UTC time, formatted. + fmt: strftime()-style format string, defaults to ISO format.""" + now = datetime.datetime.utcnow() + if fmt is None: + return now.isoformat() + return now.strftime(fmt) diff --git a/community/plugin/datetimeinsert/datetimeinsert.talon b/community/plugin/datetimeinsert/datetimeinsert.talon new file mode 100644 index 0000000..8b62cfa --- /dev/null +++ b/community/plugin/datetimeinsert/datetimeinsert.talon @@ -0,0 +1,7 @@ +date insert: insert(user.time_format("%Y-%m-%d")) +date insert UTC: insert(user.time_format_utc("%Y-%m-%d")) +timestamp insert: insert(user.time_format("%Y-%m-%d %H:%M:%S")) +timestamp insert high resolution: insert(user.time_format("%Y-%m-%d %H:%M:%S.%f")) +timestamp insert UTC: insert(user.time_format_utc("%Y-%m-%d %H:%M:%S")) +timestamp insert UTC high resolution: + insert(user.time_format_utc("%Y-%m-%d %H:%M:%S.%f")) diff --git a/community/plugin/desktops/desktops.py b/community/plugin/desktops/desktops.py new file mode 100644 index 0000000..a6d8f07 --- /dev/null +++ b/community/plugin/desktops/desktops.py @@ -0,0 +1,34 @@ +from talon import Module, app + +mod = Module() + + +@mod.action_class +class Actions: + def desktop(number: int): + """change the current desktop""" + app.notify("Not supported on this operating system") + + def desktop_show(): + """shows the current desktops""" + app.notify("Not supported on this operating system") + + def desktop_next(): + """move to next desktop""" + app.notify("Not supported on this operating system") + + def desktop_last(): + """move to previous desktop""" + app.notify("Not supported on this operating system") + + def window_move_desktop_left(): + """move the current window to the desktop to the left""" + app.notify("Not supported on this operating system") + + def window_move_desktop_right(): + """move the current window to the desktop to the right""" + app.notify("Not supported on this operating system") + + def window_move_desktop(desktop_number: int): + """move the current window to a different desktop""" + app.notify("Not supported on this operating system") diff --git a/community/plugin/desktops/desktops.talon b/community/plugin/desktops/desktops.talon new file mode 100644 index 0000000..d6bd7c4 --- /dev/null +++ b/community/plugin/desktops/desktops.talon @@ -0,0 +1,7 @@ +desk : user.desktop(number_small) +desk next: user.desktop_next() +desk last: user.desktop_last() +desk show: user.desktop_show() +window move desk : user.window_move_desktop(number) +window move desk left: user.window_move_desktop_left() +window move desk right: user.window_move_desktop_right() diff --git a/community/plugin/desktops/desktops_linux.py b/community/plugin/desktops/desktops_linux.py new file mode 100644 index 0000000..888eb3e --- /dev/null +++ b/community/plugin/desktops/desktops_linux.py @@ -0,0 +1,31 @@ +from talon import Context, actions, ui + +ctx = Context() +ctx.matches = r""" +os: linux +""" + + +@ctx.action_class("user") +class Actions: + def desktop(number: int): + ui.switch_workspace(number) + + def desktop_next(): + actions.user.desktop(ui.active_workspace() + 1) + + def desktop_last(): + actions.user.desktop(ui.active_workspace() - 1) + + def desktop_show(): + actions.key("super") + + def window_move_desktop(desktop_number: int): + ui.active_window().workspace = desktop_number + actions.user.desktop(desktop_number) + + def window_move_desktop_left(): + actions.user.window_move_desktop(ui.active_workspace() - 1) + + def window_move_desktop_right(): + actions.user.window_move_desktop(ui.active_workspace() + 1) diff --git a/community/plugin/desktops/desktops_mac.py b/community/plugin/desktops/desktops_mac.py new file mode 100644 index 0000000..0d5ff76 --- /dev/null +++ b/community/plugin/desktops/desktops_mac.py @@ -0,0 +1,58 @@ +import contextlib +import time + +from talon import Context, actions, ctrl, ui + +ctx = Context() +ctx.matches = r""" +os: mac +""" + + +@contextlib.contextmanager +def _drag_window_mac(win=None): + if win is None: + win = ui.active_window() + fs = win.children.find(AXSubrole="AXFullScreenButton")[0] + rect = fs.AXFrame + x = rect.x + rect.width + 5 + y = rect.y + rect.height / 2 + previous_position = ctrl.mouse_pos() + ctrl.mouse_move(x, y) + ctrl.mouse_click(button=0, down=True) + yield + time.sleep(0.1) + ctrl.mouse_click(button=0, up=True) + ctrl.mouse_move(*previous_position) + + +@ctx.action_class("user") +class MacActions: + def desktop(number: int): + if number < 10: + actions.key(f"ctrl-{number}") + + def desktop_next(): + actions.key("ctrl-right") + + def desktop_last(): + actions.key("ctrl-left") + + def desktop_show(): + actions.key("ctrl-up") + + def window_move_desktop_left(): + with _drag_window_mac(): + actions.user.desktop_last() + + def window_move_desktop_right(): + with _drag_window_mac(): + actions.user.desktop_next() + + def window_move_desktop(desktop_number: int): + # TODO: amethyst stuff should be pulled out into a separate file + if ui.apps(bundle="com.amethyst.Amethyst"): + actions.key(f"ctrl-alt-shift-{desktop_number}") + else: + with _drag_window_mac(): + actions.user.desktop(desktop_number) diff --git a/community/plugin/desktops/desktops_win.py b/community/plugin/desktops/desktops_win.py new file mode 100644 index 0000000..aa742c7 --- /dev/null +++ b/community/plugin/desktops/desktops_win.py @@ -0,0 +1,26 @@ +from talon import Context, actions + +ctx = Context() +ctx.matches = r""" +os: windows +""" + + +@ctx.action_class("user") +class Actions: + # def desktop(number: int): + + def desktop_next(): + actions.key("super-ctrl-right") + + def desktop_last(): + actions.key("super-ctrl-left") + + def desktop_show(): + actions.key("super-tab") + + # def window_move_desktop_left(): + + # def window_move_desktop_right(): + + # def window_move_desktop(desktop_number: int): diff --git a/community/plugin/draft_editor/draft_editor.py b/community/plugin/draft_editor/draft_editor.py new file mode 100644 index 0000000..33994f9 --- /dev/null +++ b/community/plugin/draft_editor/draft_editor.py @@ -0,0 +1,149 @@ +from talon import Context, Module, actions, app, settings, ui + +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", +) + +ctx = Context() +tags: set[str] = set() + + +def add_tag(tag: str): + if tag not in tags: + tags.add(tag) + ctx.tags = list(tags) + + +def remove_tag(tag: str): + if tag in tags: + tags.discard(tag) + ctx.tags = list(tags) + + +default_names = ["Visual Studio Code", "Code", "VSCodium", "Codium", "code-oss"] + +mod.setting( + "draft_editor", + type=str, + default=None, + desc="List of application names to use for draft editor", +) + + +def get_editor_names(): + names_csv = settings.get("user.draft_editor") + return names_csv.split(", ") if names_csv else default_names + + +def handle_app_running(_app): + editor_names = get_editor_names() + for app in ui.apps(background=False): + if app.name in editor_names: + add_tag("user.draft_editor_app_running") + return + remove_tag("user.draft_editor_app_running") + + +def handle_app_activate(app): + if app.name in get_editor_names(): + add_tag("user.draft_editor_app_focused") + else: + remove_tag("user.draft_editor_app_focused") + + +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 + +last_draft = None + + +@mod.action_class +class Actions: + def draft_editor_open(): + """Open draft editor""" + global original_window + original_window = ui.active_window() + editor_app = get_editor_app() + selected_text = actions.edit.selected_text() + actions.user.switcher_focus_app(editor_app) + # Wait additional time for talon context to update. + actions.sleep("200ms") + actions.app.tab_open() + if selected_text != "": + actions.user.paste(selected_text) + add_tag("user.draft_editor_active") + + def draft_editor_submit(): + """Submit/save draft editor""" + close_editor(submit_draft=True) + + def draft_editor_discard(): + """Discard draft editor""" + close_editor(submit_draft=False) + + def draft_editor_paste_last(): + """Paste last submitted draft""" + if last_draft: + actions.user.paste(last_draft) + + +def get_editor_app() -> ui.App: + editor_names = get_editor_names() + + for app in ui.apps(background=False): + if app.name in editor_names: + return app + + raise RuntimeError("Draft editor is not running") + + +def close_editor(submit_draft: bool) -> None: + global last_draft + + 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 intended destination and use 'draft submit' again" + ) + else: + actions.sleep("300ms") + actions.user.paste(last_draft) + else: + try: + actions.user.switcher_focus_window(original_window) + except Exception: + app.notify("Failed to focus previous window, leaving editor open") diff --git a/community/plugin/draft_editor/draft_editor.talon b/community/plugin/draft_editor/draft_editor.talon new file mode 100644 index 0000000..409d1bc --- /dev/null +++ b/community/plugin/draft_editor/draft_editor.talon @@ -0,0 +1,23 @@ +tag: user.draft_editor_app_running +and not tag: user.draft_editor_app_focused +- + +draft this: user.draft_editor_open() + +draft all: + edit.select_all() + user.draft_editor_open() + +draft line: + edit.select_line() + user.draft_editor_open() + +draft top: + edit.extend_file_start() + user.draft_editor_open() + +draft bottom: + edit.extend_file_end() + user.draft_editor_open() + +draft submit: user.draft_editor_paste_last() diff --git a/community/plugin/draft_editor/draft_editor_open.talon b/community/plugin/draft_editor/draft_editor_open.talon new file mode 100644 index 0000000..aadcfff --- /dev/null +++ b/community/plugin/draft_editor/draft_editor_open.talon @@ -0,0 +1,6 @@ +tag: user.draft_editor_active +and tag: user.draft_editor_app_focused +- + +draft submit: user.draft_editor_submit() +draft discard: user.draft_editor_discard() diff --git a/community/plugin/dropdown/dropdown.talon b/community/plugin/dropdown/dropdown.talon new file mode 100644 index 0000000..e8161bf --- /dev/null +++ b/community/plugin/dropdown/dropdown.talon @@ -0,0 +1,4 @@ +# DEPRECATED +drop down : user.deprecate_command("2024-05-29", "drop down", "choose") +drop down up : + user.deprecate_command("2024-05-29", "drop down up", "choose up") diff --git a/community/plugin/eye_tracking_settings.py b/community/plugin/eye_tracking_settings.py new file mode 100644 index 0000000..c945a9f --- /dev/null +++ b/community/plugin/eye_tracking_settings.py @@ -0,0 +1,10 @@ +# from talon import app +# from talon.types import Point2d +# from talon_plugins import eye_mouse, eye_zoom_mouse + +# if app.platform == "mac": +# eye_zoom_mouse.config.screen_area = Point2d(100, 75) +# eye_zoom_mouse.config.img_scale = 6 +# elif app.platform == "windows": +# eye_zoom_mouse.config.screen_area = Point2d(200, 150) +# eye_zoom_mouse.config.img_scale = 4.5 diff --git a/community/plugin/gamepad/README.md b/community/plugin/gamepad/README.md new file mode 100644 index 0000000..6add6b7 --- /dev/null +++ b/community/plugin/gamepad/README.md @@ -0,0 +1,29 @@ +# Gamepad + +Some predefined gamepad bindings for doing tasks like clicking, scrolling and moving your cursor. + +### Usage + +To enable the gamepad bindings activate tag `user.gamepad` in [gamepad_settings.talon](./gamepad_settings.talon) + +## Demo - Using gamepad + +[YouTube - Gamepad demo](https://youtu.be/zNeiZ9nnK_A) + +## Gamepad tester + +![Gamepad tester](./gamepad_tester.png) + +### Usage + +1. Say `"gamepad tester"` to open gamepad tester UI. +1. Press buttons on actual gamepad and see interaction in UI. +1. Close gamepad tester by saying `"gamepad tester"` again. + +### Demo - Gamepad tester + +[YouTube - Gamepad tester demo](https://youtu.be/FzfIlaHm8_w) + +### Conflict with existing gamepad implementations + +The gamepad tester doesn't disable your existing gamepad implementations. If you don't want your existing gamepad implementations to trigger during the testing phase you can add `not tag: user.gamepad_tester` at the top of your gamepad Talon files. diff --git a/community/plugin/gamepad/gamepad.py b/community/plugin/gamepad/gamepad.py new file mode 100644 index 0000000..165dc90 --- /dev/null +++ b/community/plugin/gamepad/gamepad.py @@ -0,0 +1,304 @@ +from talon import Module, actions, ctrl, ui +from talon.screen import Screen + +screen: Screen = ui.main_screen() +slow_scroll = False +slow_mouse_move = False + +mod = Module() +mod.tag("gamepad", desc="Activate tag to enable gamepad bindings") + + +@mod.action_class +class Actions: + # DPAD buttons + + def gamepad_press_dpad_left(): + """Gamepad press button dpad left""" + gamepad_mouse_jump("left") + + def gamepad_release_dpad_left(): + """Gamepad release button dpad left""" + actions.skip() + + def gamepad_press_dpad_up(): + """Gamepad press button dpad up""" + gamepad_mouse_jump("up") + + def gamepad_release_dpad_up(): + """Gamepad release button dpad up""" + actions.skip() + + def gamepad_press_dpad_right(): + """Gamepad press button dpad right""" + gamepad_mouse_jump("right") + + def gamepad_release_dpad_right(): + """Gamepad release button dpad right""" + actions.skip() + + def gamepad_press_dpad_down(): + """Gamepad press button dpad down""" + gamepad_mouse_jump("down") + + def gamepad_release_dpad_down(): + """Gamepad release button dpad down""" + actions.skip() + + # Compass / ABXY buttons + + def gamepad_press_west(): + """Gamepad press button west""" + actions.mouse_drag(0) + + def gamepad_release_west(): + """Gamepad release button west""" + actions.mouse_release(0) + + def gamepad_press_north(): + """Gamepad press button north""" + actions.mouse_drag(1) + + def gamepad_release_north(): + """Gamepad release button north""" + actions.mouse_release(1) + + def gamepad_press_east(): + """Gamepad press button east""" + actions.key("ctrl:down") + actions.mouse_click() + actions.key("ctrl:up") + + def gamepad_release_east(): + """Gamepad release button east""" + actions.skip() + + def gamepad_press_south(): + """Gamepad press button south""" + actions.mouse_drag(2) + + def gamepad_release_south(): + """Gamepad release button south""" + actions.mouse_release(2) + + # Select / Start buttons + + def gamepad_press_select(): + """Gamepad press button select""" + actions.skip() + + def gamepad_release_select(): + """Gamepad release button select""" + actions.skip() + + def gamepad_press_start(): + """Gamepad press button start""" + actions.speech.toggle() + + def gamepad_release_start(): + """Gamepad release button start""" + actions.skip() + + # Shoulder buttons + + def gamepad_press_left_shoulder(): + """Gamepad press button left shoulder""" + actions.user.go_back() + + def gamepad_release_left_shoulder(): + """Gamepad release button left shoulder""" + actions.skip() + + def gamepad_press_right_shoulder(): + """Gamepad press button right shoulder""" + actions.user.go_forward() + + def gamepad_release_right_shoulder(): + """Gamepad release button right shoulder""" + actions.skip() + + # Stick buttons + + def gamepad_press_left_stick(): + """Gamepad press button left thumb stick""" + gamepad_scroll_slow_toggle() + + def gamepad_release_left_stick(): + """Gamepad release button left thumb stick""" + actions.skip() + + def gamepad_press_right_stick(): + """Gamepad press button right thumb stick""" + gamepad_mouse_move_slow_toggle() + + def gamepad_release_right_stick(): + """Gamepad release button right thumb stick""" + actions.skip() + + # Analog triggers + + def gamepad_trigger_left(value: float): + """Gamepad trigger left movement""" + gamepad_scroll(0, value * -1) + + def gamepad_trigger_right(value: float): + """Gamepad trigger right movement""" + gamepad_scroll(0, value) + + # Analog thumb sticks + + def gamepad_stick_left(x: float, y: float): + """Gamepad left stick movement""" + gamepad_scroll(x, y) + + def gamepad_stick_right(x: float, y: float): + """Gamepad right stick movement""" + gamepad_mouse_move(x, y) + + # Scaffolding actions used by the Talon file + + def gamepad_button_down(button: str): + """Gamepad press button