28 Commits

Author SHA1 Message Date
Storm Dragon f462ca7990 Merge branch 'testing' minor settings file update. 2025-12-03 16:25:47 -05:00
Storm Dragon f0bbcb8a38 Updated settings file to document that capslock as fenrir key and echo mode 2 are incompatible. 2025-12-03 16:25:22 -05:00
Storm Dragon aed627ec2a Discovered through much pain that echo mode 2 and capslock as fenrir key are incompatible. Documented in settings file. 2025-12-03 16:18:02 -05:00
Storm Dragon e62b887e9c Some socket improvements for remote manager I thought should make it into this release. 2025-12-03 12:20:14 -05:00
Storm Dragon bf0d134187 Tests added, see the documentation in the tests directory for details. Improved the socket code. 2025-12-03 02:51:49 -05:00
Storm Dragon c66a9ba9c2 Problems with voice selection fixed.: 2025-12-02 18:38:06 -05:00
Storm Dragon 2092a3e257 Fixed voice selection. 2025-12-02 18:36:46 -05:00
Storm Dragon d46d8de3ee Updated sound driver to gstreamer by default. 2025-12-02 16:25:25 -05:00
Storm Dragon 75a8447759 One more feature addition before hopefully releasing the new version. 2025-12-02 16:13:15 -05:00
Storm Dragon 1650eec768 Add ability to switch speech-dispatcher module and voice to the speeach keys. 2025-12-02 16:11:47 -05:00
Storm Dragon 5bb786ef4c Bug fix in vmenu for keyboard layouts. 2025-11-27 22:44:51 -05:00
Storm Dragon 7f7faa17d3 keyboard layout fixed in vmenu. 2025-11-27 22:42:36 -05:00
Storm Dragon 2766f70c5d Some cleanup and minor fixes in docs. 2025-11-24 09:07:45 -05:00
Storm Dragon 8d781643bc Remove group and user comments from the Arch service file, not being used, so just extra stuff. 2025-11-24 08:49:31 -05:00
Storm Dragon c184cf023a Code cleanups, fixes to systemd files, url corrections. 2025-11-24 08:44:49 -05:00
Storm Dragon 841c221c7b Preparing for new tagged release. 2025-11-23 18:51:02 -05:00
Storm Dragon 87553bdc38 more fixes for the pickle error. 2025-11-23 18:37:21 -05:00
Storm Dragon 77a3aae5a4 Attempt to fix cannot pickle 'TextIOWrapper' instances error for some distros. 2025-11-23 18:29:38 -05:00
Storm Dragon aabc202d83 Latest version of configure_pipewire.sh tested and appears to work. 2025-10-17 22:19:58 -04:00
Storm Dragon 2f3a114790 Pipewire configuration tool updated. In a bout of pure insanity I tested this on my production system and it worked without a hitch, so should be good to go. 2025-10-17 22:19:30 -04:00
Storm Dragon c797974560 Pre tag release. If no problems reported this will become the new stable release. 2025-10-17 21:27:36 -04:00
Storm Dragon af4740d5ad Various minor fixes in preparation for new release. 2025-10-17 21:26:13 -04:00
Storm Dragon 5ef5faaebe Progressbar tweaks. 2025-10-17 21:03:12 -04:00
Storm Dragon 7041d2567a Progress bar updates. 2025-09-26 18:07:35 -04:00
Storm Dragon 2c38bcf5f4 Another attempt at fixing external numpad detection. I *think* some of them send numlock state with every event. If that's the case, this should fix it. 2025-09-13 14:11:27 -04:00
Storm Dragon 96cdda99c4 Separate menu entries for some settings, improves usability. Fixed device detection for devices that do not contain what they do in their name, e.g. numpads that are not listed as numpads. This hopefully fixes a bug with some external numpads. 2025-09-12 12:09:00 -04:00
Storm Dragon 0658d37ae8 I don't wanna say this too loud, but I think tab completion is much more reliable now. No more not reading when you press tab and something appears. 2025-08-31 20:45:32 -04:00
Storm Dragon a6bb3e1301 A bit of code cleanup. Nothing should be changed at all functionally. 2025-08-31 20:30:06 -04:00
162 changed files with 3627 additions and 955 deletions
-3
View File
@@ -1,3 +0,0 @@
V2.0
Cleanup folders and config files.
+1 -3
View File
@@ -4,12 +4,10 @@ Wants=systemd-udev-settle.service
After=systemd-udev-settle.service getty.target After=systemd-udev-settle.service getty.target
[Service] [Service]
Type=forking Type=forking
PIDFile=/var/run/fenrir.pid PIDFile=/run/fenrir.pid
ExecStart=/usr/bin/fenrir ExecStart=/usr/bin/fenrir
ExecReload=/usr/bin/kill -HUP $MAINPID ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always Restart=always
#Group=fenrirscreenreader
#User=fenrirscreenreader
[Install] [Install]
WantedBy=getty.target WantedBy=getty.target
+1 -1
View File
@@ -4,7 +4,7 @@ Wants=systemd-udev-settle.service
After=systemd-udev-settle.service sound.target After=systemd-udev-settle.service sound.target
[Service] [Service]
Type=forking Type=forking
PIDFile=/var/run/fenrir.pid PIDFile=/run/fenrir.pid
ExecStart=/usr/local/bin/fenrir ExecStart=/usr/local/bin/fenrir
ExecReload=/usr/bin/kill -HUP $MAINPID ExecReload=/usr/bin/kill -HUP $MAINPID
Restart=always Restart=always
+2 -2
View File
@@ -1,5 +1,5 @@
Please report Bugs and feature requests to: Please report bugs and feature requests to:
https://github.com/chrys87/fenrir/issues https://git.stormux.org/storm/fenrir/issues
For bugs, please provide a debug file that shows the issue. For bugs, please provide a debug file that shows the issue.
How to create a debug file: How to create a debug file:
+11 -4
View File
@@ -3,9 +3,9 @@
enabled=True enabled=True
# Select the driver used to play sounds, choices are genericDriver and gstreamerDriver. # Select the driver used to play sounds, choices are genericDriver and gstreamerDriver.
# Sox is the default. # Gstreamer is the default.
#driver=gstreamerDriver driver=gstreamerDriver
driver=genericDriver #driver=genericDriver
# Sound themes. These are the pack of sounds used for sound alerts. # Sound themes. These are the pack of sounds used for sound alerts.
# Sound packs may be located at /usr/share/sounds # Sound packs may be located at /usr/share/sounds
@@ -110,7 +110,7 @@ keyboardLayout=desktop
# echo chars while typing. # echo chars while typing.
# 0 = None # 0 = None
# 1 = always # 1 = always
# 2 = only while capslock # 2 = only while capslock (not compatible with capslock as fenrir key)
charEchoMode=1 charEchoMode=1
# echo deleted chars # echo deleted chars
charDeleteEcho=True charDeleteEcho=True
@@ -217,6 +217,13 @@ list=
[menu] [menu]
vmenuPath= vmenuPath=
# quickMenu: Semicolon-separated list of settings for quick adjustment
# Format: section#setting;section#setting;...
# Supported settings:
# - speech#rate, speech#pitch, speech#volume (0.0-1.0)
# - speech#module, speech#voice (speechdDriver only, auto-added)
# Note: speech#module and speech#voice are automatically added when
# speechdDriver is active. Do not add them manually.
quickMenu=speech#rate;speech#pitch;speech#volume quickMenu=speech#rate;speech#pitch;speech#volume
[prompt] [prompt]
+6 -6
View File
@@ -7,14 +7,14 @@ configurable and easy to customize and extend.
=== Credit and intended audience === Credit and intended audience
This document is just a customization for Slint of the genuine This document is just a customization for Slint of the genuine
https://github.com/chrys87/fenrir/blob/master/docu/user.txt[Fenrir User https://git.stormux.org/storm/fenrir/src/branch/master/docs/user.txt[Fenrir User
Manual] motly written by Chrys, main developer of Fenrir. Manual] motly written by Chrys, main developer of Fenrir.
It has been adapted to its intended audience: end users of Fenrir on It has been adapted to its intended audience: end users of Fenrir on
Slint where it is already installed, thus concentrates on its setting Slint where it is already installed, thus concentrates on its setting
and usage. You will find more information about its features, and usage. You will find more information about its features,
installation and how customize and troubleshoot it and contribute to its installation and how customize and troubleshoot it and contribute to its
development on https://github.com/chrys87/fenrir[the Fenrir Git development on https://git.stormux.org/storm/fenrir[the Fenrir Git
repository]. repository].
=== Getting started with Fenrir === Getting started with Fenrir
@@ -2193,9 +2193,9 @@ settings.conf). Commands are python files with a special scheme. You can
assign them to a shortcut using the filename without an extension or assign them to a shortcut using the filename without an extension or
place them in a hook trigger like OnInput or OnScreenChange. For further place them in a hook trigger like OnInput or OnScreenChange. For further
information see developer guide. Good Examples: information see developer guide. Good Examples:
https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/date.py["date.py"] https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/date.py["date.py"]
(announce the Date), (announce the Date),
https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/shut_up.py["shut_up.py"] https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/shut_up.py["shut_up.py"]
(interrupt output) the basic scheme for a command is as follows: (interrupt output) the basic scheme for a command is as follows:
.... ....
@@ -2218,7 +2218,7 @@ class command():
pass pass
.... ....
* https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/command_template.py[Template * https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/command_template.py[Template
lives here] lives here]
* The class needs to have the name "command". * The class needs to have the name "command".
* "initialize" is running once whilst loading the command. * "initialize" is running once whilst loading the command.
@@ -2276,7 +2276,7 @@ root.
=== Bugreports and feature requests === Bugreports and feature requests
Please report Bugs and feature requests to: Please report Bugs and feature requests to:
https://github.com/chrys87/fenrir/issues https://git.stormux.org/storm/fenrir/issues
for bugs please provide a link:#Howto create a debug file[debug] file for bugs please provide a link:#Howto create a debug file[debug] file
that shows the issue. that shows the issue.
+6 -6
View File
@@ -160,7 +160,7 @@ For Arch there are PKGBUILDs in the AUR:
- Download the latest stable version from the [[https://linux-a11y.org/index.php?page=fenrir-screenreader|Fenrir-Project]] site. - Download the latest stable version from the [[https://linux-a11y.org/index.php?page=fenrir-screenreader|Fenrir-Project]] site.
- Unpack the archive - Unpack the archive
- Check the needed Dependencys by running [[https://github.com/chrys87/fenrir/blob/master/check-dependencies.py|check-dependencys.py]] script - Check the needed Dependencys by running [[https://git.stormux.org/storm/fenrir/src/branch/master/check-dependencies.py|check-dependencys.py]] script
- install the missing dependencies an standard installation requires the following: - install the missing dependencies an standard installation requires the following:
* python3 >= 3.3 (and all the following is needed for python3 ) * python3 >= 3.3 (and all the following is needed for python3 )
* python3-speechd (screen) * python3-speechd (screen)
@@ -171,7 +171,7 @@ For Arch there are PKGBUILDs in the AUR:
* python3-pyenchant (spellchecker) * python3-pyenchant (spellchecker)
* your language for aspell (aspell-<lang>) (spellchecker) * your language for aspell (aspell-<lang>) (spellchecker)
* sox (sound) * sox (sound)
* For an individual installation see [[#Support and Requirements|Support and Requirements]] or consult the [[https://github.com/chrys87/fenrir/blob/master/README.md|Readme]]) * For an individual installation see [[#Support and Requirements|Support and Requirements]] or consult the [[https://git.stormux.org/storm/fenrir/src/branch/master/README.md|Readme]])
- run "install.sh" as root - run "install.sh" as root
this installs Fenrir as the following this installs Fenrir as the following
@@ -185,7 +185,7 @@ to remove Fenrir just run uninstall.sh as root
if you want to get the latest code you can use git to get a development snapshot: if you want to get the latest code you can use git to get a development snapshot:
git clone https://github.com/chrys87/fenrir.git git clone https://git.stormux.org/storm/fenrir.git
===== Auto Start ===== ===== Auto Start =====
@@ -1270,7 +1270,7 @@ File: ''/usr/share/fenrirscreenreader/scripts/helloWorld__-__key_h.sh'':
===== Commands ===== ===== Commands =====
You can place your own commands in "/usr/share/fenrirscreenreader/commands" (path is configurable in settings.conf). You can place your own commands in "/usr/share/fenrirscreenreader/commands" (path is configurable in settings.conf).
Commands are python files with a special scheme. You can assign them to a shortcut using the filename without an extension or place them in a hook trigger like OnInput or OnScreenChange. For further information see developer guide. Commands are python files with a special scheme. You can assign them to a shortcut using the filename without an extension or place them in a hook trigger like OnInput or OnScreenChange. For further information see developer guide.
Good Examples: [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/date.py|"date.py"]] (announce the Date), [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/commands/shut_up.py|"shut_up.py"]] (interrupt output) Good Examples: [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/date.py|"date.py"]] (announce the Date), [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/commands/shut_up.py|"shut_up.py"]] (interrupt output)
the basic scheme for a command is as follows: the basic scheme for a command is as follows:
from core import debug from core import debug
@@ -1289,7 +1289,7 @@ the basic scheme for a command is as follows:
def setCallback(self, callback): def setCallback(self, callback):
pass pass
* [[https://github.com/chrys87/fenrir/blob/master/src/fenrir/commands/command_template.py|Template lives here]] * [[https://git.stormux.org/storm/fenrir/src/branch/master/src/fenrirscreenreader/commands/command_template.py|Template lives here]]
* The class needs to have the name "command". * The class needs to have the name "command".
* "initialize" is running once whilst loading the command. * "initialize" is running once whilst loading the command.
* "shutdown" is running on unload like the command (quit fenrir) * "shutdown" is running on unload like the command (quit fenrir)
@@ -1319,7 +1319,7 @@ the basic scheme for a command is as follows:
- You can test if speech-dispatcher works by invoking it as root\\ ''sudo spd-say "hello world"'' - You can test if speech-dispatcher works by invoking it as root\\ ''sudo spd-say "hello world"''
===== Bugreports and feature requests ===== ===== Bugreports and feature requests =====
Please report Bugs and feature requests to: Please report Bugs and feature requests to:
[[https://github.com/chrys87/fenrir/issues|https://github.com/chrys87/fenrir/issues]] [[https://git.stormux.org/storm/fenrir/issues|https://git.stormux.org/storm/fenrir/issues]]
for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue. for bugs please provide a [[#Howto create a debug file|debug]] file that shows the issue.
==== How-to create a debug file ==== ==== How-to create a debug file ====
+1 -1
View File
@@ -56,7 +56,7 @@ To test Fenrir:
sudo fenrir sudo fenrir
To have Fenrir start on system boot using systemd: To have Fenrir start on system boot using systemd:
download service file: https://raw.githubusercontent.com/chrys87/fenrir/master/autostart/systemd/Arch/fenrir.service download service file: https://git.stormux.org/storm/fenrir/raw/branch/master/autostart/systemd/Arch/fenrir.service
move the service file to: /etc/systemd/system/fenrir.service move the service file to: /etc/systemd/system/fenrir.service
sudo systemctl enable fenrir sudo systemctl enable fenrir
+69
View File
@@ -0,0 +1,69 @@
[pytest]
# Pytest configuration for Fenrir screen reader
# Test discovery patterns
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
# Test paths
testpaths = tests
# Minimum Python version
minversion = 3.7
# Output options
addopts =
# Verbose output with test names
-v
# Show extra test summary info
-ra
# Enable strict markers (only registered markers allowed)
--strict-markers
# Show local variables in tracebacks
--showlocals
# Warnings configuration
-W ignore::DeprecationWarning
# Optional plugins (uncomment if installed):
# --timeout=30 # Requires pytest-timeout
# --cov-report=term-missing # Requires pytest-cov
# -x # Stop on first failure
# Register custom markers
markers =
unit: Unit tests (fast, no mocking)
integration: Integration tests (require mocking)
driver: Driver tests (require root access)
slow: Tests that take more than 1 second
remote: Tests for remote control functionality
settings: Tests for settings and configuration
commands: Tests for command system
vmenu: Tests for VMenu system
# Coverage configuration
[coverage:run]
source = src/fenrirscreenreader
omit =
*/tests/*
*/vmenu-profiles/*
*/__pycache__/*
*/site-packages/*
[coverage:report]
# Fail if coverage falls below this percentage
# fail_under = 70
exclude_lines =
# Standard pragma
pragma: no cover
# Don't complain about missing debug code
def __repr__
# Don't complain if tests don't hit defensive assertion code
raise AssertionError
raise NotImplementedError
# Don't complain about abstract methods
@abstractmethod
# Don't complain about initialization
if __name__ == .__main__.:
[coverage:html]
directory = htmlcov
@@ -67,4 +67,4 @@ class command:
def set_callback(self, callback): def set_callback(self, callback):
pass pass
@@ -62,4 +62,4 @@ class command:
def set_callback(self, callback): def set_callback(self, callback):
pass pass
@@ -4,7 +4,7 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core.i18n import _ from fenrirscreenreader.core.i18n import _
@@ -14,55 +14,150 @@ class command:
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
# Initialize tab completion state tracking
if "tabCompletion" not in self.env["commandBuffer"]:
self.env["commandBuffer"]["tabCompletion"] = {
"lastTabTime": 0,
"pendingCompletion": None,
"retryCount": 0
}
def shutdown(self): def shutdown(self):
pass pass
def get_description(self): def get_description(self):
return "No Description found" return _("Announces tab completions when detected")
def _is_recent_tab_input(self):
"""Check if TAB was pressed recently (within 200ms window)"""
current_time = time.time()
tab_detected = False
# Check KEY mode
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
if (self.env["runtime"]["InputManager"].get_last_deepest_input()
in [["KEY_TAB"]]):
tab_detected = True
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
# Check BYTE mode
elif self.env["runtime"]["InputManager"].get_shortcut_type() in ["BYTE"]:
for currByte in self.env["runtime"]["ByteManager"].get_last_byte_key():
if currByte == 9: # Tab character
tab_detected = True
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
# Check if tab was pressed recently (200ms window)
if not tab_detected:
time_since_tab = current_time - self.env["commandBuffer"]["tabCompletion"]["lastTabTime"]
if time_since_tab <= 0.2: # 200ms window
tab_detected = True
return tab_detected
def _is_flexible_completion_match(self, x_move, delta_text):
"""Use flexible matching instead of strict equality"""
if not delta_text:
return False
delta_len = len(delta_text)
# Exact match (preserve original behavior)
if x_move == delta_len:
return True
# Flexible range: allow ±2 characters difference
# Handles spacing adjustments and unicode width variations
if abs(x_move - delta_len) <= 2 and delta_len > 0:
return True
# For longer completions, allow proportional variance
if delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2):
return True
return False
def _detect_completion_patterns(self, delta_text):
"""Detect common tab completion patterns for improved accuracy"""
if not delta_text:
return False
delta_stripped = delta_text.strip()
# File extension completion
if '.' in delta_stripped and delta_stripped.count('.') <= 2:
return True
# Path completion (contains / or \)
if '/' in delta_stripped or '\\' in delta_stripped:
return True
# Command parameter completion (starts with -)
if delta_stripped.startswith('-') and len(delta_stripped) > 1:
return True
# Word boundary completion (alphanumeric content)
if delta_stripped.isalnum() and len(delta_stripped) >= 2:
return True
return False
def run(self): def run(self):
# try to detect the tab completion by cursor change """Enhanced tab completion detection with improved reliability"""
# Basic cursor movement check (preserve original logic)
x_move = ( x_move = (
self.env["screen"]["new_cursor"]["x"] self.env["screen"]["new_cursor"]["x"]
- self.env["screen"]["old_cursor"]["x"] - self.env["screen"]["old_cursor"]["x"]
) )
if x_move <= 0: if x_move <= 0:
return return
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
if not ( # Enhanced tab input detection with persistence
self.env["runtime"]["InputManager"].get_last_deepest_input() tab_detected = self._is_recent_tab_input()
in [["KEY_TAB"]]
): # Fallback for non-tab movements (preserve original thresholds)
if x_move < 5: if not tab_detected:
return if x_move < 5:
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [ return
"BYTE"
]: # Screen delta availability check
found = False
for currByte in self.env["runtime"][
"ByteManager"
].get_last_byte_key():
if currByte == 9:
found = True
if not found:
if x_move < 5:
return
# is there any change?
if not self.env["runtime"]["ScreenManager"].is_delta(): if not self.env["runtime"]["ScreenManager"].is_delta():
# If tab was detected but no delta yet, store for potential retry
if tab_detected and self.env["commandBuffer"]["tabCompletion"]["retryCount"] < 2:
self.env["commandBuffer"]["tabCompletion"]["pendingCompletion"] = {
"x_move": x_move,
"timestamp": time.time()
}
self.env["commandBuffer"]["tabCompletion"]["retryCount"] += 1
return return
if not x_move == len(self.env["screen"]["new_delta"]):
return delta_text = self.env["screen"]["new_delta"]
# filter unneded space on word begin
curr_delta = self.env["screen"]["new_delta"] # Enhanced correlation checking with flexible matching
if ( if not self._is_flexible_completion_match(x_move, delta_text):
len(curr_delta.strip()) != len(curr_delta) # Additional pattern-based validation for edge cases
and curr_delta.strip() != "" if not (tab_detected and self._detect_completion_patterns(delta_text)):
): return
# Reset retry counter on successful detection
self.env["commandBuffer"]["tabCompletion"]["retryCount"] = 0
self.env["commandBuffer"]["tabCompletion"]["pendingCompletion"] = None
# Mark that we've handled this delta to prevent duplicate announcements
# This prevents the incoming text handler from also announcing the same content
self.env["commandBuffer"]["tabCompletion"]["lastProcessedDelta"] = delta_text
self.env["commandBuffer"]["tabCompletion"]["lastProcessedTime"] = time.time()
# Text filtering and announcement (preserve original behavior)
curr_delta = delta_text
if (len(curr_delta.strip()) != len(curr_delta) and curr_delta.strip() != ""):
curr_delta = curr_delta.strip() curr_delta = curr_delta.strip()
self.env["runtime"]["OutputManager"].present_text(
curr_delta, interrupt=True, announce_capital=True, flush=False # Enhanced announcement with better handling of empty completions
) if curr_delta:
self.env["runtime"]["OutputManager"].present_text(
curr_delta, interrupt=True, announce_capital=True, flush=False
)
def set_callback(self, callback): def set_callback(self, callback):
pass pass
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core.i18n import _
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _("Handles delayed retry for tab completion detection")
def run(self):
"""Check for and process pending tab completions with slight delay"""
# Only process if we have tab completion state
if "tabCompletion" not in self.env["commandBuffer"]:
return
tab_state = self.env["commandBuffer"]["tabCompletion"]
pending = tab_state.get("pendingCompletion")
if not pending:
return
current_time = time.time()
# Process pending completion after 50ms delay
if current_time - pending["timestamp"] < 0.05:
return
# Check if screen delta is now available
if not self.env["runtime"]["ScreenManager"].is_delta():
# Give up after 200ms total
if current_time - pending["timestamp"] > 0.2:
tab_state["pendingCompletion"] = None
tab_state["retryCount"] = 0
return
# Process the delayed completion
delta_text = self.env["screen"]["new_delta"]
x_move = pending["x_move"]
# Use the same flexible matching logic as main tab completion
match_found = self._is_flexible_completion_match(x_move, delta_text)
if not match_found:
# Try pattern-based detection as final fallback
match_found = self._detect_completion_patterns(delta_text)
if match_found and delta_text:
# Mark that we've handled this delta to prevent duplicate announcements
tab_state["lastProcessedDelta"] = delta_text
tab_state["lastProcessedTime"] = current_time
# Filter and announce the completion
curr_delta = delta_text
if (len(curr_delta.strip()) != len(curr_delta) and
curr_delta.strip() != ""):
curr_delta = curr_delta.strip()
if curr_delta:
self.env["runtime"]["OutputManager"].present_text(
curr_delta, interrupt=True, announce_capital=True, flush=False
)
# Clear pending completion
tab_state["pendingCompletion"] = None
tab_state["retryCount"] = 0
def _is_flexible_completion_match(self, x_move, delta_text):
"""Use flexible matching (duplicated from main command for heartbeat use)"""
if not delta_text:
return False
delta_len = len(delta_text)
# Exact match
if x_move == delta_len:
return True
# Flexible range: allow ±2 characters difference
if abs(x_move - delta_len) <= 2 and delta_len > 0:
return True
# For longer completions, allow proportional variance
if delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2):
return True
return False
def _detect_completion_patterns(self, delta_text):
"""Detect common tab completion patterns (duplicated from main command)"""
if not delta_text:
return False
delta_stripped = delta_text.strip()
# File extension completion
if '.' in delta_stripped and delta_stripped.count('.') <= 2:
return True
# Path completion
if '/' in delta_stripped or '\\' in delta_stripped:
return True
# Command parameter completion
if delta_stripped.startswith('-') and len(delta_stripped) > 1:
return True
# Word boundary completion
if delta_stripped.isalnum() and len(delta_stripped) >= 2:
return True
return False
def set_callback(self, callback):
pass
@@ -24,11 +24,22 @@ class command:
def run(self): def run(self):
if self.env["input"]["oldNumLock"] == self.env["input"]["newNumLock"]: if self.env["input"]["oldNumLock"] == self.env["input"]["newNumLock"]:
return return
# Only announce numlock changes if an actual numlock key was pressed # Only announce numlock changes if an actual numlock key was pressed
# This prevents spurious announcements from external numpad automatic state changes # AND the LED state actually changed (some numpads send spurious NUMLOCK events)
current_input = self.env["input"]["currInput"] current_input = self.env["input"]["currInput"]
if current_input and "KEY_NUMLOCK" in current_input:
# Check if this is a genuine numlock key press by verifying:
# 1. KEY_NUMLOCK is in the current input sequence
# 2. The LED state has actually changed
# 3. This isn't just a side effect from a KP_ key (which some buggy numpads do)
is_genuine_numlock = (
current_input and
"KEY_NUMLOCK" in current_input and
not any(key.startswith("KEY_KP") for key in current_input if isinstance(key, str))
)
if is_genuine_numlock:
if self.env["input"]["newNumLock"]: if self.env["input"]["newNumLock"]:
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("Numlock on"), interrupt=True _("Numlock on"), interrupt=True
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.core import debug
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"TUI focus mode handler - suppresses screen update spam "
"for interactive TUI applications"
)
def run(self):
# Check if TUI mode is enabled
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"focus", "tui"
):
return
# TUI mode is active - set suppression flag for incoming handler
# This prevents the 70000-incoming.py command from announcing
# screen updates
self.env["commandBuffer"]["tuiSuppressIncoming"] = True
self.env["runtime"]["DebugManager"].write_debug_out(
"tui_focus_handler: TUI mode active, suppressing incoming text",
debug.DebugLevel.INFO
)
def set_callback(self, callback):
pass
@@ -66,12 +66,18 @@ class command:
# Check if delta is too large (screen change) vs small incremental # Check if delta is too large (screen change) vs small incremental
# updates # updates
delta_length = len(self.env["screen"]["new_delta"]) delta_text = self.env["screen"]["new_delta"]
delta_length = len(delta_text)
if ( if (
delta_length > 200 delta_length > 200
): # Allow longer progress lines like Claude Code's status ): # Allow longer progress lines like Claude Code's status
return False return False
# If delta contains newlines and is substantial, let incoming handler
# deal with it to avoid interfering with multi-line text output
if '\n' in delta_text and delta_length > 50:
return False
# Check if current line looks like a prompt - progress unlikely during # Check if current line looks like a prompt - progress unlikely during
# prompts # prompts
if self.is_current_line_prompt(): if self.is_current_line_prompt():
@@ -270,7 +276,7 @@ class command:
self.env["commandBuffer"]["lastProgressTime"] = current_time self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
# Pattern 5: Braille progress indicators # Pattern 5: Braille spinner indicators
braille_match = re.search(r'[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⡿⣟⣯⣷⣾⣽⣻⢿]', text) braille_match = re.search(r'[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⡿⣟⣯⣷⣾⣽⣻⢿]', text)
if braille_match: if braille_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0: if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
@@ -279,26 +285,46 @@ class command:
return return
# Pattern 6: Claude Code progress indicators # Pattern 6: Claude Code progress indicators
claude_progress_match = re.search(r'^[·✶✢✻*]\s+[^(]+[…\.]*\s*\(esc to interrupt[^)]*\)\s*$', text) claude_progress_match = re.search(r'[·✶✢✻*].*?\(esc to interrupt[^)]*\)', text)
if claude_progress_match: if claude_progress_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0: if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.play_activity_beep() self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
# Pattern 7: Moon phase progress indicators # Pattern 7: Moon phase spinner indicators
moon_match = re.search(r'[🌑🌒🌓🌔🌕🌖🌗🌘]', text) moon_match = re.search(r'[🌑🌒🌓🌔🌕🌖🌗🌘]', text)
if moon_match: if moon_match:
moon_phases = { if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
'🌑': 0, '🌒': 12.5, '🌓': 25, '🌔': 37.5, self.play_activity_beep()
'🌕': 50, '🌖': 62.5, '🌗': 75, '🌘': 87.5 self.env["commandBuffer"]["lastProgressTime"] = current_time
} return
moon_char = moon_match.group(0)
if moon_char in moon_phases: # Pattern 8: Thinking/processing with timing (🔄 Thinking... 23s)
percentage = moon_phases[moon_char] thinking_match = re.search(r'🔄[^\w]*(?:thinking|processing|working|analyzing)[^\d]*(\d+)s?\b', text, re.IGNORECASE)
if percentage != self.env["commandBuffer"]["lastProgressValue"]: if thinking_match:
self.play_progress_tone(percentage) # Extract timing value for activity beep frequency adjustment
self.env["commandBuffer"]["lastProgressValue"] = percentage seconds = int(thinking_match.group(1))
# Use slightly longer interval for thinking patterns to avoid spam
thinking_interval = 1.5 if seconds < 10 else 2.0
if (
current_time - self.env["commandBuffer"]["lastProgressTime"]
>= thinking_interval
):
self.env["runtime"]["DebugManager"].write_debug_out(
f"Playing thinking activity beep (timing: {seconds}s)",
debug.DebugLevel.INFO,
)
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 9: Half-circle/circle progress indicators (◐ ◓ ◒ ◑)
circle_match = re.search(r'[◐◓◒◑]', text)
if circle_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return return
def play_progress_tone(self, percentage): def play_progress_tone(self, percentage):
@@ -4,7 +4,7 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
import time
from fenrirscreenreader.core.i18n import _ from fenrirscreenreader.core.i18n import _
@@ -19,7 +19,26 @@ class command:
pass pass
def get_description(self): def get_description(self):
return "No Description found" return _("Announces incoming text changes")
def _was_handled_by_tab_completion(self, delta_text):
"""Check if this delta was already handled by tab completion to avoid duplicates"""
if "tabCompletion" not in self.env["commandBuffer"]:
return False
tab_state = self.env["commandBuffer"]["tabCompletion"]
# Check if this exact delta was processed recently by tab completion
if (tab_state.get("lastProcessedDelta") == delta_text and
tab_state.get("lastProcessedTime")):
# Only suppress if processed within the last 50ms to avoid stale suppression
# Reduced from 100ms to minimize false positives with rapid multi-line updates
time_since_processed = time.time() - tab_state["lastProcessedTime"]
if time_since_processed <= 0.05:
return True
return False
def run(self): def run(self):
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool( if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
@@ -30,6 +49,12 @@ class command:
if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True): if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True):
return return
delta_text = self.env["screen"]["new_delta"]
# Skip if tab completion already handled this delta
if self._was_handled_by_tab_completion(delta_text):
return
# this must be a keyecho or something # this must be a keyecho or something
# if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1: # if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1:
x_move = abs( x_move = abs(
@@ -41,14 +66,14 @@ class command:
- self.env["screen"]["old_cursor"]["y"] - self.env["screen"]["old_cursor"]["y"]
) )
if (x_move >= 1) and x_move == len(self.env["screen"]["new_delta"]): if (x_move >= 1) and x_move == len(delta_text):
# if len(self.env['screen']['new_delta'].strip(' \n\t0123456789')) # if len(self.env['screen']['new_delta'].strip(' \n\t0123456789'))
# <= 2: # <= 2:
if "\n" not in self.env["screen"]["new_delta"]: if "\n" not in delta_text:
return return
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta'])) # print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
self.env["screen"]["new_delta"], interrupt=False, flush=False delta_text, interrupt=False, flush=False
) )
def set_callback(self, callback): def set_callback(self, callback):
@@ -1 +1 @@
# Emoji VMenu category # Emoji VMenu category
@@ -1 +1 @@
# Flags emoji subcategory # Flags emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Canada flag to clipboard", "Added Canada flag to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added UK flag to clipboard", "Added UK flag to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added USA flag to clipboard", "Added USA flag to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# Food emoji subcategory # Food emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added red apple to clipboard", "Added red apple to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added avocado to clipboard", "Added avocado to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added beer to clipboard", "Added beer to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added birthday cake to clipboard", "Added birthday cake to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added coffee to clipboard", "Added coffee to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added donut to clipboard", "Added donut to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added hamburger to clipboard", "Added hamburger to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added pizza to clipboard", "Added pizza to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added strawberry to clipboard", "Added strawberry to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added taco to clipboard", "Added taco to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# Holidays emoji subcategory # Holidays emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added alien monster to clipboard", "Added alien monster to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added bat to clipboard", "Added bat to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added black cat to clipboard", "Added black cat to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added bunny to clipboard", "Added bunny to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added mage to clipboard", "Added mage to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Christmas tree to clipboard", "Added Christmas tree to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added coffin to clipboard", "Added coffin to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Easter egg to clipboard", "Added Easter egg to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added fireworks to clipboard", "Added fireworks to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added ghost to clipboard", "Added ghost to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added gift to clipboard", "Added gift to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added jack o'lantern to clipboard", "Added jack o'lantern to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added mummy to clipboard", "Added mummy to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added Santa to clipboard", "Added Santa to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added shamrock to clipboard", "Added shamrock to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added skull to clipboard", "Added skull to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added snowman to clipboard", "Added snowman to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added spider to clipboard", "Added spider to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added turkey to clipboard", "Added turkey to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added vampire to clipboard", "Added vampire to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added spider web to clipboard", "Added spider web to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added witch to clipboard", "Added witch to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added zombie to clipboard", "Added zombie to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# Nature emoji subcategory # Nature emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added butterfly to clipboard", "Added butterfly to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added cat to clipboard", "Added cat to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added cherry blossom to clipboard", "Added cherry blossom to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added dog to clipboard", "Added dog to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added moon to clipboard", "Added moon to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added rainbow to clipboard", "Added rainbow to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added rose to clipboard", "Added rose to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added sun to clipboard", "Added sun to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added sunflower to clipboard", "Added sunflower to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added tree to clipboard", "Added tree to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added the mighty wolf Fenrir to clipboard", "Added the mighty wolf Fenrir to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# People emoji subcategory # People emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added grinning face to clipboard", "Added grinning face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added angry face to clipboard", "Added angry face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added beaming face to clipboard", "Added beaming face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added face blowing kiss to clipboard", "Added face blowing kiss to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added cool face to clipboard", "Added cool face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added crying face to clipboard", "Added crying face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added devil face to clipboard", "Added devil face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added dizzy face to clipboard", "Added dizzy face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added exploding head to clipboard", "Added exploding head to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added face with symbols to clipboard", "Added face with symbols to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added grinning face with sweat to clipboard", "Added grinning face with sweat to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added heart-eyes face to clipboard", "Added heart-eyes face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added smiling face with hearts to clipboard", "Added smiling face with hearts to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added angry face with horns to clipboard", "Added angry face with horns to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added laughing face to clipboard", "Added laughing face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added nauseated face to clipboard", "Added nauseated face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added pleading face to clipboard", "Added pleading face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added poop to clipboard", "Added poop to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added rolling on floor laughing to clipboard", "Added rolling on floor laughing to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added sad face to clipboard", "Added sad face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added face savoring food to clipboard", "Added face savoring food to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added screaming face to clipboard", "Added screaming face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added shocked face to clipboard", "Added shocked face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added smiling face to clipboard", "Added smiling face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added smirking face to clipboard", "Added smirking face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added thumbs up to clipboard", "Added thumbs up to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added winking face to clipboard", "Added winking face to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -1 +1 @@
# Symbols emoji subcategory # Symbols emoji subcategory
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added chains to clipboard", "Added chains to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )
@@ -19,4 +19,4 @@ class command():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
"Added checkmark to clipboard", "Added checkmark to clipboard",
interrupt=False, flush=False interrupt=False, flush=False
) )

Some files were not shown because too many files have changed in this diff Show More