19 Commits

Author SHA1 Message Date
ef3ebee10c Preparing for new tagged version. Please watch for bugs. 2025-07-09 09:33:19 -04:00
271c4fc18f Table mode fixes and improvements to application detection. 2025-07-08 14:41:43 -04:00
ea56b90b48 Oops, getting used to this pep8 thing myself. Fixed codeName to code_name. 2025-07-06 18:51:43 -04:00
1268d989b7 Merge after mostly converting to pep8 compliance. 2025-07-06 18:34:28 -04:00
23c3ad20a1 More code optmizations. Removed fenrir+pk_plus. The functionality of bringing back speech has been added to the temperary speech interruption key kp_enter. 2025-06-30 22:25:01 -04:00
8af1cca879 Latest changes and bug fixes. 2025-06-27 21:18:27 -04:00
a394ea0222 Code cleanup and bug fixes. 2025-06-20 02:19:57 -04:00
efb308ac72 latest testing code merged. Nothing major reported from testing branch, so if we get no reports, this will become the next stable release. I'm waiting a bit to tag because major new features introduced. 2025-06-17 00:53:28 -04:00
f6be6c54fb Bug fixes mostlry, tested and seems to be working better. 2025-06-13 23:21:46 -04:00
f18c31df6c Merge branch 'testing' bug fix for remoteDriver 2025-06-07 18:24:44 -04:00
3dca3e5b23 Merged for new release. 2025-06-07 12:23:53 -04:00
1b9a9a90b1 Fixed version conflict. 2025-06-06 20:35:07 -04:00
4c8c8d896d Fixed version conflict. 2025-06-05 16:05:11 -04:00
4672592dba Latest merge from testing. 2025-04-28 15:41:14 -04:00
7a12992b88 latest release. 2025-04-17 00:36:26 -04:00
7a87fb51bb Fixed version for master branch. 2025-04-14 20:04:14 -04:00
2cc2fda28c Actually fix the version file this time. 2025-03-02 17:59:20 -05:00
c99d0f6ee1 Fixed version.py. 2025-03-02 17:44:32 -05:00
5b642cd9e2 Fixed error in settings file. 2025-02-26 17:41:01 -05:00
70 changed files with 159 additions and 1924 deletions

173
README.md
View File

@ -12,8 +12,6 @@ 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
@ -158,7 +156,6 @@ 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
@ -405,176 +402,6 @@ 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

View File

@ -50,7 +50,7 @@ def check_dependency(dep: Dependency) -> bool:
dependencyList = [
# Core dependencies
Dependency('FenrirCore', 'core', 'core',
pythonImports=['daemonize', 'enchant', 'pyperclip', 'setproctitle']),
pythonImports=['daemonize', 'enchant']),
# Screen drivers
Dependency('DummyScreen', 'screen', 'dummyDriver'),
@ -58,7 +58,7 @@ dependencyList = [
pythonImports=['dbus'],
devicePaths=['/dev/vcsa']),
Dependency('PTY', 'screen', 'ptyDriver',
pythonImports=['pyte', 'xdg']),
pythonImports=['pyte']),
# Input drivers
Dependency('DummyInput', 'input', 'dummyDriver'),
@ -82,11 +82,7 @@ dependencyList = [
Dependency('Speechd', 'speech', 'speechdDriver',
pythonImports=['speechd']),
Dependency('GenericSpeech', 'speech', 'genericDriver',
checkCommands=['espeak-ng']),
# Additional dependencies
Dependency('Pexpect', 'core', 'pexpectDriver',
pythonImports=['pexpect'])
checkCommands=['espeak-ng'])
]
defaultModules = {
@ -94,8 +90,7 @@ defaultModules = {
'VCSA',
'Evdev',
'GenericSpeech',
'GenericSound',
'Pexpect'
'GenericSound'
}
def check_all_dependencies():

View File

@ -1,46 +1,4 @@
# 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

View File

@ -50,12 +50,6 @@ 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
@ -246,47 +240,6 @@ 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

View File

@ -99,13 +99,6 @@ 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
@ -154,16 +147,8 @@ 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 or transfer_match or pacman_match:
if time_match or token_match or dd_match or curl_match:
# For non-percentage progress, use a single activity beep every 2
# seconds
if (
@ -198,7 +183,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))
@ -235,30 +220,6 @@ class command:
):
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 5: Braille progress indicators
braille_match = re.search(r'[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⡿⣟⣯⣷⣾⣽⣻⢿]', text)
if braille_match:
if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0:
self.play_activity_beep()
self.env["commandBuffer"]["lastProgressTime"] = current_time
return
# Pattern 6: Moon phase progress indicators
moon_match = re.search(r'[🌑🌒🌓🌔🌕🌖🌗🌘]', text)
if moon_match:
moon_phases = {
'🌑': 0, '🌒': 12.5, '🌓': 25, '🌔': 37.5,
'🌕': 50, '🌖': 62.5, '🌗': 75, '🌘': 87.5
}
moon_char = moon_match.group(0)
if moon_char in moon_phases:
percentage = moon_phases[moon_char]
if percentage != self.env["commandBuffer"]["lastProgressValue"]:
self.play_progress_tone(percentage)
self.env["commandBuffer"]["lastProgressValue"] = percentage
return
def play_progress_tone(self, percentage):
# Map 0-100% to 400-1200Hz frequency range
@ -389,22 +350,5 @@ 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

View File

@ -1 +0,0 @@
# Emoji VMenu category

View File

@ -1 +0,0 @@
# Flags emoji subcategory

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
# Food emoji subcategory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
# Holidays emoji subcategory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
# Nature emoji subcategory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
# People emoji subcategory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
# Symbols emoji subcategory

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,15 +20,22 @@ class command:
pass
def get_description(self):
return _("Test mc search functionality")
return _("presents the date")
def run(self):
# Test command for mc search operations
test_message = _("MC search test: This demonstrates search functionality")
date_format = self.env["runtime"]["SettingsManager"].get_setting(
"general", "date_format"
)
# present the test message
# 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
self.env["runtime"]["OutputManager"].present_text(
test_message, sound_icon="", interrupt=True
date_string, sound_icon="", interrupt=True
)
def set_callback(self, callback):

View File

@ -20,15 +20,22 @@ class command:
pass
def get_description(self):
return _("Test mutt search functionality")
return _("presents the date")
def run(self):
# Test command for mutt search operations
test_message = _("Mutt search test: This demonstrates search functionality")
date_format = self.env["runtime"]["SettingsManager"].get_setting(
"general", "date_format"
)
# present the test message
# 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
self.env["runtime"]["OutputManager"].present_text(
test_message, sound_icon="", interrupt=True
date_string, sound_icon="", interrupt=True
)
def set_callback(self, callback):

View File

@ -20,15 +20,22 @@ class command:
pass
def get_description(self):
return _("Test nano search functionality")
return _("presents the date")
def run(self):
# Test command for nano search operations
test_message = _("Nano search test: This demonstrates search functionality")
date_format = self.env["runtime"]["SettingsManager"].get_setting(
"general", "date_format"
)
# present the test message
# 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
self.env["runtime"]["OutputManager"].present_text(
test_message, sound_icon="", interrupt=True
date_string, sound_icon="", interrupt=True
)
def set_callback(self, callback):

View File

@ -20,15 +20,22 @@ class command:
pass
def get_description(self):
return _("Test vim search functionality")
return _("presents the date")
def run(self):
# Test command for vim search operations
test_message = _("Vim search test: This demonstrates search functionality")
date_format = self.env["runtime"]["SettingsManager"].get_setting(
"general", "date_format"
)
# present the test message
# 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
self.env["runtime"]["OutputManager"].present_text(
test_message, sound_icon="", interrupt=True
date_string, sound_icon="", interrupt=True
)
def set_callback(self, callback):

View File

@ -64,7 +64,7 @@ class ProcessManager:
args=(event_queue, function, pargs, run_once),
)
self._Processes.append(t)
else: # use thread instead of process
else: # thread not implemented yet
t = Thread(
target=self.custom_event_worker_thread,
args=(event_queue, function, pargs, run_once),

View File

@ -494,14 +494,11 @@ class SettingsManager:
self.set_setting("general", "debug_level", 3)
self.set_setting("general", "debug_mode", "PRINT")
if cliArgs.emulated_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)
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")
if cliArgs.emulated_evdev:
self.set_setting("screen", "driver", "ptyDriver")
self.set_setting("keyboard", "driver", "evdevDriver")

View File

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

View File

@ -39,177 +39,7 @@ class driver(inputDriver):
Args:
environment: Fenrir environment dictionary
Returns:
bool: True if initialization successful, False otherwise
"""
try:
if environment is None:
raise ValueError("Environment cannot be None")
self.env = environment
# Validate required managers are available
if "runtime" not in self.env:
raise ValueError("Runtime environment missing")
if "InputManager" not in self.env["runtime"]:
raise ValueError("InputManager not available")
self.env["runtime"]["InputManager"].set_shortcut_type("BYTE")
self._is_initialized = True
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY inputDriver: Initialized with byte-based shortcuts",
debug.DebugLevel.INFO
)
return True
except Exception as e:
# Log error if possible, otherwise fallback to print
try:
if hasattr(self, 'env') and self.env and "runtime" in self.env:
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY inputDriver: Initialization failed: {e}",
debug.DebugLevel.ERROR
)
else:
print(f"PTY inputDriver initialization error: {e}")
except:
print(f"PTY inputDriver initialization error: {e}")
self._is_initialized = False
return False
def shutdown(self):
"""Shutdown the PTY input driver.
Performs cleanup operations when the driver is being stopped.
For PTY driver, this involves cleaning up any resources and
logging the shutdown.
"""
if not self._is_initialized:
return
try:
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY inputDriver: Shutting down",
debug.DebugLevel.INFO
)
except Exception as e:
# Fallback logging if debug manager is unavailable
print(f"PTY inputDriver shutdown error: {e}")
finally:
self._is_initialized = False
def get_input_event(self):
"""Get input event from PTY.
For PTY driver, input events are handled through the byte-based
shortcut system rather than direct device events. This method
returns None as PTY input is processed through the screen driver
and InputManager's byte processing.
Returns:
None: PTY driver uses byte-based processing, not event-based
"""
return None
def is_device_connected(self):
"""Check if PTY input device is connected.
For PTY driver, the "device" is the terminal interface itself,
which is considered connected if the driver is initialized.
Returns:
bool: True if driver is initialized, False otherwise
"""
return self._is_initialized
def get_device_name(self):
"""Get the name of the PTY input device.
Returns:
str: Human-readable name of the PTY input device
"""
return "PTY (Pseudo-terminal) Input"
def grab_devices(self, grab=True):
"""Grab or release input devices.
For PTY driver, device grabbing is not applicable since input
is processed through terminal emulation rather than direct
device access.
Args:
grab (bool): Whether to grab (True) or release (False) devices
Returns:
bool: Always returns True for PTY driver (no-op success)
"""
if not self._is_initialized:
return False
action = "grab" if grab else "release"
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY inputDriver: {action} devices (no-op for PTY)",
debug.DebugLevel.INFO
)
return True
def has_device_detection(self):
"""Check if driver supports device detection.
PTY driver does not support dynamic device detection since
it operates on the terminal interface directly.
Returns:
bool: Always False for PTY driver
"""
return False
def get_device_list(self):
"""Get list of available input devices.
For PTY driver, there is only one logical device - the terminal
interface itself.
Returns:
list: Single-item list containing PTY device info
"""
if not self._is_initialized:
return []
return [{
'name': 'PTY Terminal',
'path': '/dev/pts/*',
'type': 'terminal',
'connected': True
}]
def get_led_state(self, led_mask=None):
"""Get LED state information.
PTY driver cannot access LED states since it operates through
terminal emulation rather than direct hardware access.
Args:
led_mask: LED mask parameter (ignored for PTY)
Returns:
dict: Empty dict (no LED access for PTY)
"""
return {}
def set_led_state(self, led_dict):
"""Set LED states.
PTY driver cannot control LEDs since it operates through
terminal emulation rather than direct hardware access.
Args:
led_dict (dict): LED state dictionary (ignored for PTY)
Returns:
bool: Always False (LED control not supported)
"""
return False

View File

@ -13,7 +13,6 @@ import signal
import struct
import sys
import termios
import threading
import time
import tty
from select import select
@ -25,27 +24,6 @@ from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.screenDriver import ScreenDriver as screenDriver
from fenrirscreenreader.utils import screen_utils
# PTY Driver Constants
class PTYConstants:
# Timeouts (in seconds)
DEFAULT_READ_TIMEOUT = 0.3
INPUT_READ_TIMEOUT = 0.01
OUTPUT_READ_TIMEOUT = 0.05 # Faster than default but allows for network lag
SELECT_TIMEOUT = 0.05
PROCESS_TERMINATION_TIMEOUT = 3.0
PROCESS_KILL_DELAY = 0.5
# Polling intervals (in seconds)
MIN_POLL_INTERVAL = 0.001
# Limits
MAX_TERMINAL_LINES = 10000
DEFAULT_READ_BUFFER_SIZE = 65536
INPUT_BUFFER_SIZE = 4096
# Error codes
IO_ERROR_ERRNO = 5
class FenrirScreen(pyte.Screen):
def set_margins(self, *args, **kwargs):
@ -54,39 +32,16 @@ class FenrirScreen(pyte.Screen):
class Terminal:
def __init__(self, columns, lines, p_in, env=None):
def __init__(self, columns, lines, p_in):
self.text = ""
self.attributes = None
self.screen = FenrirScreen(columns, lines)
self.env = env # Environment for proper logging
# Pre-create default attribute template to avoid repeated allocation
self._default_attribute = [
"default", "default", False, False, False, False, False, False,
"default", "default"
]
self.screen.write_process_input = lambda data: p_in.write(
data.encode()
)
self.stream = pyte.ByteStream()
self.stream.attach(self.screen)
def _log_error(self, message, level=None):
"""Log error message using proper debug manager if available."""
if self.env and "runtime" in self.env and "DebugManager" in self.env["runtime"]:
try:
log_level = level if level else debug.DebugLevel.ERROR
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY Terminal: {message}",
log_level
)
return
except Exception:
pass # Fallback to print if debug manager fails
# Fallback logging when debug manager unavailable
print(f"PTY Terminal: {message}")
def feed(self, data):
self.stream.feed(data)
@ -97,58 +52,45 @@ class Terminal:
lines = self.screen.dirty
else:
lines = range(self.screen.lines)
try:
self.attributes = [
[
list(attribute[1:]) + [False, "default", "default"]
if len(attribute) > 1 else [False, "default", "default"]
for attribute in line.values()
]
for line in buffer.values()
]
except Exception as e:
self._log_error(f"Error initializing attributes: {e}")
# Fallback to empty attributes
self.attributes = [[] for _ in range(self.screen.lines)]
for y in lines:
# Validate y is within reasonable bounds (prevent memory exhaustion)
if y >= PTYConstants.MAX_TERMINAL_LINES:
self._log_error(
f"Line index {y} exceeds maximum {PTYConstants.MAX_TERMINAL_LINES}, "
f"skipping attribute update",
debug.DebugLevel.WARNING
try:
t = self.attributes[y]
except Exception as e:
# Terminal class doesn't have access to env, use fallback
# logging
print(
f"ptyDriver Terminal update_attributes: Error accessing "
f"attributes: {e}"
)
continue
# Check if line y exists in buffer before accessing it
if y not in buffer:
# Only log this occasionally to prevent spam
if y % 10 == 0: # Log every 10th missing line
# Pre-format string to avoid repeated f-string operations
line_range = f"{y}-{y+9}"
self._log_error(
f"Lines {line_range} not found in buffer, skipping attribute updates",
debug.DebugLevel.WARNING
)
continue
# Ensure attributes array is large enough for line y
while len(self.attributes) <= y:
self.attributes.append([])
try:
self.attributes[y] = [
list(attribute[1:]) + [False, "default", "default"]
for attribute in (buffer[y].values())
]
except Exception as e:
self._log_error(f"Error updating attributes for line {y}: {e}")
# Initialize with empty attributes if update fails
self.attributes[y] = []
if len(self.attributes[y]) < self.screen.columns:
diff = self.screen.columns - len(self.attributes[y])
# Use pre-created template for efficiency
self.attributes[y] += [self._default_attribute[:] for _ in range(diff)]
self.attributes[y] += [
[
"default",
"default",
False,
False,
False,
False,
False,
False,
"default",
"default",
]
] * diff
def resize(self, lines, columns):
self.screen.resize(lines, columns)
@ -156,37 +98,31 @@ class Terminal:
self.update_attributes(True)
def set_cursor(self, x=-1, y=-1):
# Determine target cursor position
x_pos = x if x != -1 else self.screen.cursor.x
y_pos = y if y != -1 else self.screen.cursor.y
# Validate and clamp cursor position to screen bounds
max_x = max(0, self.screen.columns - 1)
max_y = max(0, self.screen.lines - 1)
self.screen.cursor.x = max(0, min(x_pos, max_x))
self.screen.cursor.y = max(0, min(y_pos, max_y))
x_pos = x
y_pos = y
if x_pos == -1:
x_pos = self.screen.cursor.x
if y_pos == -1:
y_pos = self.screen.cursor.y
self.screen.cursor.x = min(
self.screen.cursor.x, self.screen.columns - 1
)
self.screen.cursor.y = min(self.screen.cursor.y, self.screen.lines - 1)
def get_screen_content(self):
cursor = self.screen.cursor
# Only regenerate text if screen is dirty or text doesn't exist
if not hasattr(self, 'text') or self.screen.dirty:
self.text = "\n".join(self.screen.display)
self.update_attributes(self.attributes is None)
self.screen.dirty.clear()
# Return screen content without unnecessary copying
# Only copy attributes if they exist and need protection
screen_data = {
return {
"cursor": (cursor.x, cursor.y),
"lines": self.screen.lines,
"columns": self.screen.columns,
"text": self.text,
"attributes": self.attributes[:] if self.attributes else [], # Shallow copy only if needed
"attributes": self.attributes.copy(),
"screen": "pty",
"screenUpdateTime": time.time(),
}
return screen_data
}.copy()
class driver(screenDriver):
@ -196,64 +132,13 @@ class driver(screenDriver):
self.p_out = None
self.terminal = None
self.p_pid = -1
self.terminal_lock = threading.Lock() # Synchronize terminal operations
signal.signal(signal.SIGWINCH, self.handle_sigwinch)
# Runtime configuration storage
self.pty_config = {}
def _load_pty_settings(self):
"""Load PTY-specific settings from configuration with fallbacks to defaults."""
try:
settings_manager = self.env["runtime"]["SettingsManager"]
# Load timeout settings with defaults
self.pty_config = {
'input_timeout': float(settings_manager.get_setting(
'screen', 'ptyInputTimeout', PTYConstants.INPUT_READ_TIMEOUT
)),
'output_timeout': float(settings_manager.get_setting(
'screen', 'ptyOutputTimeout', PTYConstants.OUTPUT_READ_TIMEOUT
)),
'select_timeout': float(settings_manager.get_setting(
'screen', 'ptySelectTimeout', PTYConstants.SELECT_TIMEOUT
)),
'process_termination_timeout': float(settings_manager.get_setting(
'screen', 'ptyProcessTimeout', PTYConstants.PROCESS_TERMINATION_TIMEOUT
)),
'poll_interval': float(settings_manager.get_setting(
'screen', 'ptyPollInterval', PTYConstants.MIN_POLL_INTERVAL
))
}
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Loaded configuration: {self.pty_config}",
debug.DebugLevel.INFO
)
except Exception as e:
# Fallback to constants if settings fail
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Failed to load settings, using defaults: {e}",
debug.DebugLevel.WARNING
)
self.pty_config = {
'input_timeout': PTYConstants.INPUT_READ_TIMEOUT,
'output_timeout': PTYConstants.OUTPUT_READ_TIMEOUT,
'select_timeout': PTYConstants.SELECT_TIMEOUT,
'process_termination_timeout': PTYConstants.PROCESS_TERMINATION_TIMEOUT,
'poll_interval': PTYConstants.MIN_POLL_INTERVAL
}
def initialize(self, environment):
self.env = environment
self.command = self.env["runtime"]["SettingsManager"].get_setting(
"general", "shell"
)
# Load configurable timeouts from settings
self._load_pty_settings()
self.shortcutType = self.env["runtime"][
"InputManager"
].get_shortcut_type()
@ -277,57 +162,29 @@ class driver(screenDriver):
self.env["general"]["prev_user"] = getpass.getuser()
self.env["general"]["curr_user"] = getpass.getuser()
def read_all(self, fd, timeout=PTYConstants.DEFAULT_READ_TIMEOUT, interruptFd=None, len=PTYConstants.DEFAULT_READ_BUFFER_SIZE):
"""Read all available data from file descriptor with efficient polling.
Uses progressively longer wait times to balance responsiveness with CPU usage.
"""
def read_all(self, fd, timeout=0.3, interruptFd=None, len=65536):
msg_bytes = b""
fd_list = [fd]
fd_list = []
fd_list += [fd]
if interruptFd:
fd_list.append(interruptFd)
fd_list += [interruptFd]
starttime = time.time()
poll_timeout = self.pty_config.get('poll_interval', PTYConstants.MIN_POLL_INTERVAL) # Use configured interval
while True:
# Use consistent short polling for responsiveness
r = screen_utils.has_more_what(fd_list, poll_timeout)
# Nothing more to read
r = screen_utils.has_more_what(fd_list, 0.0001)
# nothing more to read
if fd not in r:
# Check overall timeout
if (time.time() - starttime) >= timeout:
break
continue
try:
data = os.read(fd, len)
if data == b"":
raise EOFError
msg_bytes += data
except OSError as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver read_all: OS error reading from fd {fd}: {e}",
debug.DebugLevel.ERROR
)
# For I/O errors, exit immediately to prevent endless retry loops
if e.errno == PTYConstants.IO_ERROR_ERRNO: # Input/output error
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY screenDriver: Terminal connection lost, stopping read loop",
debug.DebugLevel.ERROR
)
raise EOFError("Terminal connection lost")
# exit on interrupt available
if interruptFd in r:
break
# Exit on interrupt available
if interruptFd and interruptFd in r:
break
# Check overall timeout
# respect timeout but wait a little bit of time to see if something
# more is here
if (time.time() - starttime) >= timeout:
break
return msg_bytes
def open_terminal(self, columns, lines, command):
@ -340,16 +197,16 @@ class driver(screenDriver):
if env["TERM"] == "":
env["TERM"] = "linux"
except Exception as e:
# Child process doesn't have access to debug manager
# Use fallback logging with more context
# Child process doesn't have access to env, use fallback
# logging
print(
f"ptyDriver open_terminal (child): TERM environment error: {e}"
f"ptyDriver spawnTerminal: Error checking TERM environment: {e}"
)
env["TERM"] = "linux"
os.execvpe(argv[0], argv, env)
# File-like object for I/O with the child process aka command.
p_out = os.fdopen(master_fd, "w+b", 0)
return Terminal(columns, lines, p_out, self.env), p_pid, p_out
return Terminal(columns, lines, p_out), p_pid, p_out
def resize_terminal(self, fd):
s = struct.pack("HHHH", 0, 0, 0, 0)
@ -382,7 +239,7 @@ class driver(screenDriver):
self.terminal.resize(lines, columns)
fd_list = [sys.stdin, self.p_out, self.signalPipe[0]]
while active.value:
r, _, _ = select(fd_list, [], [], self.pty_config.get('select_timeout', PTYConstants.SELECT_TIMEOUT)) # Configurable timeout
r, _, _ = select(fd_list, [], [], 1)
# none
if r == []:
continue
@ -394,7 +251,7 @@ class driver(screenDriver):
# input
if sys.stdin in r:
try:
msg_bytes = self.read_all(sys.stdin.fileno(), timeout=self.pty_config.get('input_timeout', PTYConstants.INPUT_READ_TIMEOUT), len=PTYConstants.INPUT_BUFFER_SIZE)
msg_bytes = self.read_all(sys.stdin.fileno(), len=4096)
except (EOFError, OSError):
event_queue.put(
{
@ -432,9 +289,7 @@ class driver(screenDriver):
if self.p_out in r:
try:
msg_bytes = self.read_all(
self.p_out.fileno(),
timeout=self.pty_config.get('output_timeout', PTYConstants.OUTPUT_READ_TIMEOUT),
interruptFd=sys.stdin.fileno()
self.p_out.fileno(), interruptFd=sys.stdin.fileno()
)
except (EOFError, OSError):
event_queue.put(
@ -444,121 +299,34 @@ class driver(screenDriver):
}
)
break
# Synchronize terminal operations to prevent race conditions
with self.terminal_lock:
# Feed data to terminal and get consistent screen state
# feed and send event bevore write, the pyte already has the right state
# so fenrir already can progress bevore os.write what
# should give some better reaction time
self.terminal.feed(msg_bytes)
screen_content = self.terminal.get_screen_content()
# Send screen update event with consistent state
event_queue.put(
{
"Type": FenrirEventType.screen_update,
"data": screen_utils.create_screen_event_data(
screen_content
self.terminal.get_screen_content()
),
}
)
# Inject to actual screen (outside lock to avoid blocking)
self.inject_text_to_screen(
msg_bytes, screen=sys.stdout.fileno()
)
except Exception as e: # Process died?
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver terminal_emulation: Exception occurred: {e}",
debug.DebugLevel.ERROR
)
print(e)
event_queue.put(
{"Type": FenrirEventType.stop_main_loop, "data": None}
)
finally:
self._safe_cleanup_process()
self._safe_cleanup_resources(old_attr)
event_queue.put(
{"Type": FenrirEventType.stop_main_loop, "data": None}
)
def _safe_cleanup_process(self):
"""Safely terminate the child process with timeout and fallback to SIGKILL."""
if not hasattr(self, 'p_pid') or self.p_pid is None:
return
try:
# Check if process is still alive
os.kill(self.p_pid, 0) # Signal 0 checks if process exists
except OSError:
# Process already dead
self.p_pid = None
return
try:
# Try graceful termination first
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Terminating process {self.p_pid} gracefully",
debug.DebugLevel.INFO
)
os.kill(self.p_pid, signal.SIGTERM)
# Wait for graceful termination
timeout = self.pty_config.get('process_termination_timeout', PTYConstants.PROCESS_TERMINATION_TIMEOUT)
start_time = time.time()
while time.time() - start_time < timeout:
try:
os.kill(self.p_pid, 0) # Check if still alive
time.sleep(0.1)
except OSError:
# Process terminated gracefully
self.p_pid = None
return
# Process didn't terminate gracefully, use SIGKILL
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Process {self.p_pid} didn't terminate gracefully, using SIGKILL",
debug.DebugLevel.WARNING
)
os.kill(self.p_pid, signal.SIGKILL)
time.sleep(PTYConstants.PROCESS_KILL_DELAY) # Give it a moment
except OSError as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Error terminating process {self.p_pid}: {e}",
debug.DebugLevel.ERROR
)
finally:
self.p_pid = None
def _safe_cleanup_resources(self, old_attr=None):
"""Safely clean up file descriptors and terminal attributes."""
# Close output pipe safely
if hasattr(self, 'p_out') and self.p_out is not None:
try:
self.p_out.close()
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY screenDriver: Closed output pipe",
debug.DebugLevel.INFO
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Error closing output pipe: {e}",
debug.DebugLevel.ERROR
)
finally:
self.p_out = None
# Restore terminal attributes safely
if old_attr is not None:
try:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_attr)
self.env["runtime"]["DebugManager"].write_debug_out(
"PTY screenDriver: Restored terminal attributes",
debug.DebugLevel.INFO
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
f"PTY screenDriver: Error restoring terminal attributes: {e}",
debug.DebugLevel.ERROR
event_queue.put(
{"Type": FenrirEventType.stop_main_loop, "data": None}
)
sys.exit(0)
def get_curr_application(self):
pass

View File

@ -170,9 +170,8 @@ class driver(screenDriver):
if screen is not None:
use_screen = screen
with open(use_screen, "w") as fd:
text_bytes = text.encode('utf-8')
for byte in text_bytes:
fcntl.ioctl(fd, termios.TIOCSTI, bytes([byte]))
for c in text:
fcntl.ioctl(fd, termios.TIOCSTI, c)
def get_session_information(self):
"""Retrieve session information via D-Bus logind interface.