Merged testing.

This commit is contained in:
Storm Dragon
2025-08-22 00:29:02 -04:00
25 changed files with 528 additions and 51 deletions

View File

@@ -60,6 +60,7 @@ class command:
if self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "autoPresentIndentMode"
) in [0, 1]:
if self.lastIdent != curr_ident:
self.env["runtime"]["OutputManager"].play_frequence(
curr_ident * 50, 0.1, interrupt=do_interrupt
)

View File

@@ -31,10 +31,9 @@ class command:
self.lastIdent = 0
return
# is a vertical change?
if not self.env["runtime"][
"CursorManager"
].is_cursor_horizontal_move():
# Skip if no cursor movement at all
if (not self.env["runtime"]["CursorManager"].is_cursor_horizontal_move() and
not self.env["runtime"]["CursorManager"].is_cursor_vertical_move()):
return
x, y, curr_line = line_utils.get_current_line(
self.env["screen"]["new_cursor"]["x"],
@@ -43,8 +42,11 @@ class command:
)
curr_ident = self.env["screen"]["new_cursor"]["x"]
if not curr_line.isspace():
# ident
if curr_line.isspace():
# Don't beep for lines with only spaces - no meaningful indentation
return
# Lines with actual content - calculate proper indentation
lastIdent, lastY, last_line = line_utils.get_current_line(
self.env["screen"]["new_cursor"]["x"],
self.env["screen"]["new_cursor"]["y"],
@@ -57,13 +59,17 @@ class command:
curr_ident = len(curr_line) - len(curr_line.lstrip())
if self.lastIdent == -1:
self.lastIdent = curr_ident
if curr_ident <= 0:
return
# Initialize lastIdent if needed
if self.lastIdent == -1:
self.lastIdent = curr_ident
# Only beep/announce if indentation level has changed
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "autoPresentIndent"
):
) and self.lastIdent != curr_ident:
if self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "autoPresentIndentMode"
) in [0, 1]:
@@ -71,14 +77,15 @@ class command:
curr_ident * 50, 0.1, interrupt=False
)
if self.env["runtime"]["SettingsManager"].get_setting_as_int(
"general", "autoPresentIndentMode"
"general", "autePresentIndentMode"
) in [0, 2]:
if self.lastIdent != curr_ident:
self.env["runtime"]["OutputManager"].present_text(
_("indented ") + str(curr_ident) + " ",
interrupt=False,
flush=False,
)
# Always update lastIdent for next comparison
self.lastIdent = curr_ident
def set_callback(self, callback):

View File

@@ -24,6 +24,11 @@ class command:
def run(self):
if self.env["input"]["oldNumLock"] == self.env["input"]["newNumLock"]:
return
# Only announce numlock changes if an actual numlock key was pressed
# This prevents spurious announcements from external numpad automatic state changes
current_input = self.env["input"]["currInput"]
if current_input and "KEY_NUMLOCK" in current_input:
if self.env["input"]["newNumLock"]:
self.env["runtime"]["OutputManager"].present_text(
_("Numlock on"), interrupt=True

View File

@@ -144,13 +144,43 @@ class command:
] = current_time
return
# Pattern 1a2: Curl classic progress format (percentage without % symbol)
# Extract percentage from curl's classic format
curl_classic_match = re.search(
r"^\s*(\d+)\s+\d+[kMGT]?\s+(\d+)\s+\d+[kMGT]?\s+\d+\s+\d+\s+\d+[kMGT]?\s+\d+\s+\d+:\d+:\d+\s+\d+:\d+:\d+\s+\d+:\d+:\d+\s+\d+[kMGT]?\s*$", text
)
if curl_classic_match:
# Use the first percentage (total progress)
percentage = float(curl_classic_match.group(1))
if 0 <= percentage <= 100:
self.env["runtime"]["DebugManager"].write_debug_out(
"found curl classic percentage: " + str(percentage),
debug.DebugLevel.INFO,
)
if (
percentage
!= self.env["commandBuffer"]["lastProgressValue"]
):
self.env["runtime"]["DebugManager"].write_debug_out(
"Playing tone for curl: " + str(percentage),
debug.DebugLevel.INFO,
)
self.play_progress_tone(percentage)
self.env["commandBuffer"][
"lastProgressValue"
] = percentage
self.env["commandBuffer"][
"lastProgressTime"
] = current_time
return
# Pattern 1b: Time/token activity (not percentage-based, so use single
# beep)
time_match = re.search(r"(\d+)s\s", text)
token_match = re.search(r"(\d+)\s+tokens", text)
time_match = re.search(r"(?:(?:remaining|elapsed|left|ETA|eta)[:;\s]*(\d+)s|(\d+)s\s+(?:remaining|elapsed|left))", text, re.IGNORECASE)
token_match = re.search(r"(?:processing|generating|used|consumed)\s+(\d+)\s+tokens", text, re.IGNORECASE)
# Pattern 1c: dd command output (bytes copied with transfer rate)
dd_match = re.search(r"\d+\s+bytes.*copied.*\d+\s+s.*[kMGT]?B/s", text)
# Pattern 1d: Curl-style transfer data (bytes, speed indicators)
# Pattern 1d: Curl-style transfer data (bytes, speed indicators - legacy)
curl_match = re.search(
r"(\d+\s+\d+\s+\d+\s+\d+.*?(?:k|M|G)?.*?--:--:--|Speed)", text
)
@@ -183,7 +213,10 @@ class command:
if fraction_match:
current = int(fraction_match.group(1))
total = int(fraction_match.group(2))
if total > 0:
# Filter out dates, page numbers, and other non-progress fractions
if (total > 0 and total <= 1000 and current <= total and
not re.search(r"\b(?:page|chapter|section|line|row|column|year|month|day)\b", text, re.IGNORECASE) and
not re.search(r"\d{1,2}/\d{1,2}/\d{2,4}", text)): # Date pattern
percentage = (current / total) * 100
if (
percentage
@@ -245,7 +278,15 @@ class command:
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 6: Moon phase progress indicators
# Pattern 6: Claude Code progress indicators
claude_progress_match = re.search(r'^[·✶✢*]\s+\w+[…\.]*\s*\(esc to interrupt\)\s*$', text)
if claude_progress_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 7: Moon phase progress indicators
moon_match = re.search(r'[🌑🌒🌓🌔🌕🌖🌗🌘]', text)
if moon_match:
moon_phases = {
@@ -274,9 +315,21 @@ class command:
self.play_quiet_tone(800, 0.08)
def play_quiet_tone(self, frequency, duration):
"""Play a quiet tone using Sox directly"""
"""Play a quiet tone using Sox directly with flood protection"""
import shlex
import subprocess
import time
# Flood protection: prevent beeps closer than 0.1 seconds apart
current_time = time.time()
if not hasattr(self, '_last_beep_time'):
self._last_beep_time = 0
if current_time - self._last_beep_time < 0.1:
# Skip this beep to prevent audio crackling on low-resource systems
return
self._last_beep_time = current_time
# Build the Sox command: play -qn synth <duration> tri <frequency> gain
# -8

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "👾"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Alien monster emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added alien monster to clipboard",
interrupt=False, flush=False
)

View File

@@ -10,13 +10,13 @@ class command():
pass
def getDescription(self):
return "Magic cauldron emoji"
return "Mage emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added magic cauldron to clipboard",
"Added mage to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "⚰️"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Coffin emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added coffin to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🧟"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Mummy emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added mummy to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🕸️"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Spider web emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added spider web to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🧟"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Zombie emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added zombie to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "😵"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Dizzy face emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added dizzy face to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🤯"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Exploding head emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added exploding head to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🤬"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Face with symbols over mouth emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added face with symbols to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "👿"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Angry face with horns emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added angry face with horns to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,23 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🤯"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Mind blown emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added mind blown to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🤢"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Nauseated face emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added nauseated face to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "😱"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Screaming face emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added screaming face to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "⛓️"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Chains emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added chains to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🦴"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Bone emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added bone to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "🗡️"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Dagger emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added dagger to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,22 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = ""
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "High voltage lightning bolt emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added high voltage lightning bolt to clipboard",
interrupt=False, flush=False
)

View File

@@ -0,0 +1,24 @@
class command():
def initialize(self, environment):
self.env = environment
self.emoji = "⚔️"
def shutdown(self):
pass
def setCallback(self, callback):
pass
def getDescription(self):
return "Sword emoji"
def run(self):
self.env["runtime"]["MemoryManager"].add_value_to_first_index(
"clipboardHistory", self.emoji
)
self.env["runtime"]["OutputManager"].present_text(
"Added sword to clipboard",
interrupt=False, flush=False
)

View File

@@ -77,6 +77,27 @@ class OutputManager:
def get_last_echo(self):
return self.last_echo
def process_mid_word_punctuation(self, text):
"""
Process punctuation that appears mid-word to ensure proper pronunciation.
Specifically handles dots between word characters (e.g., "settings.conf" -> "settings dot conf")
and dots at word beginnings (e.g., ".local" -> "dot local")
while preserving sentence-ending periods and other punctuation behavior.
"""
if not text:
return text
# Handle dots at the beginning of words (like .local, .bashrc, .config)
# Look for non-word character (or start of string), dot, then word characters
text = re.sub(r'(?<!\w)\.(\w+)', r'dot \1', text)
# Replace dots that appear between word characters with spoken form
# Use a loop to handle multiple consecutive dots like www.example.com or a.b.c.d
while re.search(r'\b\w+\.\w+\b', text):
text = re.sub(r'\b(\w+)\.(\w+)\b', r'\1 dot \2', text)
return text
def speak_text(
self,
text,
@@ -208,6 +229,7 @@ class OutputManager:
clean_text = self.env["runtime"]["TextManager"].replace_head_lines(
clean_text
)
clean_text = self.process_mid_word_punctuation(clean_text)
clean_text = self.env["runtime"][
"PunctuationManager"
].proceed_punctuation(clean_text, ignore_punctuation)

View File

@@ -4,6 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2025.08.04"
version = "2025.08.22"
codeName = "master"
code_name = "master"

View File

@@ -52,10 +52,13 @@ class driver(sound_driver):
bus.connect("message", self._on_pipeline_message)
self._source = Gst.ElementFactory.make("audiotestsrc", "src")
self._volume = Gst.ElementFactory.make("volume", "volume")
self._sink = Gst.ElementFactory.make("autoaudiosink", "output")
self._pipeline.add(self._source)
self._pipeline.add(self._volume)
self._pipeline.add(self._sink)
self._source.link(self._sink)
self._source.link(self._volume)
self._volume.link(self._sink)
self.mainloop = GLib.MainLoop()
self.thread = threading.Thread(target=self.mainloop.run)
self.thread.start()
@@ -117,8 +120,18 @@ class driver(sound_driver):
return
if interrupt:
self.cancel()
# Always reset pipeline to prevent volume accumulation
self._pipeline.set_state(Gst.State.NULL)
duration = duration * 1000
self._source.set_property("volume", self.volume * adjust_volume)
# Use dedicated volume element for better control
# GStreamer volume property behaves very differently than sox for low frequencies
if adjust_volume > 0.8: # This indicates low frequency indentation beeps
# Extremely aggressive boost - GStreamer really struggles with low frequencies
effective_volume = self.volume * adjust_volume * 50.0 # Ridiculous multiplier to match sox
else:
effective_volume = self.volume * adjust_volume * 3.0
self._volume.set_property("volume", effective_volume)
self._source.set_property("volume", 1.0) # Set source to full, control via volume element
self._source.set_property("freq", frequence)
self._pipeline.set_state(Gst.State.PLAYING)
GLib.timeout_add(duration, self._on_timeout, self._pipeline)