diff --git a/.gitignore b/.gitignore index aefde33228..224ff3db4c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __pycache__ /settings .vscode/settings.json .DS_Store +.bak diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 277a8e2257..f295196121 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black - repo: https://github.com/Lucas-C/pre-commit-hooks diff --git a/BREAKING_CHANGES.txt b/BREAKING_CHANGES.txt index eb7dd2ce1a..ba6219fd1e 100644 --- a/BREAKING_CHANGES.txt +++ b/BREAKING_CHANGES.txt @@ -1,28 +1,44 @@ -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. +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. +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. --- -* 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 actions in favor of the new action/modifier grammar. +* 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. +* 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. +* 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/README.md b/README.md index 17396e1f02..f8bd6bbfd7 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ Can be used on its own, but shines when combined with: - [Talon](https://talonvoice.com/) - Mac, Windows, or Linux -- Can work with both Talon's built-in Conformer (wav2letter) speech recognition engine (recommended), or Dragon Naturally Speaking (Windows) / Dragon for Mac (although beware that Dragon for Mac is deprecated). -- Includes commands for working with an eye tracker, but not required +- 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 @@ -27,7 +28,7 @@ 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 and hit `enter`: +3. Paste the following into the terminal window then press Enter/Return: ```bash cd ~/.talon/user @@ -41,8 +42,8 @@ Note that it is also possible to install `community` by [downloading and extract It is recommended to install `community` using [`git`](https://git-scm.com/). 1. Install [`git`](https://git-scm.com/) -2. Open a [terminal](https://www.wikihow.com/Open-the-Command-Prompt-in-Windows) -3. Paste the following into the terminal and hit `enter`: +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 @@ -53,19 +54,19 @@ Note that it is also possible to install `community` by [downloading and extract ## Getting started with Talon -1. `help active` will display the available commands for the active application. - - Available commands can change with the application, or even window title that has focus. - - You may navigate help using the displayed numbers. e.g., `help one one` or `help eleven` to open the 11th item in the help list. - - Note that all help-related commands are defined in [`core/help/help.talon`](https://github.com/talonhub/community/blob/main/core/help/help.talon) and [`core/help/help_open.talon`](https://github.com/talonhub/community/blob/main/core/help/help_open.talon) -2. You can also 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. You can also jump immediately into a particular help context display by recalling the name displayed in help window (based on the name of the .talon file) e.g. `help symbols` or `help visual studio` -4. `help alphabet` will display the alphabet -5. `command history` will toggle a display of the recent commands -6. `help format` will display the available formatters with examples. -7. Many useful, basic commands are defined in https://github.com/talonhub/community/blob/main/core/edit/edit.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/ +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. @@ -77,100 +78,98 @@ If you use vim, just start with the numbers and alphabet, otherwise look at gene ### Alphabet -The alphabet is defined here -https://github.com/talonhub/community/blob/main/core/keys/keys.py#L3 +The alphabet is defined in +[this Talon list file](core/keys/letter.talon-list). -`help alphabet` will open a window that displays the alphabet. `help close` to hide the window. +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 -Keys are defined in keys.py. The alphabet is used for A-Z. For the rest, search for `modifier_keys` and then keep scrolling through the file, eg. roughly https://github.com/talonhub/community/blob/main/core/keys/keys.py#L111 +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`. -All key commands are defined in [keys.talon](https://github.com/talonhub/community/blob/main/core/keys/keys.talon). 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 +On Windows, try commands such as: -- `control air` to press `control-a` and select all. +- `control air` to press Control+A and select all. -- `super-shift-sun` to press `windows-shift-s` to trigger the screenshot application (Windows 10). Then try `escape` to exit the screenshot application. +- `super-shift-sun` to press Win+Shift+S, triggering the screenshot application (Windows 10). Then try `escape` to exit. -On Mac, try commands such as +On Mac, try commands such as: -- `command air` to press `command-a` and select all. +- `command air` to press ⌘A and select all. -- `control shift command 4` to press ` ctrl-shift-cmd-4` to trigger the screenshot application. Then try `escape` to exit the screenshot application. Please note the order of the modifiers doesn't matter. +- `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. -Any combination of the modifiers, symbols, alphabet, numbers and function keys can be executed via voice to execute shorcuts. Modifier keys can be tapped using `press`, for example `press control` to tap the control key by itself. +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, so you can say e.g. `control colon` to press those keys. -https://github.com/talonhub/community/blob/main/core/keys/keys.py#L140 +Some symbols are defined in [keys.py](core/keys/keys.py#L144), so you can say, e.g. `control colon` to press those keys. -Some other symbols are defined here: https://github.com/talonhub/community/blob/main/plugin/symbols/symbols.talon +Multi-character punctuation (e.g., ellipses) is defined in [symbols.talon](plugin/symbols/symbols.talon). ### Formatters -`help format` will display the available formatters with examples of the output. +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". -Try using formatters by saying e.g. `snake hello world`, which will insert hello_world +Multiple formatters can be chained together — for example, `dubstring snake hello world` inserts "hello_world". -Multiple formatters can be used together, e.g. `dubstring snake hello world`. This will insert "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". -Formatters (snake, dubstring) are defined here -https://github.com/talonhub/community/blob/main/core/text/formatters.py#L137 +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`. -All formatter-related commands are defined here -https://github.com/talonhub/community/blob/main/core/text/text.talon#L8 +Formatter names (snake, dubstring) are defined [here](core/text/formatters.py#L245). Formatter-related commands are defined in [text.talon](core/text/text.talon#L8). ### Mouse commands -See https://github.com/talonhub/community/blob/main/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](https://github.com/talonhub/community/tree/main/core/mouse_grid). +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 -https://github.com/talonhub/community/blob/main/core/edit/edit.talon - -These generic commands are global. Commands such as `go word left` will work in any text box. +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 -For repeating commands, useful voice commands are defined here: https://github.com/talonhub/community/blob/main/plugin/repeater/repeater.talon +Voice commands for repeating commands are defined in [repeater.talon](plugin/repeater/repeater.talon). -Try saying e.g. `go up fifth` will go up five lines. -Try saying e.g. `select up third` to hit `shift-up` three times to select some lines in a text field. +Say `go up fifth` or `go up five times` to go up five lines. `select up third` will press Shift+Up three times to select several lines of text. ### Window management -Global window managment commands are defined here: -https://github.com/talonhub/community/blob/main/core/windows_and_tabs/window_management.talon +Global window managment commands are defined in [window_management.talon](core/windows_and_tabs/window_management.talon). -- `running list` will toggle a GUI list of words you can say to switch to running applications. -- `focus chrome` will focus the chrome application. -- `launch music` will launch the music application. Note this is currently only implemented on Mac OS X. +- `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 -https://github.com/talonhub/community/blob/main/plugin/screenshot/screenshot.talon +See [screenshot.talon](plugin/screenshot/screenshot.talon). -### Programming Languages +### 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. -The commands for enabling languages are defined here: https://github.com/talonhub/community/blob/main/core/modes/language_modes.talon +Commands for enabling languages are defined in [language_modes.talon](core/modes/language_modes.talon). -By default, title tracking activates coding languages in supported applications such as VSCode, Visual Studio (requires plugin), and Notepad++. +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. The active filename (including extension) must be included in the editor's title -2. Implement the required Talon-defined `filename` action to correctly extract the filename from the programs's title. See https://github.com/talonhub/community/blob/main/apps/vscode/vscode.py#L122-L138 for an example. +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 currently broken up into several tags in an attempt to define a common grammar where possible between languages. Each tag is defined by a .talon file, which defines the voice commands, and a Python file which declares the actions that should be implemented by each concrete language implementation to support those voice commands. Currently, the tags which are available are: +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 `/** */`) @@ -190,51 +189,42 @@ Python, C#, Talon and javascript language support is currently broken up into se - `lang/tags/operators_math.{talon,py}` - numeric, comparison, and logical operators - `lang/tags/operators_pointer.{talon,py}` - pointer operators (e.g., C's `&x`) -The support for the language-specific implementations of actions are then located in: +Language-specific implementations of the above features are in files named `lang/{your-language}/{your-language}.py`. -- `lang/{your-language}/{your-language}.py` - -To start support for a new language, ensure the appropriate extension is added to the [`language_extensions` in language_modes.py](https://github.com/talonhub/community/blob/main/core/modes/language_modes.py#L9). -Then create the following files: +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. -If you wish to add additional voice commands for your language, put those in `{your-language}.talon`. -You may also want to add a force command to `language_modes.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 +## File manager commands -For the following file manager commands to work, your file manager must display the full folder path in the title bar. https://github.com/talonhub/community/blob/main/tags/file_manager/file_manager.talon +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 Mac OS X's Finder, run this command in terminal to display the full path in the title. +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 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 folder 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. +- 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. -To implement support for a new program, you need to implement the relevant file manager actions for your application and assert the user.file_manager tag. +- 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. -- There are a number of example implementations in the repository. Finder is a good example to copy and customize to your application as needed. - https://github.com/talonhub/community/blob/main/apps/finder/finder.py +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 mdoify. ## Terminal commands -Many terminal programs are supported out of the box, but you may not want all the commands enabled. +Many terminal applications are supported out of the box, but you may not want all the commands enabled. -To disable various commandsets in your terminal, find the relevant talon file and enable/disable the tags for command sets as appropriate. +To use command sets in your terminal applications, enable/disable the corresponding tags in the terminal application-specific .talon file. ``` tag(): user.file_manager @@ -245,11 +235,11 @@ 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](https://github.com/talonhub/community/tree/main/tags), others, like git, are defined within [apps](https://github.com/talonhub/community/tree/main/apps). Additionally, the commands for tabs are defined in [tabs.talon](https://github.com/talonhub/community/blob/main/core/windows_and_tabs/tabs.talon). +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. OSX) or Linux computer, you can enable support for a number of +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): @@ -258,8 +248,7 @@ line in [unix_shell.py](tags/terminal/unix_shell.py): ``` Once you have uncommented the line, you can customize your utility commands by editing -`settings/unix_utilities.csv`. Note: this directory is created when first running Talon -with community enabled. +`tags/terminal/unix_utility.talon-list`. ## Jetbrains commands @@ -271,9 +260,9 @@ into each editor. 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](https://github.com/talonhub/community/blob/main/core/README.md) -- The lang folder has commands for writing [programming languages](https://github.com/talonhub/community?tab=readme-ov-file#programming-languages) -- The plugin folder has various commands described [here](https://github.com/talonhub/community/blob/main/plugin/README.md) +- 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 @@ -282,15 +271,44 @@ Several options are configurable via a [single settings file](settings.talon) ou 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%. +- `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. -• `user.help_max_command_lines_per_page` and `user.help_max_contexts_per_page` to ensure all help information is visible. +## Customizing words and lists -• `user.mouse_wheel_down_amount` and `user.mouse_continuous_scroll_amount` for adjusting the scroll amounts for the various scroll commands. +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). -Also, you can add additional vocabulary words, words to replace, search engines and more. Complete the community setup instructions above, then open the `settings` folder to see the provided CSV files and customize them as needed. +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` or `words to replace`. 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 +## 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! @@ -351,7 +369,7 @@ To run the test suite you just need to install the `pytest` python package in to 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/ +For community-generated documentation on Talon, please visit https://talon.wiki/. ## Alternate installation method: Zip file @@ -359,6 +377,6 @@ It is possible to install `community` by downloading and extracting a zip file i 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. 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 taskbar, clicking Scripting > Open ~/talon, and navigating to `user`. +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/apps/emacs/emacs_commands.py b/apps/emacs/emacs_commands.py index 9618b7c388..12fd817285 100644 --- a/apps/emacs/emacs_commands.py +++ b/apps/emacs/emacs_commands.py @@ -33,10 +33,9 @@ def emacs_command_short_form(command_name: str) -> Optional[str]: return emacs_commands.get(command_name, Command(command_name)).short -def load_csv(): - filepath = Path(__file__).parents[0] / "emacs_commands.csv" - with resource.open(filepath) as f: - rows = list(csv.reader(f)) +@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"] @@ -46,7 +45,7 @@ def load_csv(): continue if len(row) > 4: print( - f'"{filepath}": More than four values in row: {row}. ' + f"emacs_commands.csv: More than four values in row: {row}. " + " Ignoring the extras" ) name, keys, short, spoken = ( @@ -70,7 +69,3 @@ def load_csv(): if c.spoken: command_list[c.spoken] = c.name ctx.lists["self.emacs_command"] = command_list - - -# TODO: register on change to file! -app.register("ready", load_csv) diff --git a/apps/git/git.py b/apps/git/git.py index e9010f0109..01acd5df40 100644 --- a/apps/git/git.py +++ b/apps/git/git.py @@ -10,32 +10,6 @@ mod.list("git_command", desc="Git commands.") mod.list("git_argument", desc="Command-line git options and arguments.") -dirpath = Path(__file__).parent -arguments_csv_path = str(dirpath / "git_arguments.csv") -commands_csv_path = str(dirpath / "git_commands.csv") - - -def make_list(path): - with resource.open(path, "r") as f: - rows = list(csv.reader(f)) - mapping = {} - # ignore header row - for row in rows[1:]: - if len(row) == 0: - continue - if len(row) == 1: - row = row[0], row[0] - if len(row) > 2: - print("{path!r}: More than two values in row: {row}. Ignoring the extras.") - output, spoken_form = row[:2] - spoken_form = spoken_form.strip() - mapping[spoken_form] = output - return mapping - - -ctx.lists["self.git_argument"] = make_list(arguments_csv_path) -ctx.lists["self.git_command"] = make_list(commands_csv_path) - @mod.capture(rule="{user.git_argument}+") def git_arguments(m) -> str: diff --git a/apps/git/git_argument.talon-list b/apps/git/git_argument.talon-list new file mode 100644 index 0000000000..ac991af401 --- /dev/null +++ b/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/apps/git/git_arguments.csv b/apps/git/git_arguments.csv deleted file mode 100644 index f0abf0c85f..0000000000 --- a/apps/git/git_arguments.csv +++ /dev/null @@ -1,68 +0,0 @@ -Option, Spoken form ---abort, abort ---all, all ---allow-empty, allow empty ---amend, amend ---cached, cached ---cached, cashed ---color-words, color words ---color-words, colour words ---continue, continue ---copy, copy ---create, create ---delete, delete ---detach, detach ---dir-diff, dir diff ---dir-diff, directory diff ---dry-run, dry run ---edit, edit ---ff-only, fast forward 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-upstream, set up stream ---set-upstream-to, set up stream to ---short, short ---shortstat, short stat ---skip, skip ---soft, soft ---staged, staged ---stat, stat ---system, system ---track, track ---update, update ---verbose, verbose --b, branch --c, combined --d, deep --vv, very verbose -HEAD -main -master -origin -upstream -origin/main,origin main -origin/master,origin master diff --git a/apps/git/git_commands.csv b/apps/git/git_command.talon-list similarity index 59% rename from apps/git/git_commands.csv rename to apps/git/git_command.talon-list index 50235bb33d..04655a3254 100644 --- a/apps/git/git_commands.csv +++ b/apps/git/git_command.talon-list @@ -1,38 +1,39 @@ -Subcommand, Spoken form (optional) +list: user.git_command +- add archive bisect blame branch checkout -cherry-pick, cherry pick +cherry pick: cherry-pick clean clone commit config diff -difftool, diff tool +diff tool: difftool fetch gc grep help -init, in it +in it: init log -ls-files, ls files +ls files: ls-files merge -mergetool, merge tool -mv, move +merge tool: mergetool +move: mv pull push -range-diff, range diff +range diff: range-diff rebase -reflog,ref log +ref log: reflog remote remote add remote remove remote rename -remote set-url, remote set url -remote set-url, remote set you are el +remote set url: remote set-url +remote set you are el: remote set-url remote show rerere rerere diff @@ -40,10 +41,10 @@ rerere status reset restore revert -rm, remove -shortlog,short log +remove: rm +short log: shortlog show -sparse-checkout,sparse checkout +sparse checkout: sparse-checkout stash stash apply stash list @@ -54,7 +55,7 @@ stash save status submodule submodule add -submodule init, submodule in it +submodule in it: submodule init submodule status submodule update switch diff --git a/apps/talon/talon_repl/talon_repl.py b/apps/talon/talon_repl/talon_repl.py new file mode 100644 index 0000000000..ca7a968b05 --- /dev/null +++ b/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/apps/talon/talon_repl/talon_repl.talon b/apps/talon/talon_repl/talon_repl.talon index 7081206fca..4f0fd512db 100644 --- a/apps/talon/talon_repl/talon_repl.talon +++ b/apps/talon/talon_repl/talon_repl.talon @@ -1,7 +1,7 @@ -win.title: /repl/ -win.title: /Talon - REPL/ +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. diff --git a/apps/vscode/vscode.py b/apps/vscode/vscode.py index b8e8f1c36d..929c8bd499 100644 --- a/apps/vscode/vscode.py +++ b/apps/vscode/vscode.py @@ -26,6 +26,8 @@ and app.name: VSCodium os: linux and app.name: Codium +os: linux +and app.name: Cursor """ mod.apps.vscode = r""" os: windows diff --git a/core/abbreviate/abbreviate.py b/core/abbreviate/abbreviate.py index a4fb9cf74c..cb43d5c192 100644 --- a/core/abbreviate/abbreviate.py +++ b/core/abbreviate/abbreviate.py @@ -1,11 +1,14 @@ +import re + from talon import Context, Module -from ..user_settings import get_list_from_csv +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", @@ -445,18 +448,26 @@ "work in progress": "wip", } -# This variable is also considered exported for the create_spoken_forms module -abbreviations_list = get_list_from_csv( - "abbreviations.csv", - headers=("Abbreviation", "Spoken Form"), - default=abbreviations, + +@track_csv_list( + "abbreviations.csv", headers=("Abbreviation", "Spoken Form"), default=abbreviations ) +def on_abbreviations(values): + global abbreviations_list -# Allows the abbreviated/short form to be used as spoken phrase. eg "brief app" -> app -abbreviations_list_with_values = { - **{v: v for v in abbreviations_list.values()}, - **abbreviations_list, -} + # note: abbreviations_list is imported by the create_spoken_forms module + abbreviations_list = values -ctx = Context() -ctx.lists["user.abbreviation"] = abbreviations_list_with_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/core/create_spoken_forms.py b/core/create_spoken_forms.py index 02fe77b098..508e8a93ff 100644 --- a/core/create_spoken_forms.py +++ b/core/create_spoken_forms.py @@ -6,34 +6,58 @@ from talon import Module, actions -from .abbreviate.abbreviate import abbreviations_list -from .file_extension.file_extension import file_extensions from .keys.keys import symbol_key_words 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]+" -FILE_EXTENSIONS_REGEX = "|".join( - re.escape(file_extension.strip()) + "$" - for file_extension in file_extensions.values() -) SYMBOLS_REGEX = "|".join(re.escape(symbol) for symbol in set(symbol_key_words.values())) -REGEX_NO_SYMBOLS = re.compile( - "|".join( - [ - FANCY_REGULAR_EXPRESSION, - FILE_EXTENSIONS_REGEX, - ] +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 -REGEX_WITH_SYMBOLS = re.compile( - "|".join([FANCY_REGULAR_EXPRESSION, FILE_EXTENSIONS_REGEX, SYMBOLS_REGEX]) -) REVERSE_PRONUNCIATION_MAP = { **{str(value): key for key, value in digits_map.items()}, diff --git a/core/edit/edit_command.py b/core/edit/edit_command.py index 5f274c44c9..a4cb58e01b 100644 --- a/core/edit/edit_command.py +++ b/core/edit/edit_command.py @@ -17,7 +17,7 @@ ("delete", "word"): actions.edit.delete_word, ("delete", "line"): actions.edit.delete_line, ("delete", "paragraph"): actions.edit.delete_paragraph, - ("delete", "document"): actions.edit.delete_all, + # ("delete", "document"): actions.edit.delete_all, # Beta only # Cut to clipboard ("cutToClipboard", "line"): actions.user.cut_line, } diff --git a/core/edit_settings.talon b/core/edit_settings.talon deleted file mode 100644 index 386180ea27..0000000000 --- a/core/edit_settings.talon +++ /dev/null @@ -1,4 +0,0 @@ -customize {user.talon_settings_csv}: - user.edit_text_file(talon_settings_csv) - sleep(500ms) - edit.file_end() diff --git a/core/edit_text_file.py b/core/edit_text_file.py index f246baf763..cab76c9635 100644 --- a/core/edit_text_file.py +++ b/core/edit_text_file.py @@ -9,25 +9,39 @@ mod = Module() ctx = Context() +mod.list( + "edit_file", + desc="Absolute paths to frequently edited files (Talon list, CSV, etc.)", +) + +_edit_files = { + "additional words": os.path.join( + REPO_DIR, "core", "vocabulary", "vocabulary.talon-list" + ), + "alphabet": os.path.join(REPO_DIR, "core", "keys", "letter.talon-list"), + "homophones": os.path.join(REPO_DIR, "core", "homophones", "homophones.csv"), + "search engines": os.path.join( + REPO_DIR, "core", "websites_and_search_engines", "search_engine.talon-list" + ), + "unix utilities": os.path.join( + REPO_DIR, "tags", "terminal", "unix_utility.talon-list" + ), + "websites": os.path.join( + REPO_DIR, "core", "websites_and_search_engines", "website.talon-list" + ), +} -mod.list("talon_settings_csv", desc="Absolute paths to talon user settings csv files.") -_csvs = { +_settings_csvs = { name: os.path.join(SETTINGS_DIR, file_name) for name, file_name in { "abbreviations": "abbreviations.csv", - "additional words": "additional_words.csv", - "alphabet": "alphabet.csv", - "directories": "directories.csv", "file extensions": "file_extensions.csv", - "search engines": "search_engines.csv", - "system paths": "system_paths.csv", - "unix utilities": "unix_utilities.csv", - "websites": "websites.csv", "words to replace": "words_to_replace.csv", }.items() } -_csvs["homophones"] = os.path.join(REPO_DIR, "core", "homophones", "homophones.csv") -ctx.lists["self.talon_settings_csv"] = _csvs + +_edit_files.update(_settings_csvs) +ctx.lists["self.edit_file"] = _edit_files @mod.action_class diff --git a/core/edit_text_file.talon b/core/edit_text_file.talon new file mode 100644 index 0000000000..690c462c3e --- /dev/null +++ b/core/edit_text_file.talon @@ -0,0 +1,4 @@ +customize {user.edit_file}: + user.edit_text_file(edit_file) + sleep(500ms) + edit.file_end() diff --git a/core/file_extension/file_extension.py b/core/file_extension/file_extension.py index 04a1c23ca2..1a75e76c42 100644 --- a/core/file_extension/file_extension.py +++ b/core/file_extension/file_extension.py @@ -1,6 +1,6 @@ from talon import Context, Module -from ..user_settings import get_list_from_csv +from ..user_settings import track_csv_list mod = Module() mod.list("file_extension", desc="A file extension, such as .py") @@ -55,11 +55,13 @@ "dot log": ".log", } -file_extensions = get_list_from_csv( +ctx = Context() + + +@track_csv_list( "file_extensions.csv", headers=("File extension", "Name"), default=_file_extensions_defaults, ) - -ctx = Context() -ctx.lists["self.file_extension"] = file_extensions +def on_update(values): + ctx.lists["self.file_extension"] = values diff --git a/core/homophones/homophones.py b/core/homophones/homophones.py index 1d24826b3a..32cda848ff 100644 --- a/core/homophones/homophones.py +++ b/core/homophones/homophones.py @@ -39,6 +39,7 @@ def update_homophones(name, flags): 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: @@ -93,7 +94,6 @@ def raise_homophones(word_to_find_homophones_for, forced=False, selection=False) 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 ) @@ -140,7 +140,6 @@ def raise_homophones(word_to_find_homophones_for, forced=False, selection=False) clip.set(new) actions.edit.paste() - return ctx.tags = ["user.homophones_open"] diff --git a/core/keys/arrow_key.talon-list b/core/keys/arrow_key.talon-list new file mode 100644 index 0000000000..42bc4779b8 --- /dev/null +++ b/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/core/keys/function_key.talon-list b/core/keys/function_key.talon-list new file mode 100644 index 0000000000..936eec4729 --- /dev/null +++ b/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/core/keys/keypad_key.talon-list b/core/keys/keypad_key.talon-list new file mode 100644 index 0000000000..cf4e251222 --- /dev/null +++ b/core/keys/keypad_key.talon-list @@ -0,0 +1,19 @@ +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 diff --git a/core/keys/keys.py b/core/keys/keys.py index a325fe7c2d..59855b4e97 100644 --- a/core/keys/keys.py +++ b/core/keys/keys.py @@ -1,28 +1,4 @@ -from talon import Context, Module, app - -from ..user_settings import get_list_from_csv - - -def setup_default_alphabet(): - """set up common default alphabet. - - no need to modify this here, change your alphabet using alphabet.csv""" - initial_default_alphabet = "air bat cap drum each fine gust harp sit jury crunch look made near odd pit quench red sun trap urge vest whale plex yank zip".split() - initial_letters_string = "abcdefghijklmnopqrstuvwxyz" - initial_default_alphabet_dict = dict( - zip(initial_default_alphabet, initial_letters_string) - ) - - return initial_default_alphabet_dict - - -alphabet_list = get_list_from_csv( - "alphabet.csv", ("Letter", "Spoken Form"), setup_default_alphabet() -) - -# used for number keys & function keys respectively -digits = "zero one two three four five six seven eight nine".split() -f_digits = "one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty".split() +from talon import Context, Module, actions, app mod = Module() mod.list("letter", desc="The spoken phonetic alphabet") @@ -32,6 +8,7 @@ def setup_default_alphabet(): 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") @@ -59,6 +36,12 @@ def number_key(m) -> str: 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" @@ -91,7 +74,7 @@ def any_alphanumeric_key(m) -> str: @mod.capture( rule="( | | " - "| | | )" + "| | | | )" ) def unmodified_key(m) -> str: "A single key with no modifiers" @@ -121,18 +104,6 @@ def letters(m) -> str: ctx = Context() -modifier_keys = { - # If you find 'alt' is often misrecognized, try using 'alter'. - "alt": "alt", #'alter': 'alt', - "control": "ctrl", #'troll': 'ctrl', - "shift": "shift", #'sky': 'shift', - "super": "super", -} -if app.platform == "mac": - modifier_keys["command"] = "cmd" - modifier_keys["option"] = "alt" -ctx.lists["self.modifier_key"] = modifier_keys -ctx.lists["self.letter"] = alphabet_list # `punctuation_words` is for words you want available BOTH in dictation and as key names in command mode. # `symbol_key_words` is for key names that should be available in command mode, but NOT during dictation. @@ -230,42 +201,3 @@ def letters(m) -> str: symbol_key_words.update(punctuation_words) ctx.lists["self.punctuation"] = punctuation_words ctx.lists["self.symbol_key"] = symbol_key_words -ctx.lists["self.number_key"] = {name: str(i) for i, name in enumerate(digits)} -ctx.lists["self.arrow_key"] = { - "down": "down", - "left": "left", - "right": "right", - "up": "up", -} - -simple_keys = [ - "end", - "enter", - "escape", - "home", - "insert", - "pagedown", - "pageup", - "space", - "tab", -] - -alternate_keys = { - "wipe": "backspace", - "delete": "backspace", - #'junk': 'backspace', - "forward delete": "delete", - "page up": "pageup", - "page down": "pagedown", -} -# mac apparently doesn't have the menu key. -if app.platform in ("windows", "linux"): - alternate_keys["menu key"] = "menu" - alternate_keys["print screen"] = "printscr" - -special_keys = {k: k for k in simple_keys} -special_keys.update(alternate_keys) -ctx.lists["self.special_key"] = special_keys -ctx.lists["self.function_key"] = { - f"F {name}": f"f{i}" for i, name in enumerate(f_digits, start=1) -} diff --git a/core/keys/letter.talon-list b/core/keys/letter.talon-list new file mode 100644 index 0000000000..824a2939fc --- /dev/null +++ b/core/keys/letter.talon-list @@ -0,0 +1,30 @@ +list: user.letter +- +# for common alternative spoken forms for letters, visit +# https://talon.wiki/quickstart/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/core/keys/mac/modifier_key.talon-list b/core/keys/mac/modifier_key.talon-list new file mode 100644 index 0000000000..589235d5b9 --- /dev/null +++ b/core/keys/mac/modifier_key.talon-list @@ -0,0 +1,9 @@ +list: user.modifier_key +os: mac +- +alt: alt +control: ctrl +shift: shift +super: cmd +command: cmd +option: alt diff --git a/core/keys/mac/special_key.talon-list b/core/keys/mac/special_key.talon-list new file mode 100644 index 0000000000..27f2d877ad --- /dev/null +++ b/core/keys/mac/special_key.talon-list @@ -0,0 +1,16 @@ +list: user.special_key +os: mac +- + +end: end +home: home +minus: minus +enter: enter +page down: pagedown +page up: pageup +escape: escape +space: space +tab: tab +wipe: backspace +delete: backspace +forward delete: delete diff --git a/core/keys/number_key.talon-list b/core/keys/number_key.talon-list new file mode 100644 index 0000000000..b83e24b2a8 --- /dev/null +++ b/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/core/keys/win/modifier_key.talon-list b/core/keys/win/modifier_key.talon-list new file mode 100644 index 0000000000..4ab343a343 --- /dev/null +++ b/core/keys/win/modifier_key.talon-list @@ -0,0 +1,11 @@ +list: user.modifier_key +os: windows +os: linux +- +alt: alt +control: ctrl +shift: shift +# super is the windows key +super: super +command: ctrl +option: alt diff --git a/core/keys/win/special_key.talon-list b/core/keys/win/special_key.talon-list new file mode 100644 index 0000000000..2e921d9640 --- /dev/null +++ b/core/keys/win/special_key.talon-list @@ -0,0 +1,19 @@ +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 +wipe: backspace +delete: backspace +forward delete: delete +menu key: menu +print screen: printscr diff --git a/core/snippets/snippets/elseIfStatement.snippet b/core/snippets/snippets/elseIfStatement.snippet index f9405931ad..6aec0a370d 100644 --- a/core/snippets/snippets/elseIfStatement.snippet +++ b/core/snippets/snippets/elseIfStatement.snippet @@ -20,3 +20,9 @@ language: python elif $1: $0 --- + +language: lua +- +elseif $1 then + $0 +--- diff --git a/core/snippets/snippets/elseStatement.snippet b/core/snippets/snippets/elseStatement.snippet index 6c976bdd25..50d00379bb 100644 --- a/core/snippets/snippets/elseStatement.snippet +++ b/core/snippets/snippets/elseStatement.snippet @@ -18,3 +18,9 @@ language: python else: $0 --- + +language: lua +- +else + $0 +--- diff --git a/core/snippets/snippets/ifStatement.snippet b/core/snippets/snippets/ifStatement.snippet index d41a6ba3a2..9d2cdf0fe6 100644 --- a/core/snippets/snippets/ifStatement.snippet +++ b/core/snippets/snippets/ifStatement.snippet @@ -20,3 +20,10 @@ language: python if $1: $0 --- + +language: lua +- +if $1 then + $0 +end +--- diff --git a/core/snippets/snippets/lambdaExpression.snippet b/core/snippets/snippets/lambdaExpression.snippet index d33bd19426..4227601dd0 100644 --- a/core/snippets/snippets/lambdaExpression.snippet +++ b/core/snippets/snippets/lambdaExpression.snippet @@ -1,5 +1,8 @@ name: lambdaExpression phrase: lambda + +$0.wrapperPhrase: lambda +$0.wrapperScope: statement --- language: javascript diff --git a/core/snippets/snippets/lua.snippet b/core/snippets/snippets/lua.snippet new file mode 100644 index 0000000000..b4be8de438 --- /dev/null +++ b/core/snippets/snippets/lua.snippet @@ -0,0 +1,21 @@ +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 +--- diff --git a/core/snippets/snippets/ternary.snippet b/core/snippets/snippets/ternary.snippet index c7639a8665..20641f9ca2 100644 --- a/core/snippets/snippets/ternary.snippet +++ b/core/snippets/snippets/ternary.snippet @@ -11,3 +11,8 @@ language: python - $1 if $2 else $0 --- + +language: lua +- +$1 and $2 or $0 +--- diff --git a/core/system_paths.py b/core/system_paths.py index e3aad89c01..8cf1ae6db2 100644 --- a/core/system_paths.py +++ b/core/system_paths.py @@ -1,60 +1,69 @@ """ 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.csv in the settings folder so they user can easily add their own custom paths. +system_paths-.talon-list in the core folder so the user can easily add their own +custom paths. """ -import os +from pathlib import Path -from talon import Context, Module, actions, app - -from .user_settings import get_list_from_csv +from talon import Context, Module, actions, app, registry mod = Module() -ctx = Context() - mod.list("system_paths", desc="List of system paths") -user_path = os.path.expanduser("~") - -# We need to wait for ready before we can call "actions.path.talon_home()" and -# "actions.path.talon_user()" def on_ready(): - default_system_paths = { - "user": user_path, - "profile": user_path, - "desktop": os.path.join(user_path, "Desktop"), - "desk": os.path.join(user_path, "Desktop"), - "documents": os.path.join(user_path, "Documents"), - "docks": os.path.join(user_path, "Documents"), - "downloads": os.path.join(user_path, "Downloads"), - "music": os.path.join(user_path, "Music"), - "pictures": os.path.join(user_path, "Pictures"), - "videos": os.path.join(user_path, "Videos"), - "talon home": str(actions.path.talon_home()), - "talon user": str(actions.path.talon_user()), - } + # If user.system_paths defined otherwise, don't generate a file + if registry.lists["user.system_paths"][0]: + return - if app.platform == "windows": - 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"))): - onedrive_paths = { - "desktop": os.path.join(one_drive_path, "Desktop"), - "documents": os.path.join(one_drive_path, "Documents"), - "one drive": one_drive_path, - "pictures": os.path.join(one_drive_path, "Pictures"), - } + hostname = actions.user.talon_get_hostname() + system_paths = Path(__file__).with_name(f"system_paths-{hostname}.talon-list") + if system_paths.is_file(): + return - default_system_paths.update(onedrive_paths) + home = Path.home() + talon_home = Path(actions.path.talon_home()) - system_paths = get_list_from_csv( - "system_paths.csv", headers=("Path", "Spoken"), default=default_system_paths - ) + 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(), + } - ctx.lists["user.system_paths"] = system_paths + 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}") diff --git a/core/text/phrase_ender.talon-list b/core/text/phrase_ender.talon-list new file mode 100644 index 0000000000..b1a2ac9b86 --- /dev/null +++ b/core/text/phrase_ender.talon-list @@ -0,0 +1,4 @@ +list: user.phrase_ender +- + +over: "" diff --git a/core/text/text.talon b/core/text/text.talon index 619825ad4f..cea870f145 100644 --- a/core/text/text.talon +++ b/core/text/text.talon @@ -2,13 +2,17 @@ phrase $: user.add_phrase_to_history(text) insert(text) -phrase over: +phrase {user.phrase_ender}: user.add_phrase_to_history(text) - insert(text) + insert("{text}{phrase_ender}") {user.prose_formatter} $: user.insert_formatted(prose, prose_formatter) -{user.prose_formatter} over: 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) -+ over: 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) diff --git a/core/text/text_and_dictation.py b/core/text/text_and_dictation.py index e7caa580b3..6f0724e2e7 100644 --- a/core/text/text_and_dictation.py +++ b/core/text/text_and_dictation.py @@ -15,6 +15,7 @@ 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") ctx = Context() # Maps spoken forms to DictationFormat method names (see DictationFormat below). ctx.lists["user.prose_modifiers"] = { diff --git a/core/user_settings.py b/core/user_settings.py index 2630f2c518..6e08cfe035 100644 --- a/core/user_settings.py +++ b/core/user_settings.py @@ -1,35 +1,23 @@ import csv import os 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/knausj folder. SETTINGS_DIR = Path(__file__).parents[1] / "settings" +SETTINGS_DIR.mkdir(exist_ok=True) -if not SETTINGS_DIR.is_dir(): - os.mkdir(SETTINGS_DIR) +CallbackT = Callable[[dict[str, str]], None] +DecoratorT = Callable[[CallbackT], CallbackT] -def get_list_from_csv( - filename: str, headers: tuple[str, str], default: dict[str, str] = {} -): - """Retrieves list from CSV""" - path = SETTINGS_DIR / filename - assert filename.endswith(".csv") - - if not path.is_file(): - with open(path, "w", encoding="utf-8", newline="") as file: - writer = csv.writer(file) - writer.writerow(headers) - for key, value in default.items(): - writer.writerow([key] if key == value else [value, key]) - - # Now read via resource to take advantage of talon's - # ability to reload this script for us when the resource changes - with resource.open(str(path), "r") as f: - rows = list(csv.reader(f)) +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 = {} @@ -37,7 +25,7 @@ def get_list_from_csv( actual_headers = rows[0] if not actual_headers == list(headers): print( - f'"{filename}": Malformed headers - {actual_headers}.' + f'"{f.name}": Malformed headers - {actual_headers}.' + f" Should be {list(headers)}. Ignoring row." ) for row in rows[1:]: @@ -47,10 +35,14 @@ def get_list_from_csv( if len(row) == 1: output = spoken_form = row[0] else: - output, spoken_form = row[:2] + if is_spoken_form_first: + spoken_form, output = row[:2] + else: + output, spoken_form = row[:2] + if len(row) > 2: print( - f'"{filename}": More than two values in row: {row}.' + f'"{f.name}": More than two values in row: {row}.' + " Ignoring the extras." ) # Leading/trailing whitespace in spoken form can prevent recognition. @@ -60,6 +52,44 @@ def get_list_from_csv( 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") 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, +) -> DecoratorT: + assert filename.endswith(".csv") + path = 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]): path = SETTINGS_DIR / filename assert filename.endswith(".csv") diff --git a/core/vocabulary/vocabulary.py b/core/vocabulary/vocabulary.py index d3b35ad9af..3792138ed1 100644 --- a/core/vocabulary/vocabulary.py +++ b/core/vocabulary/vocabulary.py @@ -1,18 +1,18 @@ 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, get_list_from_csv +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 @@ -43,45 +43,7 @@ # 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 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. -phrases_to_replace = get_list_from_csv( - "words_to_replace.csv", - headers=("Replacement", "Original"), - default=_word_map_defaults, -) - -# "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"] = phrases_to_replace - - -# Default words that should be added to Talon's vocabulary. -# Don't edit this. Edit 'additional_vocabulary.csv' instead -_simple_vocab_default = ["nmap", "admin", "Cisco", "Citrix", "VPN", "DNS", "Minecraft"] - -# Defaults for different pronounciations of words that need to be added to -# Talon's vocabulary. -_default_vocabulary = { - "N map": "nmap", - "under documented": "under-documented", -} -_default_vocabulary.update({word: word for word in _simple_vocab_default}) - -# "user.vocabulary" is used to explicitly add words/phrases that Talon doesn't -# recognize. Words in user.vocabulary (or other lists and captures) are -# "command-like" and their recognition is prioritized over ordinary words. -vocabulary = get_list_from_csv( - "additional_words.csv", - headers=("Word(s)", "Spoken Form (If Different)"), - default=_default_vocabulary, -) -ctx.lists["user.vocabulary"] = vocabulary +phrases_to_replace = {} class PhraseReplacer: @@ -93,7 +55,10 @@ class PhraseReplacer: - phrase_dict: dictionary mapping recognized/spoken forms to written forms """ - def __init__(self, phrase_dict: dict[str, str]): + 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(): @@ -143,7 +108,8 @@ def replace_string(self, text: str) -> str: # Unit tests for PhraseReplacer -rep = PhraseReplacer( +rep = PhraseReplacer() +rep.update( { "this": "foo", "that": "bar", @@ -159,7 +125,27 @@ def replace_string(self, text: str) -> str: 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) +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") @@ -194,11 +180,11 @@ def _create_vocabulary_entries(spoken_form, written_form, type): # 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_csv( +def _add_selection_to_file( phrase: Union[Phrase, str], type: str, - csv: str, - csv_contents: dict[str, str], + file_name: str, + file_contents: dict[str, str], skip_identical_replacement: bool, ): written_form = actions.edit.selected_text().strip() @@ -208,32 +194,75 @@ def _add_selection_to_csv( 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) - new_entries = {} added_some_phrases = False - 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 csv_contents: - actions.app.notify(f'Spoken form "{spoken_form}" is already in {csv}') - else: - new_entries[spoken_form] = written_form - added_some_phrases = True - append_to_csv(csv, new_entries) + + # until we add support for parsing or otherwise getting the active + # vocabulary.talon-list, skip the logic for checking for duplicates etc + if file_contents: + # clear the new entries dictionary + new_entries = {} + for spoken_form, written_form in entries.items(): + if skip_identical_replacement and spoken_form == written_form: + actions.app.notify(f'Skipping identical replacement: "{spoken_form}"') + elif spoken_form in file_contents: + actions.app.notify( + f'Spoken form "{spoken_form}" is already in {file_name}' + ) + else: + new_entries[spoken_form] = written_form + added_some_phrases = True + else: + new_entries = entries + added_some_phrases = True + + 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 {csv}: {new_entries}") + 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: + 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_csv( + _add_selection_to_file( phrase, type, - "additional_words.csv", - vocabulary, + "vocabulary.talon-list", + None, False, ) @@ -241,7 +270,7 @@ 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_csv( + _add_selection_to_file( phrase, type, "words_to_replace.csv", diff --git a/core/vocabulary/vocabulary.talon-list b/core/vocabulary/vocabulary.talon-list new file mode 100644 index 0000000000..63b7c795e4 --- /dev/null +++ b/core/vocabulary/vocabulary.talon-list @@ -0,0 +1,11 @@ +list: user.vocabulary +- +N map: nmap +under documented: under-documented +nmap +admin +Cisco +Citrix +VPN +DNS +Minecraft diff --git a/core/websites_and_search_engines/search_engine.talon-list b/core/websites_and_search_engines/search_engine.talon-list new file mode 100644 index 0000000000..8f0ba95bcc --- /dev/null +++ b/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/core/websites_and_search_engines/website.talon-list b/core/websites_and_search_engines/website.talon-list new file mode 100644 index 0000000000..cff1c1a31b --- /dev/null +++ b/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/core/websites_and_search_engines/websites_and_search_engines.py b/core/websites_and_search_engines/websites_and_search_engines.py index eef2ad97a9..1b5f934cd9 100644 --- a/core/websites_and_search_engines/websites_and_search_engines.py +++ b/core/websites_and_search_engines/websites_and_search_engines.py @@ -1,9 +1,7 @@ import webbrowser from urllib.parse import quote_plus -from talon import Context, Module - -from ..user_settings import get_list_from_csv +from talon import Module mod = Module() mod.list("website", desc="A website.") @@ -12,49 +10,6 @@ desc="A search engine. Any instance of %s will be replaced by query text", ) -# Please do not edit these defaults. Instead, add / edit your own entries in -# settings/websites.csv in your user directory. -website_defaults = { - "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/", -} - -# Please do not edit these defaults. Instead, add / edit your own entries in -# settings/search_engines.csv in your user directory. -_search_engine_defaults = { - "amazon": "https://www.amazon.com/s/?field-keywords=%s", - "google": "https://www.google.com/search?q=%s", - "map": "https://maps.google.com/maps?q=%s", - "scholar": "https://scholar.google.com/scholar?q=%s", - "wiki": "https://en.wikipedia.org/w/index.php?search=%s", -} - -ctx = Context() -ctx.lists["self.website"] = get_list_from_csv( - "websites.csv", - headers=("URL", "Spoken name"), - default=website_defaults, -) -ctx.lists["self.search_engine"] = get_list_from_csv( - "search_engines.csv", - headers=("URL Template", "Name"), - default=_search_engine_defaults, -) - @mod.action_class class Actions: diff --git a/migration_helpers/migration_helpers.py b/migration_helpers/migration_helpers.py new file mode 100644 index 0000000000..0c32f82102 --- /dev/null +++ b/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/plugin/gamepad/gamepad.py b/plugin/gamepad/gamepad.py index 9ca7586390..165dc90c5e 100644 --- a/plugin/gamepad/gamepad.py +++ b/plugin/gamepad/gamepad.py @@ -282,7 +282,6 @@ def gamepad_mouse_jump(direction: str): """Move the mouse cursor to the specified quadrant of the active screen""" x, y = ctrl.mouse_pos() rect = ui.screen_containing(x, y).rect - print(ui.active_window()) # Half distance between cursor and screen edge match direction: diff --git a/plugin/mouse/mouse.py b/plugin/mouse/mouse.py index d74f3f886f..4088b2fd22 100644 --- a/plugin/mouse/mouse.py +++ b/plugin/mouse/mouse.py @@ -59,6 +59,12 @@ default=False, desc="When enabled, pop stops continuous scroll modes (wheel upper/downer/gaze)", ) +mod.setting( + "mouse_enable_pop_stops_drag", + type=bool, + default=False, + desc="When enabled, pop stops mouse drag", +) mod.setting( "mouse_enable_hiss_scroll", type=bool, @@ -132,17 +138,23 @@ def mouse_wake(): def mouse_drag(button: int): """Press and hold/release a specific mouse button for dragging""" # Clear any existing drags - self.mouse_drag_end() + actions.user.mouse_drag_end() # Start drag ctrl.mouse_click(button=button, down=True) def mouse_drag_end(): """Releases any held mouse buttons""" - buttons_held_down = list(ctrl.mouse_buttons_down()) - for button in buttons_held_down: + for button in ctrl.mouse_buttons_down(): ctrl.mouse_click(button=button, up=True) + def mouse_drag_toggle(button: int): + """If the button is held down, release the button, else start dragging""" + if button in list(ctrl.mouse_buttons_down()): + ctrl.mouse_click(button=button, up=True) + else: + actions.user.mouse_drag(button=button) + def mouse_sleep(): """Disables control mouse, zoom mouse, and re-enables cursor""" actions.tracking.control_zoom_toggle(False) @@ -151,11 +163,7 @@ def mouse_sleep(): show_cursor_helper(True) stop_scroll() - - # todo: fixme temporary fix for drag command - button_down = len(list(ctrl.mouse_buttons_down())) > 0 - if button_down: - ctrl.mouse_click(button=0, up=True) + actions.user.mouse_drag_end() def mouse_scroll_down(amount: float = 1): """Scrolls down""" @@ -221,6 +229,13 @@ def mouse_gaze_scroll(): actions.tracking.control_toggle(True) control_mouse_forced = True + def mouse_gaze_scroll_toggle(): + """If not scrolling, start gaze scroll, else stop scrolling.""" + if continuous_scroll_mode == "": + actions.user.mouse_gaze_scroll() + else: + actions.user.mouse_scroll_stop() + def copy_mouse_position(): """Copy the current mouse position coordinates""" position = ctrl.mouse_pos() @@ -280,7 +295,12 @@ def show_cursor_helper(show): @ctx.action_class("user") class UserActions: def noise_trigger_pop(): - if settings.get("user.mouse_enable_pop_stops_scroll") and ( + if ( + settings.get("user.mouse_enable_pop_stops_drag") + and ctrl.mouse_buttons_down() + ): + actions.user.mouse_drag_end() + elif settings.get("user.mouse_enable_pop_stops_scroll") and ( gaze_job or scroll_job ): # Allow pop to stop scroll diff --git a/settings.talon b/settings.talon index c1212b8032..99e4331d5a 100644 --- a/settings.talon +++ b/settings.talon @@ -27,6 +27,9 @@ settings(): # If `true`, stop continuous scroll/gaze scroll with a pop user.mouse_enable_pop_stops_scroll = true + # If `true`, stop mouse drag with a pop + user.mouse_enable_pop_stops_drag = true + # Choose how pop click should work in 'control mouse' mode # 0 = off # 1 = on with eyetracker but not zoom mouse mode diff --git a/tags/emoji/emoji.csv b/tags/emoji/emoji.csv deleted file mode 100644 index af2fd8c148..0000000000 --- a/tags/emoji/emoji.csv +++ /dev/null @@ -1,28 +0,0 @@ -angry, 😠 -blushing, 😊 -broken heart, 💔 -clapping, 👏 -cool, 😎 -crying, 😭 -dancing, 💃 -disappointed, 😞 -eyes, 👀 -happy, 😀 -heart, ❤ -heart eyes, 😍 -hugging, 🤗 -kissing, 😗 -monocle, 🧐 -party, 🎉 -pleading, 🥺 -poop, 💩 -rofl, 🤣 -roll eyes, 🙄 -sad, 🙁 -shrugging, 🤷 -shushing, 🤫 -star eyes, 🤩 -thinking, 🤔 -thumbs down, 👎 -thumbs up, 👍 -worried, 😟 diff --git a/tags/emoji/emoji.py b/tags/emoji/emoji.py index 8034a6b8e1..f2c00cada0 100644 --- a/tags/emoji/emoji.py +++ b/tags/emoji/emoji.py @@ -16,19 +16,5 @@ path = Path(__file__).parents[0] mod.list("emoticon", desc="Western emoticons (ascii)") -with open(path / "emoticon.csv") as f: - ctx.lists["user.emoticon"] = { - k.strip(): v.strip() for k, v in [line.split(",", 1) for line in f] - } - mod.list("emoji", desc="Emoji (unicode)") -with open(path / "emoji.csv") as f: - ctx.lists["user.emoji"] = { - k.strip(): v.strip() for k, v in [line.split(",", 1) for line in f] - } - mod.list("kaomoji", desc="Eastern kaomoji (unicode)") -with open(path / "kaomoji.csv") as f: - ctx.lists["user.kaomoji"] = { - k.strip(): v.strip() for k, v in [line.split(",", 1) for line in f] - } diff --git a/tags/emoji/emoji.talon-list b/tags/emoji/emoji.talon-list new file mode 100644 index 0000000000..1a8ecdda8b --- /dev/null +++ b/tags/emoji/emoji.talon-list @@ -0,0 +1,30 @@ +list: user.emoji +- +angry: 😠 +blushing: 😊 +broken heart: 💔 +clapping: 👏 +cool: 😎 +crying: 😭 +dancing: 💃 +disappointed: 😞 +eyes: 👀 +happy: 😀 +heart: ❤ +heart eyes: 😍 +hugging: 🤗 +kissing: 😗 +monocle: 🧐 +party: 🎉 +pleading: 🥺 +poop: 💩 +rofl: 🤣 +roll eyes: 🙄 +sad: 🙁 +shrugging: 🤷 +shushing: 🤫 +star eyes: 🤩 +thinking: 🤔 +thumbs down: 👎 +thumbs up: 👍 +worried: 😟 diff --git a/tags/emoji/emoticon.csv b/tags/emoji/emoticon.csv deleted file mode 100644 index 790ad32866..0000000000 --- a/tags/emoji/emoticon.csv +++ /dev/null @@ -1,13 +0,0 @@ -broken heart, DecoratorT: + def decorator(fn: CallbackT) -> CallbackT: + extensions = { + "dot see sharp": ".cs", + } + abbreviations = {"source": "src", "whats app": "WhatsApp"} + if filename == "abbreviations.csv": + fn(abbreviations) + elif filename == "file_extensions.csv": + fn(extensions) + + return decorator + + # replace track_csv_list before importing create_spoken_forms + core.user_settings.track_csv_list = track_csv_list_test + import core.create_spoken_forms def test_excludes_words(): @@ -43,6 +72,7 @@ def test_expands_file_extensions(): assert "hi dot see sharp" in result def test_expands_abbreviations(): + result = actions.user.create_spoken_forms("src", None, 0, True) assert "source" in result