Preparing for new tagged version. Please watch for bugs.
This commit is contained in:
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
(
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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"]
|
||||
|
@ -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,
|
||||
|
@ -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=""):
|
||||
|
@ -50,6 +50,7 @@ settings_data = {
|
||||
"punctuationProfile": "default",
|
||||
"punctuationLevel": "some",
|
||||
"respectPunctuationPause": True,
|
||||
"replaceUndefinedPunctuationWithSpace": True,
|
||||
"newLinePause": True,
|
||||
"numberOfClipboards": 10,
|
||||
"emoticons": True,
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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([])
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user