Expirmental improvements to highlight tracking.

This commit is contained in:
Storm Dragon
2025-07-09 01:06:54 -04:00
parent 2ad754a372
commit 5e858cfde1
7 changed files with 543 additions and 1 deletions

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

@ -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

@ -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,6 +4,6 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2025.07.08"
version = "2025.07.09"
codeName = "testing"
code_name = "testing"