Preparing for new tagged version. Please watch for bugs.

This commit is contained in:
Storm Dragon
2025-07-09 09:33:19 -04:00
29 changed files with 671 additions and 36 deletions

View File

@ -20,7 +20,8 @@ class command:
def get_description(self):
return _(
"read line to cursor pos, use review cursor if you are in review mode, otherwhise use text cursor"
"read line to cursor pos, use review cursor if you are in review mode, "
"otherwhise use text cursor"
)
def run(self):

View File

@ -20,7 +20,8 @@ class command:
def get_description(self):
return _(
"read to end of line, use review cursor if you are in review mode, otherwhise use text cursor"
"read to end of line, use review cursor if you are in review mode, "
"otherwhise use text cursor"
)
def run(self):

View File

@ -82,7 +82,8 @@ class command:
else:
self.env["runtime"]["OutputManager"].present_text(
_(
"failed to export to X clipboard. No available display found."
"failed to export to X clipboard. No available display "
"found."
),
interrupt=True,
)

View File

@ -20,7 +20,8 @@ class command:
def get_description(self):
return _(
"Presents the currently selected text that will be copied to the clipboard"
"Presents the currently selected text that will be copied to the "
"clipboard"
)
def run(self):

View File

@ -131,7 +131,10 @@ class command:
# Pattern 4: Generic activity indicators (Loading..., Working..., etc.)
activity_pattern = re.search(
r"(loading|processing|working|installing|downloading|compiling|building).*\.{2,}",
(
r"(loading|processing|working|installing|downloading|"
r"compiling|building).*\.{2,}"
),
text,
re.IGNORECASE,
)

View File

@ -31,14 +31,21 @@ class command:
table_info = self.env["runtime"]["TableManager"].get_current_table_cell_info()
if table_info:
cursor_pos = self.env["screen"]["newCursorReview"]
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
line_text = self.env["runtime"]["ScreenManager"].get_line_text(
cursor_pos["y"]
)
if line_text:
column_start = self.env["runtime"]["TableManager"].get_column_start_position(line_text, table_info["column_index"])
column_start = self.env["runtime"]["TableManager"].get_column_start_position(
line_text, table_info["column_index"]
)
cell_content = table_info["cell_content"]
cell_end = column_start + len(cell_content)
# If cursor is outside the current cell, move to cell start
if cursor_pos["x"] < column_start or cursor_pos["x"] >= cell_end:
if (
cursor_pos["x"] < column_start or
cursor_pos["x"] >= cell_end
):
self.env["screen"]["newCursorReview"]["x"] = column_start
(

View File

@ -47,10 +47,14 @@ class command:
)
if is_table_mode:
# Get current cell info using internal column tracking
table_info = self.env["runtime"]["TableManager"].get_current_table_cell_info()
table_info = (
self.env["runtime"]["TableManager"].get_current_table_cell_info()
)
if table_info:
# Announce with table context - cell content first, then header
output_text = f"{table_info['cell_content']} {table_info['column_header']}"
output_text = (
f"{table_info['cell_content']} {table_info['column_header']}"
)
self.env["runtime"]["OutputManager"].present_text(
output_text, interrupt=True, flush=False
)

View File

@ -24,6 +24,27 @@ class command:
)
def run(self):
# Check if we're in table mode first
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
if is_table_mode:
table_info = self.env["runtime"]["TableManager"].move_to_first_cell()
if table_info:
output_text = (
f"{table_info['cell_content']} {table_info['column_header']}"
)
self.env["runtime"]["OutputManager"].present_text(
output_text, interrupt=True, flush=False
)
self.env["runtime"]["OutputManager"].present_text(
_("first cell"), interrupt=False
)
else:
self.env["runtime"]["OutputManager"].present_text(
_("no table data"), interrupt=True, flush=False
)
return
# Regular line begin navigation (when not in table mode)
cursor_pos = self.env["runtime"][
"CursorManager"
].get_review_or_text_cursor()

View File

@ -24,6 +24,27 @@ class command:
)
def run(self):
# Check if we're in table mode first
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
if is_table_mode:
table_info = self.env["runtime"]["TableManager"].move_to_last_cell()
if table_info:
output_text = (
f"{table_info['cell_content']} {table_info['column_header']}"
)
self.env["runtime"]["OutputManager"].present_text(
output_text, interrupt=True, flush=False
)
self.env["runtime"]["OutputManager"].present_text(
_("last cell"), interrupt=False
)
else:
self.env["runtime"]["OutputManager"].present_text(
_("no table data"), interrupt=True, flush=False
)
return
# Regular line end navigation (when not in table mode)
cursor_pos = self.env["runtime"][
"CursorManager"
].get_review_or_text_cursor()

View File

@ -23,6 +23,29 @@ class command:
return _("Move Review to the first character on the line")
def run(self):
# Check if we're in table mode first
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
if is_table_mode:
table_info = self.env["runtime"]["TableManager"].move_to_first_char_in_cell()
if table_info:
char_utils.present_char_for_review(
self.env,
table_info['character'],
interrupt=True,
announce_capital=True,
flush=False,
)
self.env["runtime"]["OutputManager"].present_text(
_("first character in cell {0}").format(table_info['column_header']),
interrupt=False,
)
else:
self.env["runtime"]["OutputManager"].present_text(
_("no table data"), interrupt=True, flush=False
)
return
# Regular line first character navigation (when not in table mode)
cursor_pos = self.env["runtime"][
"CursorManager"
].get_review_or_text_cursor()

View File

@ -22,6 +22,29 @@ class command:
return _("Move Review to the last character on the line")
def run(self):
# Check if we're in table mode first
is_table_mode = self.env["runtime"]["TableManager"].is_table_mode()
if is_table_mode:
table_info = self.env["runtime"]["TableManager"].move_to_last_char_in_cell()
if table_info:
char_utils.present_char_for_review(
self.env,
table_info['character'],
interrupt=True,
announce_capital=True,
flush=False,
)
self.env["runtime"]["OutputManager"].present_text(
_("last character in cell {0}").format(table_info['column_header']),
interrupt=False,
)
else:
self.env["runtime"]["OutputManager"].present_text(
_("no table data"), interrupt=True, flush=False
)
return
# Regular line last character navigation (when not in table mode)
cursor_pos = self.env["runtime"][
"CursorManager"
].get_review_or_text_cursor()

View File

@ -31,9 +31,13 @@ class command:
table_info = self.env["runtime"]["TableManager"].get_current_table_cell_info()
if table_info:
cursor_pos = self.env["screen"]["newCursorReview"]
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
line_text = self.env["runtime"]["ScreenManager"].get_line_text(
cursor_pos["y"]
)
if line_text:
column_start = self.env["runtime"]["TableManager"].get_column_start_position(line_text, table_info["column_index"])
column_start = self.env["runtime"]["TableManager"].get_column_start_position(
line_text, table_info["column_index"]
)
cell_content = table_info["cell_content"]
cell_end = column_start + len(cell_content)
@ -48,7 +52,9 @@ class command:
flush=False,
)
self.env["runtime"]["OutputManager"].present_text(
_("end of cell"), interrupt=False, sound_icon="EndOfLine"
_("end of cell"),
interrupt=False,
sound_icon="EndOfLine"
)
return
@ -56,7 +62,9 @@ class command:
relative_pos = cursor_pos["x"] - column_start
if relative_pos < len(cell_content) - 1:
new_relative_pos = relative_pos + 1
self.env["screen"]["newCursorReview"]["x"] = column_start + new_relative_pos
self.env["screen"]["newCursorReview"]["x"] = (
column_start + new_relative_pos
)
# Get character at new position
if new_relative_pos < len(cell_content):

View File

@ -30,7 +30,9 @@ class command:
debug.DebugLevel.INFO
)
if is_table_mode:
table_info = self.env["runtime"]["TableManager"].move_to_next_column()
table_info = (
self.env["runtime"]["TableManager"].move_to_next_column()
)
if table_info and table_info.get("at_end"):
# Stay on current cell and play end of line sound
current_info = table_info["current_info"]
@ -41,11 +43,15 @@ class command:
)
# Play end of line sound
self.env["runtime"]["OutputManager"].present_text(
_("end of line"), interrupt=False, sound_icon="EndOfLine"
_("end of line"),
interrupt=False,
sound_icon="EndOfLine"
)
elif table_info:
# Normal column navigation - announce cell content with column info
output_text = f"{table_info['cell_content']} {table_info['column_header']}"
output_text = (
f"{table_info['cell_content']} {table_info['column_header']}"
)
self.env["runtime"]["OutputManager"].present_text(
output_text, interrupt=True, flush=False
)

View File

@ -35,22 +35,31 @@ class command:
table_info = self.env["runtime"]["TableManager"].get_current_table_cell_info()
if table_info:
cursor_pos = self.env["screen"]["newCursorReview"]
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
line_text = self.env["runtime"]["ScreenManager"].get_line_text(
cursor_pos["y"]
)
if line_text:
column_start = self.env["runtime"]["TableManager"].get_column_start_position(line_text, table_info["column_index"])
column_start = self.env["runtime"]["TableManager"].get_column_start_position(
line_text, table_info["column_index"]
)
# Check if we're already at the start of the cell
if cursor_pos["x"] <= column_start:
# At cell boundary - announce start and don't move
char_utils.present_char_for_review(
self.env,
table_info["cell_content"][0] if table_info["cell_content"] else "",
(
table_info["cell_content"][0]
if table_info["cell_content"] else ""
),
interrupt=True,
announce_capital=True,
flush=False,
)
self.env["runtime"]["OutputManager"].present_text(
_("start of cell"), interrupt=False, sound_icon="StartOfLine"
_("start of cell"),
interrupt=False,
sound_icon="StartOfLine"
)
return
@ -59,7 +68,9 @@ class command:
relative_pos = cursor_pos["x"] - column_start
if relative_pos > 0:
new_relative_pos = relative_pos - 1
self.env["screen"]["newCursorReview"]["x"] = column_start + new_relative_pos
self.env["screen"]["newCursorReview"]["x"] = (
column_start + new_relative_pos
)
# Get character at new position
if new_relative_pos < len(cell_content):

View File

@ -30,22 +30,30 @@ class command:
debug.DebugLevel.INFO
)
if is_table_mode:
table_info = self.env["runtime"]["TableManager"].move_to_prev_column()
table_info = (
self.env["runtime"]["TableManager"].move_to_prev_column()
)
if table_info and table_info.get("at_start"):
# Stay on current cell at beginning of line
current_info = table_info["current_info"]
if current_info:
output_text = f"{current_info['cell_content']} {current_info['column_header']}"
output_text = (
f"{current_info['cell_content']} {current_info['column_header']}"
)
self.env["runtime"]["OutputManager"].present_text(
output_text, interrupt=True, flush=False
)
# Play start of line sound
self.env["runtime"]["OutputManager"].present_text(
_("start of line"), interrupt=False, sound_icon="StartOfLine"
_("start of line"),
interrupt=False,
sound_icon="StartOfLine"
)
elif table_info:
# Normal column navigation - announce cell content with column info
output_text = f"{table_info['cell_content']} {table_info['column_header']}"
output_text = (
f"{table_info['cell_content']} {table_info['column_header']}"
)
self.env["runtime"]["OutputManager"].present_text(
output_text, interrupt=True, flush=False
)

View File

@ -19,7 +19,8 @@ class command:
def get_description(self):
return _(
"Enables or disables automatic reading of time after specified intervals"
"Enables or disables automatic reading of time after specified "
"intervals"
)
def run(self):

View File

@ -31,7 +31,8 @@ class command:
table_mode = self.env["runtime"]["TableManager"].is_table_mode()
self.env["runtime"]["DebugManager"].write_debug_out(
f"toggle_highlight_tracking: highlight={highlight_mode}, cursor={cursor_mode}, table={table_mode}",
f"toggle_highlight_tracking: highlight={highlight_mode}, "
f"cursor={cursor_mode}, table={table_mode}",
debug.DebugLevel.INFO
)

View File

@ -29,7 +29,9 @@ class command:
if self.env["runtime"]["HelpManager"].is_tutorial_mode():
self.env["runtime"]["OutputManager"].present_text(
_(
"Entering tutorial mode. In this mode commands are described but not executed. You can move through the list of commands with the up and down arrow keys. To Exit tutorial mode press Fenrir+f1."
"Entering tutorial mode. In this mode commands are described but not "
"executed. You can move through the list of commands with the up and "
"down arrow keys. To Exit tutorial mode press Fenrir+f1."
),
interrupt=True,
)

View File

@ -30,7 +30,8 @@ class command:
return
if self.env["runtime"]["AttributeManager"].is_attribute_change():
return
# hack for pdmenu and maybe other dialog apps that place the cursor at last cell/row
# hack for pdmenu and maybe other dialog apps that place the cursor at
# last cell/row
# this is not to be identified as history
if (
self.env["screen"]["new_cursor"]["x"]

View File

@ -322,6 +322,9 @@ class AttributeManager:
This is crucial for screen readers to announce when text becomes highlighted,
selected, or changes visual emphasis (bold, reverse video, color changes, etc.)
Enhanced version includes bracket pattern detection for better context in applications
like ninjam that use patterns like [X]mute or [ ]mute.
Returns:
tuple: (highlighted_text, cursor_position)
- highlighted_text: string of characters that gained highlighting
@ -352,6 +355,9 @@ class AttributeManager:
if len(text_lines) != len(self.currAttributes):
return result, curr_cursor
# Track highlighted positions for context analysis
highlighted_positions = []
# Compare attributes line by line, character by character
for line in range(len(self.prevAttributes)):
if self.prevAttributes[line] != self.currAttributes[line]:
@ -373,13 +379,316 @@ class AttributeManager:
# for navigation
if not curr_cursor:
curr_cursor = {"x": column, "y": line}
# Store position for context analysis
highlighted_positions.append((line, column))
# Accumulate highlighted characters
result += text_lines[line][column]
# Add space between lines of highlighted text for speech
# clarity
result += " "
# Enhanced bracket pattern detection for better context
if highlighted_positions:
try:
enhanced_result = self._detect_bracket_context(text_lines, highlighted_positions, result)
if enhanced_result and enhanced_result != result:
# Debug logging for bracket detection
try:
self.env["runtime"]["DebugManager"].write_debug_out(
f"AttributeManager bracket detection: Original='{result}' Enhanced='{enhanced_result}'",
debug.DebugLevel.INFO
)
except Exception:
pass # Don't let debug logging break functionality
result = enhanced_result
except Exception as e:
# If bracket detection fails, fall back to original result
try:
self.env["runtime"]["DebugManager"].write_debug_out(
f"AttributeManager bracket detection error: {e}",
debug.DebugLevel.ERROR
)
except Exception:
pass # Don't let debug logging break functionality
return result, curr_cursor
def _detect_bracket_context(self, text_lines, highlighted_positions, original_result):
"""
Analyzes highlighted positions to detect bracket patterns and provide better context.
This method specifically looks for patterns like [X]mute, [ ]mute, [X]xmit, etc.
that are common in applications like ninjam where the bracket content indicates
state but the meaningful context is the surrounding text.
Args:
text_lines: List of text lines from the screen
highlighted_positions: List of (line, column) tuples of highlighted characters
original_result: The original highlighted text result
Returns:
str: Enhanced result with context, or None if no bracket pattern detected
"""
if not highlighted_positions:
return None
# Group positions by line for easier analysis
positions_by_line = {}
for line, column in highlighted_positions:
if line not in positions_by_line:
positions_by_line[line] = []
positions_by_line[line].append(column)
enhanced_results = []
for line_num, columns in positions_by_line.items():
if line_num >= len(text_lines):
continue
line_text = text_lines[line_num]
columns.sort() # Process columns in order
# Look for bracket patterns in this line
bracket_context = self._analyze_bracket_pattern(line_text, columns)
if bracket_context:
enhanced_results.append(bracket_context)
# If we found enhanced context, return it; otherwise return None to use original
if enhanced_results:
# Remove duplicates while preserving order
unique_results = []
seen = set()
for result in enhanced_results:
if result not in seen:
unique_results.append(result)
seen.add(result)
return " ".join(unique_results)
return None
def _analyze_bracket_pattern(self, line_text, highlighted_columns):
"""
Analyzes a single line for bracket patterns around highlighted positions.
Looks for patterns like:
- master: [X]mute -> "master mute on"
- metronome: [ ]mute -> "metronome mute off"
- [Left] [X]xmit -> "Left xmit on"
Args:
line_text: The text of the line
highlighted_columns: List of column positions that are highlighted
Returns:
str: Context-aware description, or None if no pattern found
"""
if not line_text or not highlighted_columns:
return None
# Look for bracket patterns around highlighted positions
# Process columns in order and only return the first meaningful match
for col in highlighted_columns:
bracket_info = self._find_bracket_at_position(line_text, col)
if bracket_info:
bracket_start, bracket_end, bracket_content = bracket_info
# Get context before and after the bracket
context_before = self._get_context_before(line_text, bracket_start)
context_after = self._get_context_after(line_text, bracket_end)
# Build meaningful description
description = self._build_bracket_description(
context_before, bracket_content, context_after
)
# Only return if we have meaningful context (not just state)
if description and (context_before or context_after):
return description
return None
def _find_bracket_at_position(self, line_text, position):
"""
Determines if a position is within a bracket pattern like [X] or [ ].
Args:
line_text: The text line
position: Column position to check
Returns:
tuple: (bracket_start, bracket_end, bracket_content) or None
"""
if position >= len(line_text):
return None
# Look for opening bracket before or at position
bracket_start = None
for i in range(max(0, position - 2), min(len(line_text), position + 3)):
if line_text[i] == '[':
bracket_start = i
break
if bracket_start is None:
return None
# Look for closing bracket after bracket_start
bracket_end = None
for i in range(bracket_start + 1, min(len(line_text), bracket_start + 5)):
if line_text[i] == ']':
bracket_end = i
break
if bracket_end is None:
return None
# Check if our position is within the bracket
if bracket_start <= position <= bracket_end:
bracket_content = line_text[bracket_start + 1:bracket_end]
# Filter out brackets that are likely not controls
# Skip brackets that contain complex content like "[-10.5dB center]"
if len(bracket_content) > 3 and ('dB' in bracket_content or 'Hz' in bracket_content):
return None
return bracket_start, bracket_end, bracket_content
return None
def _get_context_before(self, line_text, bracket_start):
"""
Gets meaningful context words before a bracket.
Args:
line_text: The text line
bracket_start: Position of opening bracket
Returns:
str: Context words before bracket, or empty string
"""
if bracket_start <= 0:
return ""
# Get text before bracket
before_text = line_text[:bracket_start].rstrip()
# Special handling for common ninjam patterns
# Look for patterns like "master: " or "metronome: " or bracketed labels
# Check if we have a colon-separated label immediately before
if ':' in before_text:
# Get the last colon-separated part
parts = before_text.split('|') # Split by pipe first
last_part = parts[-1].strip()
if ':' in last_part:
# Extract the label before the colon
label_parts = last_part.split(':')
if len(label_parts) >= 2:
return label_parts[-2].strip()
# Look for bracketed content that might be a label
# Pattern: [Something] [X]target -> "Something"
bracket_matches = []
i = 0
while i < len(before_text):
if before_text[i] == '[':
start = i
i += 1
while i < len(before_text) and before_text[i] != ']':
i += 1
if i < len(before_text): # Found closing bracket
content = before_text[start+1:i]
if content.strip():
bracket_matches.append(content.strip())
i += 1
# If we found bracketed content, use the last one
if bracket_matches:
return bracket_matches[-1]
# Fall back to removing bracketed content and getting words
cleaned_text = ""
bracket_level = 0
for char in before_text:
if char == '[':
bracket_level += 1
elif char == ']':
bracket_level -= 1
elif bracket_level == 0:
cleaned_text += char
# Clean up separators and get meaningful words
words = []
for word in cleaned_text.replace(':', '').replace('|', '').strip().split():
if word and not word.startswith('[') and not word.endswith(']'):
words.append(word)
# Return last word as context
if words:
return words[-1]
return ""
def _get_context_after(self, line_text, bracket_end):
"""
Gets meaningful context words after a bracket.
Args:
line_text: The text line
bracket_end: Position of closing bracket
Returns:
str: Context words after bracket, or empty string
"""
if bracket_end >= len(line_text) - 1:
return ""
# Get text after bracket and find first meaningful word
after_text = line_text[bracket_end + 1:].lstrip()
# Get first word, removing common separators
words = after_text.replace(':', '').replace('|', '').strip().split()
if words:
return words[0]
return ""
def _build_bracket_description(self, context_before, bracket_content, context_after):
"""
Builds a human-readable description from bracket context.
Args:
context_before: Words before the bracket
bracket_content: Content inside brackets (X, space, etc.)
context_after: Words after the bracket
Returns:
str: Human-readable description
"""
# Clean up bracket content
bracket_content = bracket_content.strip()
# Determine state based on bracket content
if bracket_content == 'X':
state = "on"
elif bracket_content == '' or bracket_content == ' ':
state = "off"
else:
state = bracket_content # For other patterns
# Build description prioritizing context_before, then context_after
# Add spaces between components for better speech clarity
components = []
if context_before:
components.append(context_before)
if context_after:
components.append(context_after)
components.append(state)
# Join with spaces for better speech flow
return " ".join(components)
def is_useful_for_tracking(
self,
line,

View File

@ -49,11 +49,38 @@ class PunctuationManager:
def remove_unused(self, text, curr_level=""):
# dont translate dot and comma because they create a pause
curr_all_punct_none = self.allPunctNone.copy()
# Check if we should replace undefined punctuation with spaces
replace_with_space = self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"general", "replaceUndefinedPunctuationWithSpace"
)
# If the setting is disabled, use the old behavior (remove completely)
if not replace_with_space:
# Create a map that removes undefined punctuation instead of replacing with spaces
curr_all_punct_none = dict.fromkeys(
map(ord, string.punctuation + "§ "), None
)
# Restore the pause-important characters
for char in [
ord("'"),
ord("."),
ord(","),
ord(";"),
ord(":"),
ord("?"),
ord("!"),
ord("-"),
]:
curr_all_punct_none[char] = chr(char)
# Remove characters that are defined in the current punctuation level
for char in curr_level:
try:
del curr_all_punct_none[ord(char)]
except Exception as e:
pass
return text.translate(curr_all_punct_none)
def use_custom_dict(self, text, customDict, seperator=""):

View File

@ -50,6 +50,7 @@ settings_data = {
"punctuationProfile": "default",
"punctuationLevel": "some",
"respectPunctuationPause": True,
"replaceUndefinedPunctuationWithSpace": True,
"newLinePause": True,
"numberOfClipboards": 10,
"emoticons": True,

View File

@ -382,6 +382,151 @@ class TableManager:
# Check if cursor is within the column bounds
return column_start <= cursor_x < column_end
def move_to_first_cell(self):
"""Move to first cell in current row"""
if not self.env["runtime"]["CursorManager"].is_review_mode():
return None
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
if not cursor_pos:
return None
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
if not line_text:
return None
columns = self.parse_line_into_columns(line_text)
if not columns:
return None
# Set current column to first column
self.currentColumn = 0
# Return info for the first column
return self.get_table_cell_info_by_indices(cursor_pos["y"], 0)
def move_to_last_cell(self):
"""Move to last cell in current row"""
if not self.env["runtime"]["CursorManager"].is_review_mode():
return None
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
if not cursor_pos:
return None
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
if not line_text:
return None
columns = self.parse_line_into_columns(line_text)
if not columns:
return None
# Set current column to last column
self.currentColumn = len(columns) - 1
# Return info for the last column
return self.get_table_cell_info_by_indices(cursor_pos["y"], self.currentColumn)
def move_to_first_char_in_cell(self):
"""Move to first character in current cell"""
if not self.env["runtime"]["CursorManager"].is_review_mode():
return None
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
if not cursor_pos:
return None
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
if not line_text:
return None
columns = self.parse_line_into_columns(line_text)
if not columns or self.currentColumn < 0 or self.currentColumn >= len(columns):
return None
# Get column start position
column_start = self.get_column_start_position(line_text, self.currentColumn)
# Find first non-space character in the column
column_text = columns[self.currentColumn]
first_char_offset = len(column_text) - len(column_text.lstrip())
# Set cursor position to first character in cell
new_x = column_start + first_char_offset
self.env["runtime"]["CursorManager"].set_review_cursor_position(new_x, cursor_pos["y"])
# Get the character at the new position
from fenrirscreenreader.utils import char_utils
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
curr_char,
) = char_utils.get_current_char(
new_x,
cursor_pos["y"],
self.env["screen"]["new_content_text"],
)
return {
'cell_content': column_text.strip(),
'column_header': self.get_column_header(self.currentColumn),
'character': curr_char,
'position': 'first'
}
def move_to_last_char_in_cell(self):
"""Move to last character in current cell"""
if not self.env["runtime"]["CursorManager"].is_review_mode():
return None
cursor_pos = self.env["runtime"]["CursorManager"].get_review_or_text_cursor()
if not cursor_pos:
return None
line_text = self.env["runtime"]["ScreenManager"].get_line_text(cursor_pos["y"])
if not line_text:
return None
columns = self.parse_line_into_columns(line_text)
if not columns or self.currentColumn < 0 or self.currentColumn >= len(columns):
return None
# Get column start position
column_start = self.get_column_start_position(line_text, self.currentColumn)
column_text = columns[self.currentColumn]
# Find last non-space character in the column
trimmed_text = column_text.rstrip()
if not trimmed_text:
# If empty cell, go to column start
new_x = column_start
else:
# Find the position of the last character
new_x = column_start + len(trimmed_text) - 1
# Set cursor position to last character in cell
self.env["runtime"]["CursorManager"].set_review_cursor_position(new_x, cursor_pos["y"])
# Get the character at the new position
from fenrirscreenreader.utils import char_utils
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
curr_char,
) = char_utils.get_current_char(
new_x,
cursor_pos["y"],
self.env["screen"]["new_content_text"],
)
return {
'cell_content': column_text.strip(),
'column_header': self.get_column_header(self.currentColumn),
'character': curr_char,
'position': 'last'
}
def reset_table_mode(self):
self.set_head_line()

View File

@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2025.07.08"
version = "2025.07.09"
code_name = "master"

View File

@ -73,7 +73,8 @@ class driver(remoteDriver):
rawdata = client_sock.recv(8129)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"unixDriver watch_dog: Error receiving data from client: "
"unixDriver watch_dog: Error receiving data from "
"client: "
+ str(e),
debug.DebugLevel.ERROR,
)

View File

@ -66,7 +66,8 @@ class Terminal:
# Terminal class doesn't have access to env, use fallback
# logging
print(
f"ptyDriver Terminal update_attributes: Error accessing attributes: {e}"
f"ptyDriver Terminal update_attributes: Error accessing "
f"attributes: {e}"
)
self.attributes.append([])

View File

@ -55,7 +55,9 @@ class driver(sound_driver):
if self.soundFileCommand == "":
self.soundFileCommand = "play -q -v fenrirVolume fenrirSoundFile"
if self.frequenceCommand == "":
self.frequenceCommand = "play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence"
self.frequenceCommand = (
"play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence"
)
self._initialized = True
def play_frequence(

View File

@ -12,9 +12,11 @@ from fenrirscreenreader.core.speechDriver import speech_driver
class driver(speech_driver):
"""Speech-dispatcher driver for Fenrir screen reader.
This driver provides text-to-speech functionality through speech-dispatcher,
This driver provides text-to-speech functionality through
speech-dispatcher,
which acts as a common interface to various TTS engines. It supports voice
selection, speech parameters (rate, pitch, volume), and multiple TTS modules.
selection, speech parameters (rate, pitch, volume), and multiple TTS
modules.
Features:
- Dynamic voice switching and parameter adjustment