From 36f19a29f59ffe9285f17e4f7e7f8948934dde42 Mon Sep 17 00:00:00 2001 From: Colton <70598503+C-Loftus@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:44:10 -0400 Subject: [PATCH 1/9] update state to be class based for mypy --- core/callbacks.py | 26 +++++++++++--------- core/core-agnostic.py | 17 +++++++------ core/core-linux.py | 17 ++++++------- core/screenreader_ipc/ipc_schema.py | 2 -- nvda/nvda.py | 38 ++++++++++++----------------- 5 files changed, 47 insertions(+), 53 deletions(-) diff --git a/core/callbacks.py b/core/callbacks.py index 44a120b..2647e64 100644 --- a/core/callbacks.py +++ b/core/callbacks.py @@ -1,4 +1,10 @@ from talon import scope, registry, ui, actions, settings, speech_system, app +from typing import ClassVar, Optional + + +class CallbackState: + last_mode: ClassVar[Optional[str]] = None + last_title: ClassVar[Optional[str]] = None def on_phrase(parsed_phrase): @@ -42,19 +48,15 @@ def on_title_switch(win): # trime the title to 20 characters so super long addresses don't get read active_window_title = active_window_title[:20] - global last_title - if last_title == active_window_title: + if CallbackState.last_title == active_window_title: return - last_title = active_window_title + CallbackState.last_title = active_window_title actions.user.tts(f"{active_window_title}") - -last_mode = None - - def on_update_contexts(): - global last_mode + last_mode = CallbackState.last_mode + modes = scope.get("mode") or [] MIXED = "command" in modes and "dictation" in modes @@ -99,13 +101,13 @@ def on_update_contexts(): actions.user.tts("Talon dictation mode") if SLEEP: - last_mode = "sleep" + CallbackState.last_mode = "sleep" elif MIXED: - last_mode = "mixed" + CallbackState.last_mode = "mixed" elif COMMAND: - last_mode = "command" + CallbackState.last_mode = "command" elif DICTATION: - last_mode = "dictation" + CallbackState.last_mode = "dictation" def on_ready(): diff --git a/core/core-agnostic.py b/core/core-agnostic.py index 96643f9..955c526 100644 --- a/core/core-agnostic.py +++ b/core/core-agnostic.py @@ -3,15 +3,18 @@ and are agnostic to the tts voice being used or the operating system """ -from typing import Optional, Callable +from typing import Optional, Callable, ClassVar from talon import Module, actions, Context, settings, app mod = Module() ctx = Context() +class AgnosticState(): + speaker_cancel_callback: ClassVar[Optional[Callable]] = None + # We want to get the settings from the talon file but then update -# them locally here so we can change them globally via expose talon actions +# them locally here so we can change them globally via exposed talon actions def initialize_settings(): ctx.settings["user.echo_dictation"] = settings.get("user.echo_dictation", True) ctx.settings["user.echo_context"] = settings.get("user.echo_context", False) @@ -31,21 +34,19 @@ def set_cancel_callback(callback: Callable): Sets the callback to call when the current speaker is cancelled. Only necessary to set if the tts is coming from a subprocess where we need to store a handle """ - global speaker_cancel_callback - speaker_cancel_callback = callback + AgnosticState.speaker_cancel_callback = callback def cancel_current_speaker(): """Cancels the current speaker""" - global speaker_cancel_callback - if not speaker_cancel_callback: + if not AgnosticState.speaker_cancel_callback: return try: - speaker_cancel_callback() + AgnosticState.speaker_cancel_callback() except Exception as e: print(e) finally: - speaker_cancel_callback = None + AgnosticState.speaker_cancel_callback = None def braille(text: str): """Output braille with the screenreader""" diff --git a/core/core-linux.py b/core/core-linux.py index ff92592..737ed5b 100644 --- a/core/core-linux.py +++ b/core/core-linux.py @@ -1,15 +1,15 @@ from talon import Context, actions, settings import os import subprocess -from typing import Literal +from typing import Literal, ClassVar ctxLinux = Context() ctxLinux.matches = r""" os: linux """ - -speaker: Literal["espeak", "piper"] = "espeak" +class LinuxState(): + speaker: ClassVar[Literal["espeak", "piper"]] = "espeak" @ctxLinux.action_class("user") @@ -20,12 +20,11 @@ def toggle_reader(): def switch_voice(): """Switches the tts voice""" - global speaker - if speaker == "espeak": - speaker = "piper" + if LinuxState.speaker == "espeak": + LinuxState.speaker = "piper" actions.user.tts("Switched to piper") else: - speaker = "espeak" + LinuxState.speaker = "espeak" actions.user.tts("Switched to espeak") def tts(text: str, interrupt: bool = True): @@ -33,13 +32,13 @@ def tts(text: str, interrupt: bool = True): if interrupt: actions.user.cancel_current_speaker() - match speaker: + match LinuxState.speaker: case "espeak": actions.user.espeak(text) case "piper": actions.user.piper(text) case _: - raise ValueError(f"Unknown speaker {speaker}") + raise ValueError(f"Unknown speaker {LinuxState.speaker}") def espeak(text: str): """Text to speech with a robotic/narrator voice""" diff --git a/core/screenreader_ipc/ipc_schema.py b/core/screenreader_ipc/ipc_schema.py index acad2f2..85d7b15 100644 --- a/core/screenreader_ipc/ipc_schema.py +++ b/core/screenreader_ipc/ipc_schema.py @@ -16,7 +16,6 @@ "debug", ] - class ServerSpec(TypedDict): address: str port: str @@ -37,7 +36,6 @@ def generate_from(value: str): return member raise KeyError(f"Invalid status result: {value}") - class IPCServerResponse(TypedDict): processedCommands: List[str] returnedValues: List[Any] diff --git a/nvda/nvda.py b/nvda/nvda.py index 4098e85..4cef430 100644 --- a/nvda/nvda.py +++ b/nvda/nvda.py @@ -2,12 +2,18 @@ import os import ctypes import time +from typing import ClassVar mod = Module() ctx = Context() mod.tag("nvda_running", desc="If set, NVDA is running") +class NVDAState(): + # Check if we send the IPC command to NVDA at the start of the phrase + pre_phrase_sent: ClassVar[bool] = False + # Commands to re enable at the end of the phrase + reenable_commands: ClassVar[list[str]] = [] @mod.scope def set_nvda_running_tag(): @@ -144,19 +150,11 @@ def switch_voice(): """Switches the voice for the screen reader""" actions.user.tts("You must switch voice in NVDA manually") - -# Only send post:phrase callback if we sent a pre:phrase callback successfully -pre_phrase_sent = False - -reenable_commands = [] - - # By default the screen reader will allow you to press a key and interrupt the ph # rase however this does not work alongside typing given the fact that we are pres # sing keys. So we need to temporally disable it then re enable it at the end of # the phrase def disable_interrupt(_): - global pre_phrase_sent SLEEP_MODE = "sleep" in scope.get("mode") if ( not actions.user.is_nvda_running() @@ -178,8 +176,8 @@ def disable_interrupt(_): "disableSpeakTypedCharacters", ] results = actions.user.send_ipc_commands(commands) - global reenable_commands - reenable_commands = [] + + NVDAState.reenable_commands = [] for command, value in results: match command: case ( @@ -189,38 +187,34 @@ def disable_interrupt(_): | "getSpeakTypedCharacters" ) as cmd ) if value: - reenable_commands.append(cmd.replace("get", "enable")) + NVDAState.reenable_commands.append(cmd.replace("get", "enable")) - pre_phrase_sent = True + NVDAState.pre_phrase_sent = True def enable_interrupt(_): - global pre_phrase_sent SLEEP_MODE = "sleep" in scope.get("mode") if ( not actions.user.is_nvda_running() # If we are in sleep mode, we still send the interrupt # assuming the pre_phrase was sent, given the fact # we still want `talon sleep` to restore the setting at the end - or (SLEEP_MODE and not pre_phrase_sent) + or (SLEEP_MODE and not NVDAState.pre_phrase_sent) or not os.path.exists(SPEC_FILE) ): return - global reenable_commands # Can add more commands here if needed # We don't need to send disable commands since they are already disabled # at pre-phrase time - commands = reenable_commands - - if len(commands) == 0: + if len(NVDAState.reenable_commands) == 0: return - # this is kind of a hack since we don't know exactly when to re enable it - # because we don't have a callback at the end of the last keypress - cron.after("400ms", lambda: actions.user.send_ipc_commands(commands)) + # best way to do this because we don't have a callback at the end of the last keypress + cron.after("400ms", lambda: actions.user.send_ipc_commands(NVDAState.reenable_commands)) # Reset the pre_phrase_sent flag to prevent another post:phrase callback during sleep mode - pre_phrase_sent = False + + NVDAState.pre_phrase_sent = False if os.name == "nt": From 25b1c3658c2cc865ddfdf3008f355716dd632127 Mon Sep 17 00:00:00 2001 From: Colton <70598503+C-Loftus@users.noreply.github.com> Date: Sat, 6 Apr 2024 12:45:41 -0400 Subject: [PATCH 2/9] precommit trigger From 5d8157d3bf43709b9370bb02bea0bc39eaae6274 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 6 Apr 2024 16:45:55 +0000 Subject: [PATCH 3/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- .vscode/extensions.json | 12 +- .vscode/settings.json | 38 +++--- .vscode/typings/__builtins__.pyi | 8 +- .../chat-gpt-browser.talon | 24 ++-- .../cursorless-sightless.talon | 1 - application-integration/ms-edge-tts.talon | 1 - core/callbacks.py | 4 +- core/core-agnostic.py | 8 +- core/core-linux.py | 8 +- core/core-mac.py | 3 +- core/core-windows.py | 5 +- core/overrides.py | 2 +- core/screenreader_ipc/ipc_client.py | 33 +++-- core/screenreader_ipc/ipc_schema.py | 4 +- core/settings.py | 3 +- jaws/jaws.py | 3 +- lib/HTMLbuilder.py | 6 +- lib/sound/sound.py | 3 +- lib/sound/soundEffects.py | 5 +- .../_template_addon_release.json | 54 ++++---- .../addon/globalPlugins/nvda-addon.py | 15 ++- .../copy_to_scratchpad.ps1 | 2 +- nvda/.addOn/sight-free-talon-server/readme.md | 2 - .../site_tools/gettexttool/__init__.py | 58 ++++---- nvda/.addOn/sight-free-talon-server/style.css | 58 ++++---- nvda/debug-nvda.talon | 1 - nvda/nvda-parser.py | 5 +- nvda/nvda.py | 26 ++-- nvda/nvda.talon | 126 ++++++++++-------- nvda/overrides.talon | 1 - orca/orca.py | 3 +- orca/orca.talon | 1 - readme.md | 15 +-- sight-free-global.talon | 2 +- utils/access-focus.py | 2 +- utils/help.py | 113 ++++++++-------- utils/help.talon | 2 +- utils/log/log_checker.py | 63 ++++++--- utils/utils.py | 7 +- utils/windows-utils.talon | 1 - voiceover/readme.md | 1 - voiceover/voiceover.py | 6 +- voiceover/voiceover_tts.applescript | 6 +- 44 files changed, 405 insertions(+), 338 deletions(-) diff --git a/.gitignore b/.gitignore index 6ca5b0b..042e54c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ manifest.ini .ruff_cache/ .mypy_cache/ -__pycache__/ \ No newline at end of file +__pycache__/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 468c28c..25a3054 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,7 @@ { - "recommendations": [ - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff" - ] -} \ No newline at end of file + "recommendations": [ + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 197b3c9..53aa5f9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,21 @@ { - // Extra paths for NVDA addon development with the python environment outside Talon - "editor.accessibilitySupport": "on", - "python.autoComplete.extraPaths": [ - "~\\github\\nvda\\source", - "~\\github\\nvda\\miscDeps\\python" - ], - "python.analysis.stubPath": "${workspaceFolder}/.vscode/typings", - // Clone the NVDA repository to the github folder in your home directory to get typing support - "python.analysis.extraPaths": [ - "~\\github\\nvda\\source", - "~\\github\\nvda\\miscDeps\\python" - ], - "[python]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "always" - }, - "editor.defaultFormatter": "charliermarsh.ruff" + // Extra paths for NVDA addon development with the python environment outside Talon + "editor.accessibilitySupport": "on", + "python.autoComplete.extraPaths": [ + "~\\github\\nvda\\source", + "~\\github\\nvda\\miscDeps\\python" + ], + "python.analysis.stubPath": "${workspaceFolder}/.vscode/typings", + // Clone the NVDA repository to the github folder in your home directory to get typing support + "python.analysis.extraPaths": [ + "~\\github\\nvda\\source", + "~\\github\\nvda\\miscDeps\\python" + ], + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "always" }, -} \ No newline at end of file + "editor.defaultFormatter": "charliermarsh.ruff" + } +} diff --git a/.vscode/typings/__builtins__.pyi b/.vscode/typings/__builtins__.pyi index 88febec..48cbde4 100644 --- a/.vscode/typings/__builtins__.pyi +++ b/.vscode/typings/__builtins__.pyi @@ -1,6 +1,2 @@ -def _(msg: str) -> str: - ... - - -def pgettext(context: str, message: str) -> str: - ... +def _(msg: str) -> str: ... +def pgettext(context: str, message: str) -> str: ... diff --git a/application-integration/chat-gpt-browser.talon b/application-integration/chat-gpt-browser.talon index 21aedfb..18e0762 100644 --- a/application-integration/chat-gpt-browser.talon +++ b/application-integration/chat-gpt-browser.talon @@ -1,12 +1,15 @@ title: /https://chat.openai.com/ - - -Open new chat: key(ctrl-shift-o) -Focus chat input: key(shift-esc) -Copy last code block: key(ctrl-shift-;) -Copy last response: key(ctrl-shift-c) +Open new chat: + key(ctrl-shift-o) +Focus chat input: + key(shift-esc) +Copy last code block: + key(ctrl-shift-;) +Copy last response: + key(ctrl-shift-c) -speak [last] ( response | output | chat ): +speak [last] (response | output | chat): key(ctrl-shift-c) sleep(0.3) user.tts(clip.text()) @@ -16,6 +19,9 @@ speak last code [block]: sleep(0.3) user.tts(clip.text()) -Set custom instructions: key(ctrl-shift-i) -Toggle sidebar: key(ctrl-shift-s) -Delete chat: key(ctrl-shift-delete) +Set custom instructions: + key(ctrl-shift-i) +Toggle sidebar: + key(ctrl-shift-s) +Delete chat: + key(ctrl-shift-delete) diff --git a/application-integration/cursorless-sightless.talon b/application-integration/cursorless-sightless.talon index 6580111..0b9b93c 100644 --- a/application-integration/cursorless-sightless.talon +++ b/application-integration/cursorless-sightless.talon @@ -1,5 +1,4 @@ tag: user.cursorless - - speak : txt = user.cursorless_get_text(cursorless_target) diff --git a/application-integration/ms-edge-tts.talon b/application-integration/ms-edge-tts.talon index 88b50ad..d88f99a 100644 --- a/application-integration/ms-edge-tts.talon +++ b/application-integration/ms-edge-tts.talon @@ -2,7 +2,6 @@ os: windows and app.name: Microsoft Edge os: windows and app.exe: msedge.exe - - # This file takes advantage of the functionality for more natural text to speech within Microsoft edge # as of 2023, this functionality is not present on Linux diff --git a/core/callbacks.py b/core/callbacks.py index 2647e64..c412929 100644 --- a/core/callbacks.py +++ b/core/callbacks.py @@ -1,6 +1,7 @@ -from talon import scope, registry, ui, actions, settings, speech_system, app from typing import ClassVar, Optional +from talon import actions, app, registry, scope, settings, speech_system, ui + class CallbackState: last_mode: ClassVar[Optional[str]] = None @@ -54,6 +55,7 @@ def on_title_switch(win): CallbackState.last_title = active_window_title actions.user.tts(f"{active_window_title}") + def on_update_contexts(): last_mode = CallbackState.last_mode diff --git a/core/core-agnostic.py b/core/core-agnostic.py index 955c526..2d12dc7 100644 --- a/core/core-agnostic.py +++ b/core/core-agnostic.py @@ -3,13 +3,15 @@ and are agnostic to the tts voice being used or the operating system """ -from typing import Optional, Callable, ClassVar -from talon import Module, actions, Context, settings, app +from typing import Callable, ClassVar, Optional + +from talon import Context, Module, actions, app, settings mod = Module() ctx = Context() -class AgnosticState(): + +class AgnosticState: speaker_cancel_callback: ClassVar[Optional[Callable]] = None diff --git a/core/core-linux.py b/core/core-linux.py index 737ed5b..49b206c 100644 --- a/core/core-linux.py +++ b/core/core-linux.py @@ -1,14 +1,16 @@ -from talon import Context, actions, settings import os import subprocess -from typing import Literal, ClassVar +from typing import ClassVar, Literal + +from talon import Context, actions, settings ctxLinux = Context() ctxLinux.matches = r""" os: linux """ -class LinuxState(): + +class LinuxState: speaker: ClassVar[Literal["espeak", "piper"]] = "espeak" diff --git a/core/core-mac.py b/core/core-mac.py index 70f5b13..d8ae531 100644 --- a/core/core-mac.py +++ b/core/core-mac.py @@ -1,6 +1,7 @@ -from talon import Context, actions, settings import subprocess +from talon import Context, actions, settings + ctxMac = Context() ctxMac.matches = r""" os: mac diff --git a/core/core-windows.py b/core/core-windows.py index a7db87b..692659e 100644 --- a/core/core-windows.py +++ b/core/core-windows.py @@ -1,6 +1,7 @@ -from talon import Context, settings, actions -from collections import OrderedDict import os +from collections import OrderedDict + +from talon import Context, actions, settings if os.name == "nt": import pywintypes diff --git a/core/overrides.py b/core/overrides.py index 9025e4b..04e832f 100644 --- a/core/overrides.py +++ b/core/overrides.py @@ -1,4 +1,4 @@ -from talon import actions, Context, settings, Module, app +from talon import Context, Module, actions, app, settings ctx = Context() diff --git a/core/screenreader_ipc/ipc_client.py b/core/screenreader_ipc/ipc_client.py index c41ecdb..c975830 100644 --- a/core/screenreader_ipc/ipc_client.py +++ b/core/screenreader_ipc/ipc_client.py @@ -1,20 +1,21 @@ -from talon import Module, Context, actions, settings, cron -import os import ipaddress import json +import os import socket import threading -from typing import Tuple, assert_never, Optional +from typing import Optional, Tuple, assert_never + +from talon import Context, Module, actions, cron, settings + from .ipc_schema import ( IPC_COMMAND, - IPCServerResponse, IPCClientResponse, + IPCServerResponse, + ResponseBundle, ServerSpec, ServerStatusResult, - ResponseBundle, ) - mod = Module() lock = threading.Lock() @@ -33,13 +34,11 @@ def handle_ipc_result( match client_response, server_response: case ( - ( - IPCClientResponse.NO_RESPONSE - | IPCClientResponse.TIMED_OUT - | IPCClientResponse.GENERAL_ERROR, - _, - ) as error - ): + IPCClientResponse.NO_RESPONSE + | IPCClientResponse.TIMED_OUT + | IPCClientResponse.GENERAL_ERROR, + _, + ) as error: raise RuntimeError( f"Clientside {error=} communicating with screenreader extension" ) @@ -64,11 +63,9 @@ def handle_ipc_result( "Invalid JSON payload sent from client to screenreader" ) case ( - ( - ServerStatusResult.INTERNAL_SERVER_ERROR - | ServerStatusResult.RUNTIME_ERROR - ) as error - ): + ServerStatusResult.INTERNAL_SERVER_ERROR + | ServerStatusResult.RUNTIME_ERROR + ) as error: raise RuntimeError(f"{error} processing command '{cmd}'") case _: assert_never((cmd, value, status)) diff --git a/core/screenreader_ipc/ipc_schema.py b/core/screenreader_ipc/ipc_schema.py index 85d7b15..8e80df1 100644 --- a/core/screenreader_ipc/ipc_schema.py +++ b/core/screenreader_ipc/ipc_schema.py @@ -1,5 +1,5 @@ import enum -from typing import List, Literal, Any, TypedDict, Optional +from typing import Any, List, Literal, Optional, TypedDict # Exhaustive list of commands that can be sent to the NVDA addon server @@ -16,6 +16,7 @@ "debug", ] + class ServerSpec(TypedDict): address: str port: str @@ -36,6 +37,7 @@ def generate_from(value: str): return member raise KeyError(f"Invalid status result: {value}") + class IPCServerResponse(TypedDict): processedCommands: List[str] returnedValues: List[Any] diff --git a/core/settings.py b/core/settings.py index 833da6e..b2e55df 100644 --- a/core/settings.py +++ b/core/settings.py @@ -1,6 +1,7 @@ -from talon import Module from typing import Literal +from talon import Module + mod = Module() mod.setting( diff --git a/jaws/jaws.py b/jaws/jaws.py index fe735e8..7667230 100644 --- a/jaws/jaws.py +++ b/jaws/jaws.py @@ -1,9 +1,10 @@ # from __future__ import absolute_import # import pywintypes -from talon import actions, Module, cron, Context import os +from talon import Context, Module, actions, cron + # class Jaws(): # """Supports the Jaws for Windows screen reader.""" diff --git a/lib/HTMLbuilder.py b/lib/HTMLbuilder.py index 773bf99..8edd877 100644 --- a/lib/HTMLbuilder.py +++ b/lib/HTMLbuilder.py @@ -1,15 +1,15 @@ # Talon's imgui gui library is not accessible to screen readers. # By using HTML we can create temporary web pages that are accessible to screen readers. -import tempfile -import webbrowser import enum import os import platform +import tempfile +import webbrowser STYLE = """