Compare commits
11 Commits
271c4fc18f
...
2025.07.16
Author | SHA1 | Date | |
---|---|---|---|
e177c7f486 | |||
ae4c418323 | |||
b9abf02b12 | |||
0c116adaf2 | |||
fe5e2c065e | |||
5a14804d11 | |||
ef3ebee10c | |||
6876995d4c | |||
01f4b64390 | |||
5e858cfde1 | |||
2ad754a372 |
173
README.md
173
README.md
@ -12,6 +12,8 @@ This software is licensed under the LGPL v3.
|
||||
- **Multiple Interface Support**: Works in Linux TTY, and terminal emulators
|
||||
- **Flexible Driver System**: Modular architecture with multiple drivers for speech, sound, input, and screen
|
||||
- **Review Mode**: Navigate and review screen content without moving the edit cursor
|
||||
- **Table Navigation**: Advanced table mode with column headers, cell-by-cell navigation, and boundary feedback
|
||||
- **Progress Bar Monitoring**: Automatic detection and audio feedback for progress indicators with ascending tones
|
||||
- **Multiple Clipboard Support**: Manage multiple clipboard entries
|
||||
- **Configurable Key Bindings**: Desktop and laptop keyboard layouts
|
||||
- **Sound Icons**: Audio feedback for various events
|
||||
@ -156,6 +158,7 @@ By default Fenrir uses:
|
||||
- `Keypad 2` - Read current character
|
||||
- `Fenrir + T` - Announce time
|
||||
- `Fenrir + S` - Spell check current word
|
||||
- `Fenrir + Keypad *` - Toggle table mode / highlight tracking
|
||||
|
||||
### Keyboard Layouts
|
||||
|
||||
@ -402,6 +405,176 @@ setting <action> [parameters]
|
||||
- `time#delaySec=seconds` - Announcement interval
|
||||
- `time#onMinutes=00,30` - Specific minutes to announce
|
||||
|
||||
## Table Navigation
|
||||
|
||||
Fenrir includes advanced table navigation capabilities for working with tabular data in terminal applications, CSV files, and formatted text output.
|
||||
|
||||
### Entering Table Mode
|
||||
|
||||
Table mode is activated through the **toggle_highlight_tracking** command, which cycles through three focus modes:
|
||||
|
||||
1. **Highlight tracking mode** (default) - Follows text highlighting
|
||||
2. **Cursor tracking mode** - Follows text cursor movement
|
||||
3. **Table mode** - Enables table navigation
|
||||
|
||||
**Key bindings:**
|
||||
- **Desktop layout**: `Fenrir + Keypad *` (asterisk)
|
||||
- **Laptop layout**: `Fenrir + Y`
|
||||
|
||||
Press the key combination repeatedly to cycle through modes until you hear "table mode enabled".
|
||||
|
||||
### Table Navigation Commands
|
||||
|
||||
#### Column Navigation (Desktop Layout)
|
||||
- **Next column**: `Keypad 6` - Move to next table column
|
||||
- **Previous column**: `Keypad 4` - Move to previous table column
|
||||
- **First column**: `Fenrir + Keypad 4` - Jump to first column of current row
|
||||
- **Last column**: `Fenrir + Keypad 6` - Jump to last column of current row
|
||||
|
||||
#### Column Navigation (Laptop Layout)
|
||||
- **Next column**: `Fenrir + L` - Move to next table column
|
||||
- **Previous column**: `Fenrir + J` - Move to previous table column
|
||||
- **First column**: `Fenrir + Shift + J` - Jump to first column of current row
|
||||
- **Last column**: `Fenrir + Shift + L` - Jump to last column of current row
|
||||
|
||||
#### Cell Character Navigation
|
||||
- **First character in cell**: `Fenrir + Keypad 1` (desktop) or `Fenrir + Ctrl + J` (laptop)
|
||||
- **Last character in cell**: `Fenrir + Keypad 3` (desktop) or `Fenrir + Ctrl + L` (laptop)
|
||||
|
||||
### Setting Column Headers
|
||||
|
||||
For better navigation experience, you can set column headers:
|
||||
|
||||
1. **Navigate to header row**: Use normal navigation to reach the row containing column headers
|
||||
2. **Set headers**: Press `Fenrir + X` to mark the current line as the header row
|
||||
3. **Navigation feedback**: Column headers will be announced along with cell content
|
||||
|
||||
### Table Detection
|
||||
|
||||
Fenrir automatically detects table structures using multiple strategies:
|
||||
- **Delimited text**: CSV, pipe-separated (`|`), semicolon-separated (`;`), tab-separated
|
||||
- **Aligned columns**: Space-aligned columns (2+ spaces between columns)
|
||||
- **Flexible parsing**: Handles various table formats commonly found in terminal applications
|
||||
|
||||
### Table Mode Features
|
||||
|
||||
- **Cell-by-cell navigation**: Navigate through table cells with precise positioning
|
||||
- **Column header support**: Set and announce column headers for better context
|
||||
- **Boundary feedback**: Audio cues when reaching start/end of rows
|
||||
- **Empty cell handling**: Blank cells are announced as "blank"
|
||||
- **Independent tracking**: Table position is maintained independently of cursor movement
|
||||
|
||||
### Speech Output in Table Mode
|
||||
|
||||
When navigating in table mode, Fenrir announces:
|
||||
- **Cell content** followed by **column header/name**
|
||||
- **Boundary notifications**: "end of line", "start of line"
|
||||
- **Position indicators**: "first character in cell [column name]"
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
# Working with CSV data
|
||||
cat data.csv
|
||||
Name,Age,City
|
||||
Alice,30,New York
|
||||
Bob,25,Los Angeles
|
||||
|
||||
# 1. Press Fenrir + Keypad * until "table mode enabled"
|
||||
# 2. Navigate to "Name,Age,City" line
|
||||
# 3. Press Fenrir + X to set headers
|
||||
# 4. Use Keypad 4/6 to navigate between columns
|
||||
# 5. Each cell will be announced with its column header
|
||||
```
|
||||
|
||||
## Progress Bar Monitoring
|
||||
|
||||
Fenrir provides intelligent progress bar detection and audio feedback for various progress indicators commonly found in terminal applications.
|
||||
|
||||
### Enabling Progress Monitoring
|
||||
|
||||
**Command**: `progress_bar_monitor` (no default key binding - assign manually)
|
||||
|
||||
To enable progress monitoring:
|
||||
1. Add a key binding in your keyboard layout file
|
||||
2. Or use the remote control system: `echo "command progress_bar_monitor" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock`
|
||||
|
||||
### Progress Detection Patterns
|
||||
|
||||
Fenrir automatically detects various progress indicator formats:
|
||||
|
||||
#### 1. Percentage Progress
|
||||
```
|
||||
Download: 45%
|
||||
Processing: 67.5%
|
||||
Installing: 100%
|
||||
```
|
||||
|
||||
#### 2. Fraction Progress
|
||||
```
|
||||
Files: 15/100
|
||||
Progress: 3 of 10
|
||||
Step 7/15
|
||||
```
|
||||
|
||||
#### 3. Progress Bars
|
||||
```
|
||||
[#### ] 40%
|
||||
[====> ] 50%
|
||||
[**********] 100%
|
||||
```
|
||||
|
||||
#### 4. Activity Indicators
|
||||
```
|
||||
Loading...
|
||||
Processing...
|
||||
Working...
|
||||
Installing...
|
||||
Downloading...
|
||||
Compiling...
|
||||
Building...
|
||||
```
|
||||
|
||||
### Audio Feedback
|
||||
|
||||
#### Progress Tones
|
||||
- **Ascending tones**: 400Hz to 1200Hz frequency range
|
||||
- **Percentage mapping**: 0% = 400Hz, 100% = 1200Hz
|
||||
- **Smooth progression**: Frequency increases proportionally with progress
|
||||
|
||||
#### Activity Indicators
|
||||
- **Steady beep**: 800Hz tone every 2 seconds for ongoing activity
|
||||
- **Non-intrusive**: Beeps don't interrupt speech or other audio
|
||||
|
||||
### Progress Monitoring Features
|
||||
|
||||
- **Automatic detection**: No manual configuration required
|
||||
- **Multiple format support**: Handles various progress indicator styles
|
||||
- **Prompt awareness**: Automatically pauses when command prompts are detected
|
||||
- **Non-blocking**: Progress tones don't interrupt speech or other functionality
|
||||
- **Configurable**: Can be enabled/disabled as needed
|
||||
|
||||
### Usage Examples
|
||||
|
||||
```bash
|
||||
# Enable progress monitoring
|
||||
echo "command progress_bar_monitor" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock
|
||||
|
||||
# Common scenarios where progress monitoring is useful:
|
||||
wget https://example.com/large-file.zip # Download progress
|
||||
tar -xvf archive.tar.gz # Extraction progress
|
||||
make -j4 # Compilation progress
|
||||
pacman -S package # Package installation
|
||||
rsync -av source/ destination/ # File synchronization
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
Progress monitoring can be configured through settings:
|
||||
- **Default enabled**: Set `progressMonitoring=True` in sound section
|
||||
- **Sound integration**: Works with all sound drivers (sox, gstreamer)
|
||||
- **Remote control**: Enable/disable through remote commands
|
||||
|
||||
### Scripting Examples
|
||||
|
||||
#### Bash Script for Speech Notifications
|
||||
|
@ -50,7 +50,7 @@ def check_dependency(dep: Dependency) -> bool:
|
||||
dependencyList = [
|
||||
# Core dependencies
|
||||
Dependency('FenrirCore', 'core', 'core',
|
||||
pythonImports=['daemonize', 'enchant']),
|
||||
pythonImports=['daemonize', 'enchant', 'pyperclip', 'setproctitle']),
|
||||
|
||||
# Screen drivers
|
||||
Dependency('DummyScreen', 'screen', 'dummyDriver'),
|
||||
@ -58,7 +58,7 @@ dependencyList = [
|
||||
pythonImports=['dbus'],
|
||||
devicePaths=['/dev/vcsa']),
|
||||
Dependency('PTY', 'screen', 'ptyDriver',
|
||||
pythonImports=['pyte']),
|
||||
pythonImports=['pyte', 'xdg']),
|
||||
|
||||
# Input drivers
|
||||
Dependency('DummyInput', 'input', 'dummyDriver'),
|
||||
@ -82,7 +82,11 @@ dependencyList = [
|
||||
Dependency('Speechd', 'speech', 'speechdDriver',
|
||||
pythonImports=['speechd']),
|
||||
Dependency('GenericSpeech', 'speech', 'genericDriver',
|
||||
checkCommands=['espeak-ng'])
|
||||
checkCommands=['espeak-ng']),
|
||||
|
||||
# Additional dependencies
|
||||
Dependency('Pexpect', 'core', 'pexpectDriver',
|
||||
pythonImports=['pexpect'])
|
||||
]
|
||||
|
||||
defaultModules = {
|
||||
@ -90,7 +94,8 @@ defaultModules = {
|
||||
'VCSA',
|
||||
'Evdev',
|
||||
'GenericSpeech',
|
||||
'GenericSound'
|
||||
'GenericSound',
|
||||
'Pexpect'
|
||||
}
|
||||
|
||||
def check_all_dependencies():
|
||||
|
@ -1,4 +1,46 @@
|
||||
|
||||
# Fenrir Keyboard Configuration
|
||||
|
||||
This directory contains keyboard layout files for Fenrir screen reader.
|
||||
|
||||
## Available Layouts
|
||||
|
||||
- **desktop.conf** - Desktop layout using numeric keypad (recommended)
|
||||
- **laptop.conf** - Laptop layout for keyboards without numeric keypad
|
||||
- **nvda-desktop.conf** - NVDA-compatible desktop layout
|
||||
- **nvda-laptop.conf** - NVDA-compatible laptop layout
|
||||
- **pty.conf** - PTY emulation layout for terminal use
|
||||
- **pty2.conf** - Alternative PTY emulation layout
|
||||
|
||||
## Key Features
|
||||
|
||||
### Table Navigation
|
||||
- **Toggle table mode**: `Fenrir + Keypad *` (desktop) or `Fenrir + Y` (laptop)
|
||||
- **Column navigation**: `Keypad 4/6` (desktop) or `Fenrir + J/L` (laptop)
|
||||
- **Row boundaries**: `Fenrir + Keypad 4/6` (desktop) or `Fenrir + Shift + J/L` (laptop)
|
||||
- **Set headers**: `Fenrir + X` in table mode
|
||||
|
||||
### Progress Bar Monitoring
|
||||
- **Monitor progress**: `progress_bar_monitor` command (assign key binding manually)
|
||||
- **Auto-detection**: Percentage, fractions, progress bars, activity indicators
|
||||
- **Audio feedback**: Ascending tones (400Hz-1200Hz) for progress
|
||||
|
||||
### Review Mode
|
||||
- **Basic navigation**: `Keypad 7/8/9` (lines), `Keypad 4/5/6` (words), `Keypad 1/2/3` (characters)
|
||||
- **Exit review**: `Fenrir + Keypad .`
|
||||
- **Screen reading**: `Fenrir + Keypad 5` (current screen)
|
||||
|
||||
## Configuration
|
||||
|
||||
To change keyboard layout, edit `/etc/fenrir/settings/settings.conf`:
|
||||
|
||||
```ini
|
||||
[keyboard]
|
||||
keyboardLayout=desktop # or laptop, nvda-desktop, nvda-laptop, pty, pty2
|
||||
```
|
||||
|
||||
## Available Key Constants
|
||||
|
||||
Keymap for Fenrir
|
||||
KEY_RESERVED
|
||||
KEY_ESC
|
||||
|
@ -135,6 +135,9 @@ debugFile=
|
||||
punctuationProfile=default
|
||||
punctuationLevel=some
|
||||
respectPunctuationPause=True
|
||||
# Replace undefined punctuation with spaces instead of removing them
|
||||
# This improves readability of text with punctuation like [X]mute, IP addresses, etc.
|
||||
replaceUndefinedPunctuationWithSpace=True
|
||||
newLinePause=True
|
||||
numberOfClipboards=50
|
||||
# used path for "export_clipboard_to_file"
|
||||
|
47
docs/user.md
47
docs/user.md
@ -50,6 +50,12 @@ Navigate the screen without moving the text cursor. Essential for examining cont
|
||||
- `Keypad 1/3` - Previous/next character
|
||||
- `Fenrir + Keypad dot` - Exit review mode
|
||||
|
||||
### Table Navigation
|
||||
- `Fenrir + Keypad *` - Toggle table mode / highlight tracking
|
||||
- `Keypad 4/6` - Previous/next column (in table mode)
|
||||
- `Fenrir + Keypad 4/6` - First/last column (in table mode)
|
||||
- `Fenrir + X` - Set column headers (in table mode)
|
||||
|
||||
### Information
|
||||
- `Fenrir + T` - Announce time
|
||||
- `Fenrir + T T` - Announce date
|
||||
@ -240,6 +246,47 @@ send_fenrir_command("command say Process complete")
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Table Navigation Mode
|
||||
|
||||
Fenrir includes advanced table navigation capabilities for working with tabular data in terminal applications, CSV files, and formatted text output.
|
||||
|
||||
#### Entering Table Mode
|
||||
1. Press `Fenrir + Keypad *` (desktop) or `Fenrir + Y` (laptop)
|
||||
2. Cycle through: Highlight tracking → Cursor tracking → Table mode
|
||||
3. Listen for "table mode enabled" announcement
|
||||
|
||||
#### Table Navigation Commands
|
||||
- **Column navigation**: `Keypad 4/6` - Move between columns
|
||||
- **Row boundaries**: `Fenrir + Keypad 4/6` - Jump to first/last column
|
||||
- **Cell characters**: `Fenrir + Keypad 1/3` - First/last character in cell
|
||||
- **Set headers**: `Fenrir + X` - Mark current line as column headers
|
||||
|
||||
#### Table Features
|
||||
- **Automatic detection**: Supports CSV, pipe-separated, space-aligned columns
|
||||
- **Column headers**: Set and announce headers for better context
|
||||
- **Boundary feedback**: Audio cues when reaching row boundaries
|
||||
- **Cell-by-cell navigation**: Precise positioning within tables
|
||||
|
||||
### Progress Bar Monitoring
|
||||
|
||||
Fenrir automatically detects and provides audio feedback for progress indicators.
|
||||
|
||||
#### Progress Detection
|
||||
- **Percentage**: 45%, 67.5%, 100%
|
||||
- **Fractions**: 15/100, 3 of 10, Step 7/15
|
||||
- **Progress bars**: [#### ], [====> ]
|
||||
- **Activity indicators**: Loading..., Processing...
|
||||
|
||||
#### Audio Feedback
|
||||
- **Progress tones**: Ascending 400Hz-1200Hz frequency range
|
||||
- **Activity beeps**: 800Hz tone every 2 seconds
|
||||
- **Non-intrusive**: Doesn't interrupt speech or other audio
|
||||
|
||||
#### Usage
|
||||
- **Enable**: Use `progress_bar_monitor` command (assign key binding)
|
||||
- **Automatic**: Works with downloads, compilations, installations
|
||||
- **Remote control**: Enable via socket commands
|
||||
|
||||
### Spell Checking
|
||||
- `Fenrir + S` - Spell check current word
|
||||
- `Fenrir + S S` - Add word to dictionary
|
||||
|
@ -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"]
|
||||
|
@ -99,6 +99,13 @@ class command:
|
||||
"Progress detector checking: '" + text + "'", debug.DebugLevel.INFO
|
||||
)
|
||||
|
||||
# Filter out URLs to prevent false positives
|
||||
if self.contains_url(text):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"Skipping progress detection - text contains URL", debug.DebugLevel.INFO
|
||||
)
|
||||
return
|
||||
|
||||
# Note: Auto-disable on 100% completion removed to respect user
|
||||
# settings
|
||||
|
||||
@ -147,8 +154,16 @@ class command:
|
||||
curl_match = re.search(
|
||||
r"(\d+\s+\d+\s+\d+\s+\d+.*?(?:k|M|G)?.*?--:--:--|Speed)", text
|
||||
)
|
||||
# Pattern 1e: General transfer progress (size, rate, time patterns)
|
||||
transfer_match = re.search(
|
||||
r"\d+\s+\d+[kMGT]?\s+\d+\s+\d+[kMGT]?.*?\d+\.\d+[kMGT].*?\d+:\d+:\d+", text
|
||||
)
|
||||
# Pattern 1f: Pacman-style transfer progress (flexible size/speed/time)
|
||||
pacman_match = re.search(
|
||||
r"\d+(?:\.\d+)?\s+[kKmMgGtT]iB\s+\d+(?:\.\d+)?\s+[kKmMgGtT]iB/s\s+\d+:\d+", text
|
||||
)
|
||||
|
||||
if time_match or token_match or dd_match or curl_match:
|
||||
if time_match or token_match or dd_match or curl_match or transfer_match or pacman_match:
|
||||
# For non-percentage progress, use a single activity beep every 2
|
||||
# seconds
|
||||
if (
|
||||
@ -183,7 +198,7 @@ class command:
|
||||
|
||||
# Pattern 3: Progress bars ([#### ], [====> ], etc.)
|
||||
# Improved pattern to avoid matching IRC channels like [#channel]
|
||||
bar_match = re.search(r"\[([#=\-\*]+)([\s\.]*)\]", text)
|
||||
bar_match = re.search(r"\[([#=\*]+)([\s\.\-]*)\]", text)
|
||||
if bar_match:
|
||||
filled = len(bar_match.group(1))
|
||||
unfilled = len(bar_match.group(2))
|
||||
@ -350,5 +365,22 @@ class command:
|
||||
# If anything fails, assume it's not a prompt to be safe
|
||||
return False
|
||||
|
||||
def contains_url(self, text):
|
||||
"""Check if text contains URLs that might cause false progress detection"""
|
||||
import re
|
||||
|
||||
# Common URL patterns that might contain progress-like patterns
|
||||
url_patterns = [
|
||||
r"https?://[^\s]+", # http:// or https:// URLs
|
||||
r"ftp://[^\s]+", # ftp:// URLs
|
||||
r"www\.[^\s]+", # www. domains
|
||||
r"[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[/\w.-]*", # domain.com/path patterns
|
||||
]
|
||||
|
||||
for pattern in url_patterns:
|
||||
if re.search(pattern, text, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
|
@ -20,22 +20,15 @@ class command:
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("presents the date")
|
||||
return _("Test mc search functionality")
|
||||
|
||||
def run(self):
|
||||
date_format = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"general", "date_format"
|
||||
)
|
||||
|
||||
# get the time formatted
|
||||
date_string = datetime.datetime.strftime(
|
||||
datetime.datetime.now(), date_format
|
||||
)
|
||||
|
||||
# present the time via speak and braile, there is no soundicon,
|
||||
# interrupt the current speech
|
||||
# Test command for mc search operations
|
||||
test_message = _("MC search test: This demonstrates search functionality")
|
||||
|
||||
# present the test message
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
date_string, sound_icon="", interrupt=True
|
||||
test_message, sound_icon="", interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
|
@ -20,22 +20,15 @@ class command:
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("presents the date")
|
||||
return _("Test mutt search functionality")
|
||||
|
||||
def run(self):
|
||||
date_format = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"general", "date_format"
|
||||
)
|
||||
|
||||
# get the time formatted
|
||||
date_string = datetime.datetime.strftime(
|
||||
datetime.datetime.now(), date_format
|
||||
)
|
||||
|
||||
# present the time via speak and braile, there is no soundicon,
|
||||
# interrupt the current speech
|
||||
# Test command for mutt search operations
|
||||
test_message = _("Mutt search test: This demonstrates search functionality")
|
||||
|
||||
# present the test message
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
date_string, sound_icon="", interrupt=True
|
||||
test_message, sound_icon="", interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
|
@ -20,22 +20,15 @@ class command:
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("presents the date")
|
||||
return _("Test nano search functionality")
|
||||
|
||||
def run(self):
|
||||
date_format = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"general", "date_format"
|
||||
)
|
||||
|
||||
# get the time formatted
|
||||
date_string = datetime.datetime.strftime(
|
||||
datetime.datetime.now(), date_format
|
||||
)
|
||||
|
||||
# present the time via speak and braile, there is no soundicon,
|
||||
# interrupt the current speech
|
||||
# Test command for nano search operations
|
||||
test_message = _("Nano search test: This demonstrates search functionality")
|
||||
|
||||
# present the test message
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
date_string, sound_icon="", interrupt=True
|
||||
test_message, sound_icon="", interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
|
@ -20,22 +20,15 @@ class command:
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("presents the date")
|
||||
return _("Test vim search functionality")
|
||||
|
||||
def run(self):
|
||||
date_format = self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"general", "date_format"
|
||||
)
|
||||
|
||||
# get the time formatted
|
||||
date_string = datetime.datetime.strftime(
|
||||
datetime.datetime.now(), date_format
|
||||
)
|
||||
|
||||
# present the time via speak and braile, there is no soundicon,
|
||||
# interrupt the current speech
|
||||
# Test command for vim search operations
|
||||
test_message = _("Vim search test: This demonstrates search functionality")
|
||||
|
||||
# present the test message
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
date_string, sound_icon="", interrupt=True
|
||||
test_message, sound_icon="", interrupt=True
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
|
@ -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,
|
||||
|
@ -64,7 +64,7 @@ class ProcessManager:
|
||||
args=(event_queue, function, pargs, run_once),
|
||||
)
|
||||
self._Processes.append(t)
|
||||
else: # thread not implemented yet
|
||||
else: # use thread instead of process
|
||||
t = Thread(
|
||||
target=self.custom_event_worker_thread,
|
||||
args=(event_queue, function, pargs, run_once),
|
||||
|
@ -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,
|
||||
|
@ -494,11 +494,14 @@ class SettingsManager:
|
||||
self.set_setting("general", "debug_level", 3)
|
||||
self.set_setting("general", "debug_mode", "PRINT")
|
||||
if cliArgs.emulated_pty:
|
||||
self.set_setting("screen", "driver", "ptyDriver")
|
||||
self.set_setting("keyboard", "driver", "ptyDriver")
|
||||
# TODO needs cleanup use dict
|
||||
# self.set_option_arg_dict('keyboard', 'keyboardLayout', 'pty')
|
||||
self.set_setting("keyboard", "keyboardLayout", "pty")
|
||||
# Set PTY driver settings
|
||||
pty_settings = {
|
||||
"screen": {"driver": "ptyDriver"},
|
||||
"keyboard": {"driver": "ptyDriver", "keyboardLayout": "pty"}
|
||||
}
|
||||
for section, settings in pty_settings.items():
|
||||
for key, value in settings.items():
|
||||
self.set_setting(section, key, value)
|
||||
if cliArgs.emulated_evdev:
|
||||
self.set_setting("screen", "driver", "ptyDriver")
|
||||
self.set_setting("keyboard", "driver", "evdevDriver")
|
||||
|
@ -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.16"
|
||||
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