Updated some documentation.

This commit is contained in:
Storm Dragon committed 2026-10-28 00:45:23 -04:00
1 parent 0e50de4c76
commit 4f5a875d0e
17 files changed
+2416 -6207

No files matched your search

-159
View File
@@ -1,159 +0,0 @@
## Development Philosophy
**Clean code over backward compatibility.** Breaking changes are acceptable in major versions.
- PEP8 compliance (snake_case for all variables/functions/settings)
- Remove deprecated code completely - no compatibility hacks
- Trust users to update configs when upgrading
**Exception**: Never break core accessibility (speech output, basic navigation).
## Translation Maintenance
- Make all new or changed user-facing messages translatable, including speech
announcements, help, command descriptions, menus, and errors. Import `_` and
`ngettext` as needed from `fenrirscreenreader.core.i18n`.
- Translate complete literal messages before formatting values, for example
`_("Loading voices for {module}").format(module=module)`. Avoid translated
sentence fragments, f-strings inside `_()`, and formatting before translation.
Use `ngettext()` for count-dependent messages and preserve placeholders.
- Keep internal identifiers, configuration keys, filenames, application content,
and clipboard data unchanged.
- When messages change, regenerate `tools/fenrir.pot` with
`./tools/generate_translations.sh`, then run
`./tools/generate_translations.sh check`.
- Preserve existing translations. Update specific `.po` catalogs only when
requested; leave untranslated and fuzzy entries for translator review.
- Review the diff for user-facing strings missing translation markers; the
catalog check cannot detect every unmarked string.
- For localization behavior changes, run
`pytest tests/unit/test_translations.py` and verify the affected workflow.
Consult the README's Localization section for the maintenance workflow.
## Core Architecture
### Drivers
- **Screen**: `vcsaDriver` (TTY), `ptyDriver` (terminal emulation)
- **Input**: `evdevDriver` (evdev), `x11Driver` (X11 terminal mode)
- **Speech**: `speechdDriver` (speech-dispatcher), `genericDriver` (subprocess)
- **Sound**: `gstreamerDriver` (GStreamer), `genericDriver` (Sox subprocess)
- **Remote**: `unixDriver` (Unix sockets), `tcpDriver`
### Command System
Commands in trigger folders execute automatically:
- `onKeyInput/`, `onCursorChange/`, `onScreenUpdate/`, `onScreenChanged/`, `onHeartBeat/`
- Numeric prefixes control priority (lower = higher priority): `15000-char_echo.py`
- Manual commands in `commands/commands/`
### Core Managers (in `src/fenrirscreenreader/core/`)
`fenrirManager.py`, `commandManager.py`, `eventManager.py`, `screenManager.py`, `inputManager.py`, `outputManager.py`
### Configuration
- Main: `config/settings/settings.conf`
- Keyboards: `config/keyboard/` (desktop.conf, laptop.conf)
- Sounds: `config/sound/`
- Punctuation: `config/punctuation/`
## 🚨 CRITICAL: Files to NEVER Modify
**These are timing-critical - only fix critical bugs, never refactor:**
- `evdevDriver.py` - Input event processing
- `vcsaDriver.py` - Screen monitoring
Symptoms of breakage: echo changes, input lag, missing screen updates, sync issues.
## Development Commands
```bash
# Run from source (requires root)
cd src/ && sudo ./fenrir -f -d # foreground with debug
# Validation before commit
python3 tools/validate_syntax.py --fix
python3 tools/validate_release.py --quick
# Tests
pytest tests/
pytest tests/ --cov=src/fenrirscreenreader --cov-report=html
```
**Note**: Standard TTY Fenrir blocks - only one instance at a time. `fenrir -x`
can run multiple foreground X terminal instances. Ask user to run tests that
need real Fenrir instances or root access.
### Packaging Test Layouts
- Debian `pybuild` runs copied tests from a nested `.pybuild/.../build` tree
and may omit top-level source directories such as `config/`.
- Tests that inspect source resources must locate them by walking upward from
the resolved test path; do not assume a fixed `Path(__file__).parents[N]`.
- Packaging-sensitive test changes should be checked from both the source tree
and a representative nested build directory under `/tmp`.
## Creating Commands
Use `commands/command_template.py` as base. Required methods:
- `initialize(self, environment)`, `shutdown()`, `run()`, `get_description()`
### Import Pattern (Required)
Fenrir's command loader doesn't support relative imports:
```python
import os, importlib.util
_base_path = os.path.join(os.path.dirname(__file__), 'base_file.py')
_spec = importlib.util.spec_from_file_location("base_module", _base_path)
_module = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_module)
BaseClass = _module.BaseClass
```
### Consolidated Base Classes
- **Bookmarks**: `bookmark_base.py` - `BookmarkCommand(id, 'read'|'set'|'clear')`
- **Adjustments**: `adjustment_base.py` - `AdjustmentCommand('speech'|'sound', 'rate'|'pitch'|'volume', 'inc'|'dec')`
- **VMenu Search**: `vmenu_search_base.py` - `VMenuSearchCommand('a'-'z')`
## VMenu System
Application-specific menus in `vmenu-profiles/KEY/{app}/`:
- Directories = submenus, `.py` files = actions
- Activate: `Fenrir+F10`, navigate with arrows, Enter to execute
## Remote Control
Unix socket: `/tmp/fenrirscreenreader-daemon.sock`
TCP: localhost:22447
```bash
echo "command say Hello" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#rate=0.8" | nc localhost 22447
```
In `fenrir -x`, each instance has a private socket:
`/tmp/fenrirscreenreader-<pid>.sock`. One instance may also own the standard
socket. Use `ls`/`list` on the standard socket to discover registered instances
and their socket paths. Untargeted remote commands use a short cross-process
claim lock so only one instance handles the same broadcast command; direct
commands to a private instance socket still run on that instance.
Disable in untrusted environments: `[remote] enable=False`
## Key Implementation Details
- Commands via `command_mod.command()` instantiation
- Bookmark structure: `env['commandBuffer']['bookMarks'][ID][app]['1'/'2']`
- VMenu search: `env['runtime']['vmenuManager'].searchEntry(char)`
## Recent Fixes (Reference)
- Enhanced application detection for screen/tmux sessions
- Table navigation consistency (sound feedback)
- Blank line detection in 22 command files
- Character/space review during navigation
## Safe vs Risky Changes
**Safe**: Commands, VMenu profiles, config handling, sound/speech drivers, docs
**Risky (extra testing)**: Core managers, event processing
**Avoid**: evdevDriver, vcsaDriver, timing-critical code
+4 -20
View File
@@ -6,25 +6,9 @@
* **Storm Dragon** - Project leader and maintainer * **Storm Dragon** - Project leader and maintainer
## Current Contributors
* **Jeremiah** - Developer
## Previous Developers
* **Chrys** - Original creator and main developer
## Special Thanks ## Special Thanks
* **F123 Consulting** - Suggestions, funding, and extensive testing Special thanks to **Chrys**, without whom this project would not exist in the
* **Stormux Community** - Continuation of the project and ongoing support first place. He wrote Fenrir from scratch when I desperately needed a console
* **All contributors** - Bug reports, feature requests, and community support screen reader. His hard work and dedication to accessibility are always
appreciated.
## Community
* IRC: irc.stormux.org #stormux
* Email list: stormux+subscribe@groups.io
* Wiki: https://git.stormux.org/storm/fenrir/wiki
+194 -826
View File
File diff suppressed because it is too large. Load diff
+74 -579
View File
@@ -1,588 +1,83 @@
# Fenrir keyboard layouts
# Fenrir Keyboard Configuration Both Linux console and X11 terminal modes use the TOML layouts in this directory:
This directory contains keyboard layout files for Fenrir screen reader. - [desktop.toml](desktop.toml): numeric keypad review, with NumLock off.
- [laptop.toml](laptop.toml): review chords on the main keyboard.
## Available Layouts The Fenrir key is Insert, Super/Windows, or keypad 0 with NumLock off.
- **desktop.conf** - Desktop layout using numeric keypad (recommended) ## Selecting and customizing a layout
- **laptop.conf** - Laptop layout for keyboards without numeric keypad
## Key Features Select a layout at launch:
### Table Navigation ```bash
- **Toggle table mode**: `Fenrir + Keypad *` (desktop) or `Fenrir + Y` (laptop) ./fenrir --keyboard-layout laptop
- **Column navigation**: `Keypad 4/6` (desktop) or `Fenrir + J/L` (laptop) ./fenrir -x --keyboard-layout /path/to/custom.toml
- **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)
### Speech History
- **Open speech history**: `Fenrir + Ctrl + H`
- **Navigate history**: `Up` and `Down`
- **Copy current item**: `Enter`
- **Exit speech history**: `Escape`
## Configuration
To change keyboard layout, edit `/etc/fenrirscreenreader/settings/settings.conf`
or your user-local settings file at
`~/.local/share/stormux/fenrirscreenreader/settings/settings.conf`:
```ini
[keyboard]
keyboard_layout=desktop # or laptop
``` ```
## Available Key Constants Or set the startup choice in `settings.toml`:
Keymap for Fenrir ```toml
KEY_RESERVED [keyboard]
KEY_ESC layout = "laptop"
KEY_1 ```
KEY_2
KEY_3 For a named layout, Fenrir searches the regular user's
KEY_4 `~/.local/share/stormux/fenrirscreenreader/keyboard/`, then
KEY_5 `/etc/fenrirscreenreader/keyboard/`, then `config/keyboard/` beside the binary.
KEY_6 Root searches the system and bundled locations. The first existing layout is
KEY_7 loaded as a complete file; layouts are not merged. Copy a bundled layout as the
KEY_8 starting point for customization. Layout changes apply on the next launch.
KEY_9
KEY_0 Each binding has a label, a key chord, and a command ID:
KEY_MINUS
KEY_EQUAL ```toml
KEY_BACKSPACE version = 1
KEY_TAB
KEY_Q [[bindings]]
KEY_W name = "Current word"
KEY_E keys = ["FENRIR", "K"]
KEY_R command = "current_word"
KEY_T double_command = "spell_word"
KEY_Y triple_command = "phonetic_word"
KEY_U ```
KEY_I
KEY_O This is a minimal example, not a complete replacement layout. Optional double
KEY_P and triple commands run on successive quick presses of the same chord; a
KEY_LEFTBRACE triple command requires a double command. The default multi-press interval is
KEY_RIGHTBRACE 200 ms, configured by `keyboard.multi_press_timeout_ms`.
KEY_ENTER
KEY_LEFTCTRL Put modifiers (`FENRIR`, `CTRL`, `SHIFT`, `ALT`) before the final key.
KEY_A Use key names from the bundled layouts, such as `SPACE`, `RIGHT`, `KP8`, and
KEY_S `KPASTERISK`. Chords outside the bare review keypad keys require `FENRIR`.
KEY_D Unsupported keys, unknown commands, and duplicate chords are rejected.
KEY_F
KEY_G Validate a custom layout without opening devices:
KEY_H
KEY_J ```bash
KEY_K ./fenrir --keyboard-layout /path/to/custom.toml --check-config
KEY_L ```
KEY_SEMICOLON
KEY_APOSTROPHE ## Common shortcuts
KEY_GRAVE
KEY_LEFTSHIFT | Action | Desktop | Laptop |
KEY_BACKSLASH | --- | --- | --- |
KEY_Z | Previous / current / next line | Keypad 7 / 8 / 9 | Fenrir+U / I / O |
KEY_X | Previous / current / next word | Keypad 4 / 5 / 6 | Fenrir+J / K / L |
KEY_C | Previous / current / next character | Keypad 1 / 2 / 3 | Fenrir+M / Comma / Period |
KEY_V | Read screen | Fenrir+Keypad 5 | Fenrir+Shift+K |
KEY_B | Follow cursor | Fenrir+Keypad Period | Fenrir+Slash |
KEY_N | Cycle cursor / table / highlight tracking | Fenrir+Keypad Asterisk | Fenrir+Y |
KEY_M | Toggle progress tones | Keypad Plus | Fenrir+Shift+P |
KEY_COMMA | Set reading window; double press to clear | Fenrir+Keypad Slash | Fenrir+Control+8 |
KEY_DOT
KEY_SLASH Both layouts use **Fenrir+F** for settings, **Fenrir+H** or **Fenrir+F1** for
KEY_RIGHTSHIFT the tutorial, and **Fenrir+Control+S** to save settings. The tutorial describes
KEY_KPASTERISK loaded shortcuts and lets you browse unbound commands.
KEY_LEFTALT
KEY_SPACE In table mode, word navigation moves between cells. **Fenrir+X** selects the
KEY_CAPSLOCK current row as headers. Outside table mode it places a text-selection mark.
KEY_F1
KEY_F2 See the [user manual](../../docs/user.md) for clipboard, reading-region,
KEY_F3 spellcheck, and menu workflows.
KEY_F4
KEY_F5
KEY_F6
KEY_F7
KEY_F8
KEY_F9
KEY_F10
KEY_NUMLOCK
KEY_SCROLLLOCK
KEY_KP7
KEY_KP8
KEY_KP9
KEY_KPMINUS
KEY_KP4
KEY_KP5
KEY_KP6
KEY_KPPLUS
KEY_KP1
KEY_KP2
KEY_KP3
KEY_KP0
KEY_KPDOT
KEY_ZENKAKUHANKAKU
KEY_102ND
KEY_F11
KEY_F12
KEY_RO
KEY_KATAKANA
KEY_HIRAGANA
KEY_HENKAN
KEY_KATAKANAHIRAGANA
KEY_MUHENKAN
KEY_KPJPCOMMA
KEY_KPENTER
KEY_RIGHTCTRL
KEY_KPSLASH
KEY_SYSRQ
KEY_RIGHTALT
KEY_LINEFEED
KEY_HOME
KEY_UP
KEY_PAGEUP
KEY_LEFT
KEY_RIGHT
KEY_END
KEY_DOWN
KEY_PAGEDOWN
KEY_INSERT
KEY_DELETE
KEY_MACRO
['KEY_MIN_INTERESTING', 'KEY_MUTE']
KEY_VOLUMEDOWN
KEY_VOLUMEUP
KEY_POWER
KEY_KPEQUAL
KEY_KPPLUSMINUS
KEY_PAUSE
KEY_SCALE
KEY_KPCOMMA
['KEY_HANGEUL', 'KEY_HANGUEL']
KEY_HANJA
KEY_YEN
KEY_LEFTMETA
KEY_RIGHTMETA
KEY_COMPOSE
KEY_STOP
KEY_AGAIN
KEY_PROPS
KEY_UNDO
KEY_FRONT
KEY_COPY
KEY_OPEN
KEY_PASTE
KEY_FIND
KEY_CUT
KEY_HELP
KEY_MENU
KEY_CALC
KEY_SETUP
KEY_SLEEP
KEY_WAKEUP
KEY_FILE
KEY_SENDFILE
KEY_DELETEFILE
KEY_XFER
KEY_PROG1
KEY_PROG2
KEY_WWW
KEY_MSDOS
['KEY_COFFEE', 'KEY_SCREENLOCK']
['KEY_DIRECTION', 'KEY_ROTATE_DISPLAY']
KEY_CYCLEWINDOWS
KEY_MAIL
KEY_BOOKMARKS
KEY_COMPUTER
KEY_BACK
KEY_FORWARD
KEY_CLOSECD
KEY_EJECTCD
KEY_EJECTCLOSECD
KEY_NEXTSONG
KEY_PLAYPAUSE
KEY_PREVIOUSSONG
KEY_STOPCD
KEY_RECORD
KEY_REWIND
KEY_PHONE
KEY_ISO
KEY_CONFIG
KEY_HOMEPAGE
KEY_REFRESH
KEY_EXIT
KEY_MOVE
KEY_EDIT
KEY_SCROLLUP
KEY_SCROLLDOWN
KEY_KPLEFTPAREN
KEY_KPRIGHTPAREN
KEY_NEW
KEY_REDO
KEY_F13
KEY_F14
KEY_F15
KEY_F16
KEY_F17
KEY_F18
KEY_F19
KEY_F20
KEY_F21
KEY_F22
KEY_F23
KEY_F24
KEY_PLAYCD
KEY_PAUSECD
KEY_PROG3
KEY_PROG4
KEY_DASHBOARD
KEY_SUSPEND
KEY_CLOSE
KEY_PLAY
KEY_FASTFORWARD
KEY_BASSBOOST
KEY_PRINT
KEY_HP
KEY_CAMERA
KEY_SOUND
KEY_QUESTION
KEY_EMAIL
KEY_CHAT
KEY_SEARCH
KEY_CONNECT
KEY_FINANCE
KEY_SPORT
KEY_SHOP
KEY_ALTERASE
KEY_CANCEL
KEY_BRIGHTNESSDOWN
KEY_BRIGHTNESSUP
KEY_MEDIA
KEY_SWITCHVIDEOMODE
KEY_KBDILLUMTOGGLE
KEY_KBDILLUMDOWN
KEY_KBDILLUMUP
KEY_SEND
KEY_REPLY
KEY_FORWARDMAIL
KEY_SAVE
KEY_DOCUMENTS
KEY_BATTERY
KEY_BLUETOOTH
KEY_WLAN
KEY_UWB
KEY_UNKNOWN
KEY_VIDEO_NEXT
KEY_VIDEO_PREV
KEY_BRIGHTNESS_CYCLE
['KEY_BRIGHTNESS_AUTO', 'KEY_BRIGHTNESS_ZERO']
KEY_DISPLAY_OFF
['KEY_WIMAX', 'KEY_WWAN']
KEY_RFKILL
KEY_MICMUTE
['BTN_0', 'BTN_MISC']
BTN_1
BTN_2
BTN_3
BTN_4
BTN_5
BTN_6
BTN_7
BTN_8
BTN_9
['BTN_LEFT', 'BTN_MOUSE']
BTN_RIGHT
BTN_MIDDLE
BTN_SIDE
BTN_EXTRA
BTN_FORWARD
BTN_BACK
BTN_TASK
['BTN_JOYSTICK', 'BTN_TRIGGER']
BTN_THUMB
BTN_THUMB2
BTN_TOP
BTN_TOP2
BTN_PINKIE
BTN_BASE
BTN_BASE2
BTN_BASE3
BTN_BASE4
BTN_BASE5
BTN_BASE6
BTN_DEAD
['BTN_A', 'BTN_GAMEPAD', 'BTN_SOUTH']
['BTN_B', 'BTN_EAST']
BTN_C
['BTN_NORTH', 'BTN_X']
['BTN_WEST', 'BTN_Y']
BTN_Z
BTN_TL
BTN_TR
BTN_TL2
BTN_TR2
BTN_SELECT
BTN_START
BTN_MODE
BTN_THUMBL
BTN_THUMBR
['BTN_DIGI', 'BTN_TOOL_PEN']
BTN_TOOL_RUBBER
BTN_TOOL_BRUSH
BTN_TOOL_PENCIL
BTN_TOOL_AIRBRUSH
BTN_TOOL_FINGER
BTN_TOOL_MOUSE
BTN_TOOL_LENS
BTN_TOOL_QUINTTAP
BTN_TOUCH
BTN_STYLUS
BTN_STYLUS2
BTN_TOOL_DOUBLETAP
BTN_TOOL_TRIPLETAP
BTN_TOOL_QUADTAP
['BTN_GEAR_DOWN', 'BTN_WHEEL']
BTN_GEAR_UP
KEY_OK
KEY_SELECT
KEY_GOTO
KEY_CLEAR
KEY_POWER2
KEY_OPTION
KEY_INFO
KEY_TIME
KEY_VENDOR
KEY_ARCHIVE
KEY_PROGRAM
KEY_CHANNEL
KEY_FAVORITES
KEY_EPG
KEY_PVR
KEY_MHP
KEY_LANGUAGE
KEY_TITLE
KEY_SUBTITLE
KEY_ANGLE
KEY_ZOOM
KEY_MODE
KEY_KEYBOARD
KEY_SCREEN
KEY_PC
KEY_TV
KEY_TV2
KEY_VCR
KEY_VCR2
KEY_SAT
KEY_SAT2
KEY_CD
KEY_TAPE
KEY_RADIO
KEY_TUNER
KEY_PLAYER
KEY_TEXT
KEY_DVD
KEY_AUX
KEY_MP3
KEY_AUDIO
KEY_VIDEO
KEY_DIRECTORY
KEY_LIST
KEY_MEMO
KEY_CALENDAR
KEY_RED
KEY_GREEN
KEY_YELLOW
KEY_BLUE
KEY_CHANNELUP
KEY_CHANNELDOWN
KEY_FIRST
KEY_LAST
KEY_AB
KEY_NEXT
KEY_RESTART
KEY_SLOW
KEY_SHUFFLE
KEY_BREAK
KEY_PREVIOUS
KEY_DIGITS
KEY_TEEN
KEY_TWEN
KEY_VIDEOPHONE
KEY_GAMES
KEY_ZOOMIN
KEY_ZOOMOUT
KEY_ZOOMRESET
KEY_WORDPROCESSOR
KEY_EDITOR
KEY_SPREADSHEET
KEY_GRAPHICSEDITOR
KEY_PRESENTATION
KEY_DATABASE
KEY_NEWS
KEY_VOICEMAIL
KEY_ADDRESSBOOK
KEY_MESSENGER
['KEY_BRIGHTNESS_TOGGLE', 'KEY_DISPLAYTOGGLE']
KEY_SPELLCHECK
KEY_LOGOFF
KEY_DOLLAR
KEY_EURO
KEY_FRAMEBACK
KEY_FRAMEFORWARD
KEY_CONTEXT_MENU
KEY_MEDIA_REPEAT
KEY_10CHANNELSUP
KEY_10CHANNELSDOWN
KEY_IMAGES
KEY_DEL_EOL
KEY_DEL_EOS
KEY_INS_LINE
KEY_DEL_LINE
KEY_FN
KEY_FN_ESC
KEY_FN_F1
KEY_FN_F2
KEY_FN_F3
KEY_FN_F4
KEY_FN_F5
KEY_FN_F6
KEY_FN_F7
KEY_FN_F8
KEY_FN_F9
KEY_FN_F10
KEY_FN_F11
KEY_FN_F12
KEY_FN_1
KEY_FN_2
KEY_FN_D
KEY_FN_E
KEY_FN_F
KEY_FN_S
KEY_FN_B
KEY_BRL_DOT1
KEY_BRL_DOT2
KEY_BRL_DOT3
KEY_BRL_DOT4
KEY_BRL_DOT5
KEY_BRL_DOT6
KEY_BRL_DOT7
KEY_BRL_DOT8
KEY_BRL_DOT9
KEY_BRL_DOT10
KEY_NUMERIC_0
KEY_NUMERIC_1
KEY_NUMERIC_2
KEY_NUMERIC_3
KEY_NUMERIC_4
KEY_NUMERIC_5
KEY_NUMERIC_6
KEY_NUMERIC_7
KEY_NUMERIC_8
KEY_NUMERIC_9
KEY_NUMERIC_STAR
KEY_NUMERIC_POUND
KEY_NUMERIC_A
KEY_NUMERIC_B
KEY_NUMERIC_C
KEY_NUMERIC_D
KEY_CAMERA_FOCUS
KEY_WPS_BUTTON
KEY_TOUCHPAD_TOGGLE
KEY_TOUCHPAD_ON
KEY_TOUCHPAD_OFF
KEY_CAMERA_ZOOMIN
KEY_CAMERA_ZOOMOUT
KEY_CAMERA_UP
KEY_CAMERA_DOWN
KEY_CAMERA_LEFT
KEY_CAMERA_RIGHT
KEY_ATTENDANT_ON
KEY_ATTENDANT_OFF
KEY_ATTENDANT_TOGGLE
KEY_LIGHTS_TOGGLE
BTN_DPAD_UP
BTN_DPAD_DOWN
BTN_DPAD_LEFT
BTN_DPAD_RIGHT
KEY_ALS_TOGGLE
KEY_BUTTONCONFIG
KEY_TASKMANAGER
KEY_JOURNAL
KEY_CONTROLPANEL
KEY_APPSELECT
KEY_SCREENSAVER
KEY_VOICECOMMAND
KEY_BRIGHTNESS_MIN
KEY_BRIGHTNESS_MAX
KEY_KBDINPUTASSIST_PREV
KEY_KBDINPUTASSIST_NEXT
KEY_KBDINPUTASSIST_PREVGROUP
KEY_KBDINPUTASSIST_NEXTGROUP
KEY_KBDINPUTASSIST_ACCEPT
KEY_KBDINPUTASSIST_CANCEL
KEY_RIGHT_UP
KEY_RIGHT_DOWN
KEY_LEFT_UP
KEY_LEFT_DOWN
KEY_ROOT_MENU
KEY_MEDIA_TOP_MENU
KEY_NUMERIC_11
KEY_NUMERIC_12
KEY_AUDIO_DESC
KEY_3D_MODE
KEY_NEXT_FAVORITE
KEY_STOP_RECORD
KEY_PAUSE_RECORD
KEY_VOD
KEY_UNMUTE
['KEY_DATA', 'KEY_FASTREVERSE']
KEY_SLOWREVERSE
['BTN_TRIGGER_HAPPY', 'BTN_TRIGGER_HAPPY1']
BTN_TRIGGER_HAPPY2
BTN_TRIGGER_HAPPY3
BTN_TRIGGER_HAPPY4
BTN_TRIGGER_HAPPY5
BTN_TRIGGER_HAPPY6
BTN_TRIGGER_HAPPY7
BTN_TRIGGER_HAPPY8
BTN_TRIGGER_HAPPY9
BTN_TRIGGER_HAPPY10
BTN_TRIGGER_HAPPY11
BTN_TRIGGER_HAPPY12
BTN_TRIGGER_HAPPY13
BTN_TRIGGER_HAPPY14
BTN_TRIGGER_HAPPY15
BTN_TRIGGER_HAPPY16
BTN_TRIGGER_HAPPY17
BTN_TRIGGER_HAPPY18
BTN_TRIGGER_HAPPY19
BTN_TRIGGER_HAPPY20
BTN_TRIGGER_HAPPY21
BTN_TRIGGER_HAPPY22
BTN_TRIGGER_HAPPY23
BTN_TRIGGER_HAPPY24
BTN_TRIGGER_HAPPY25
BTN_TRIGGER_HAPPY26
BTN_TRIGGER_HAPPY27
BTN_TRIGGER_HAPPY28
BTN_TRIGGER_HAPPY29
BTN_TRIGGER_HAPPY30
BTN_TRIGGER_HAPPY31
BTN_TRIGGER_HAPPY32
BTN_TRIGGER_HAPPY33
BTN_TRIGGER_HAPPY34
BTN_TRIGGER_HAPPY35
BTN_TRIGGER_HAPPY36
BTN_TRIGGER_HAPPY37
BTN_TRIGGER_HAPPY38
BTN_TRIGGER_HAPPY39
BTN_TRIGGER_HAPPY40
+28 -1
View File
@@ -1 +1,28 @@
https://rudd-o.com/linux-and-free-software/how-to-make-pulseaudio-run-once-at-boot-for-all-your-users Fenrir console audio
Fenrir's pulse sound driver uses PulseAudio or PipeWire's PulseAudio service.
The alsa driver opens the configured ALSA PCM device directly. Speech Dispatcher
has its own synthesis and audio configuration, separate from Fenrir's sound cues.
For an X11 terminal session, start Fenrir as the regular user with ./fenrir -x.
For console mode started through sudo, Fenrir uses SUDO_UID to find the invoking
user's PulseAudio and Speech Dispatcher sockets. An explicit PULSE_SERVER takes
precedence for sound cues; SPEECHD_ADDRESS with a unix_socket: address takes
precedence for Speech Dispatcher. The relevant services must already be running
and accessible to the reader process.
If root sound still needs separate configuration, the checkout includes
tools/configure_pulse.sh for PulseAudio and
tools/configure_pipewire.sh for PipeWire.
Read the appropriate script before running it. Each has a regular-user part
and a root part, and writes audio configuration files. The PulseAudio script
replaces the user's default.pa and configures an anonymously accessible socket
at /tmp/pulse.sock. The PipeWire script also changes WirePlumber settings.
These are system audio changes, not prerequisites for every Fenrir session.
For ALSA, select driver = "alsa" under [presentation.sound] in settings.toml
and set device to the desired PCM name, such as "default". Driver and device
changes apply at the next launch.
See README.md for build and startup instructions and docs/user.md for the Sound
settings and troubleshooting sections.
+23 -8
View File
@@ -1,13 +1,28 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# needs pandoc and php installed set -euo pipefail
# remove old files docsDir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
rm fenrir.1
rm user.md
# convert to markdown if ! command -v pandoc >/dev/null 2>&1; then
php DokuWiki-to-Markdown-Converter/convert.php user.txt echo "Pandoc is required to generate the manuals." >&2
exit 1
fi
# convert markdown to manpage tempDir="$(mktemp -d "$docsDir/.manuals.XXXXXX")"
pandoc user.md -f markdown -t man -s -o fenrir.1 trap 'rm -rf -- "$tempDir"' EXIT
# Generate every format successfully before replacing any existing output.
pandoc "$docsDir/user.md" --from markdown --to plain --wrap=auto \
| sed 's/[[:blank:]]*$//' > "$tempDir/user.txt"
pandoc "$docsDir/user.md" --from markdown --to asciidoc --wrap=auto \
--output "$tempDir/fenrir.adoc"
pandoc "$docsDir/user.md" --from markdown --to man --standalone \
--lua-filter "$docsDir/man_tables.lua" \
--metadata title=fenrir --metadata section=1 \
--metadata header="Fenrir User Manual" \
--output "$tempDir/fenrir.1"
for manualFile in user.txt fenrir.adoc fenrir.1; do
mv -- "$tempDir/$manualFile" "$docsDir/$manualFile"
done
-48
View File
@@ -1,48 +0,0 @@
# Diff Review Mode
Diff review mode provides read-only navigation for unified and classic diff files with speech-first output.
## Quick Workflow
1. Copy the full absolute path of a diff file to the Fenrir clipboard.
2. Press `Fenrir + Ctrl + D` to toggle diff mode on.
3. Review the diff using the keys below.
4. Press `Esc` to leave diff mode.
If the clipboard does not contain a full absolute file path, Fenrir announces an error and does not enter diff mode.
## Keys (Active Only In Diff Mode)
- `h` - Next hunk
- `Shift + h` - Previous hunk
- `f` - Next file section
- `Shift + f` - Previous file section
- `a` - Next added line
- `Shift + a` - Previous added line
- `d` - Next removed line
- `Shift + d` - Previous removed line
- `Up` - Previous line
- `Down` - Next line
- `Left` - Previous character
- `Right` - Next character
- `Ctrl + Left` - Previous word
- `Ctrl + Right` - Next word
- `s` - Diff summary
- `F1` - Speak key help
- `Esc` - Exit diff mode
## Speech Behavior
- Added and removed content is spoken as `Added:` and `Removed:` lines.
- Marker-only lines are normalized for speech:
- `+++` is spoken as `added`
- `---` is spoken as `removed`
- Classic diff markers are spoken in plain language:
- `17c17` -> `line 17 changed`
- `17d16` -> `line 17 deleted`
- `16a17` -> `line 17 added`
## Notes
- Diff mode is read-only and does not modify the diff file.
- Normal Fenrir key bindings are restored when diff mode exits.
+672 -564
View File
File diff suppressed because it is too large. Load diff
+410 -2246
View File
File diff suppressed because it is too large. Load diff
+352 -426
View File
@@ -1,455 +1,381 @@
# Fenrir User Manual # Fenrir User Manual
Fenrir is a modern command line screen reader written in Python 3. Fenrir is a Linux console screen reader written in Go. It reads Linux text
consoles and can run in an X11 terminal as a regular user.
It has a modular structure, a flexible driver-based architecture, is highly configurable and easy to customize and extend.
**Current maintainer:** Storm Dragon **Current maintainer:** Storm Dragon
**Previous developer:** Chrys **Original creator:** Chrys (see [CREDITS](../CREDITS))
## Quick Start For build requirements and setup, see the [README](../README.md).
1. **Installation**: See the [README.md](../README.md) for installation instructions ## Starting and stopping
2. **First run**: `sudo fenrir` (or `sudo systemctl start fenrir` if installed)
3. **Tutorial mode**: Press `Fenrir + H` to learn all commands interactively
4. **Basic navigation**: Use numeric keypad for review (desktop layout)
5. **Stop speech**: Press `Ctrl` at any time
6. **Quit**: Press `Fenrir + Q`
## Key Concepts Run these commands from the checkout root after building `fenrir`:
```bash
# Linux text console, with console and keyboard device access
sudo ./fenrir
# X11 terminal, as your regular user
./fenrir -x
# Either mode can select the laptop layout
./fenrir -x --keyboard-layout laptop
```
Fenrir runs in the foreground. Press **Fenrir+Q** to quit. Run only one reader
on a given Linux console. Separate X11 terminal instances can run at once.
X11 mode starts a shell in a new PTY. The launching terminal must supply valid
`DISPLAY` and `WINDOWID` environment variables. Keyboard interception is scoped
to that terminal window; a desktop screen reader can handle other windows.
Console mode needs access to console devices, keyboard event devices, and
`/dev/uinput`. It releases keyboard input on graphical virtual terminals and
on text consoles selected in `screen.ignored_vts`.
## Keyboard layouts and help
The **Fenrir key** is Insert, Super/Windows, or keypad 0 with NumLock off.
Hold it while pressing the other keys in a shortcut. Bare keypad review keys
also require NumLock off.
The desktop layout uses the numeric keypad. The laptop layout uses Fenrir
chords on the main keyboard. Select a layout with `--keyboard-layout desktop`
or `--keyboard-layout laptop`, or set `keyboard.layout` in the settings file.
Press **Fenrir+H** or **Fenrir+F1** to enter the tutorial. Command shortcuts are
announced and described instead of executed. Up and Down browse commands;
Left and Right switch between active actions, unbound actions, and scripts.
Space repeats the current item. Escape or Fenrir+F1 exits.
The complete layouts are [desktop.toml](../config/keyboard/desktop.toml) and
[laptop.toml](../config/keyboard/laptop.toml).
## Screen review
Review moves Fenrir's reading position without moving the application's cursor.
Return to the application cursor with the Follow cursor shortcut. By default,
application cursor movement or a screen change also leaves review.
| Action | Desktop | Laptop |
| --- | --- | --- |
| Previous / current / next line | Keypad 7 / 8 / 9 | Fenrir+U / I / O |
| Previous / current / next word | Keypad 4 / 5 / 6 | Fenrir+J / K / L |
| Previous / current / next character | Keypad 1 / 2 / 3 | Fenrir+M / Comma / Period |
| Top / bottom of screen | Fenrir+Keypad 7 / 9 | Fenrir+Shift+U / O |
| Line start / end | Fenrir+Keypad 4 / 6 | Fenrir+Shift+J / L |
| First / last text on line | Fenrir+Keypad 1 / 3 | Fenrir+Control+J / L |
| Read screen | Fenrir+Keypad 5 | Fenrir+Shift+K |
| Screen before / after cursor | Fenrir+Keypad 8 / 2 | Fenrir+Shift+I / Comma |
| Character above / below | Fenrir+Control+Keypad 8 / 2 | Fenrir+Control+I / Comma |
| Follow cursor | Fenrir+Keypad Period | Fenrir+Slash |
| Review position | Keypad Period | Fenrir+Shift+Period |
Press the current-line or current-word shortcut twice quickly to spell the
text, or three times for phonetic reading. Press the current-character shortcut
twice for its phonetic name. The default interval between presses is 200 ms;
`keyboard.multi_press_timeout_ms` changes it at launch.
## Tracking modes
Cycle through **cursor**, **table**, and **highlight** with
**Fenrir+Keypad Asterisk** on desktop or **Fenrir+Y** on laptop.
The startup selection is `reading.mode`.
- **Cursor**: follows application cursor movement.
- **Table**: treats delimited or space-aligned text as cells.
- **Highlight**: follows changes to highlighted text, such as menu selections.
In table mode, word navigation moves between cells, line start/end moves to the
first/last cell, and first/last text moves to the first/last character in the
current cell. Place review on a header row and press **Fenrir+X** to use its
cells as column headers. Outside table mode, Fenrir+X places a text-selection
mark.
## Speech and automatic reading
- **Control** interrupts speech.
- **Fenrir+F4** toggles mute.
- **Keypad Enter** on desktop temporarily silences speech. Speech can return on
the next keypress or a newly recognized prompt or completion.
- **Fenrir+Keypad Enter** on desktop, or **Fenrir+Enter** on laptop, toggles
automatic reading of new text.
- **Fenrir+Backspace** passes the next key through to the application.
Character echo, word echo, and cursor echo can be configured separately.
Punctuation levels are `none`, `some`, `most`, and `all`. Character review still
names punctuation. Capital indicators, emoticons, repeated-symbol summaries,
indentation feedback, and attribute feedback are configurable in Reading.
**Fenrir+T** announces the time; press twice quickly for the date.
**Fenrir+Shift+V** announces the Fenrir version.
## Fenrir Menu and quick settings
Press **Fenrir+F** to open the Fenrir Menu. It contains settings tabs for
presentation, speech, sound, reading, keyboard, clipboard, and other options.
The form captures its navigation keys until it closes.
- At the tab selector, use arrows to choose a tab.
- Tab and Shift+Tab move between controls and the tab selector.
- Space or Enter toggles a checkbox.
- Arrows change list selections and numeric values.
- Activate **OK** to save, or **Cancel** or Escape to discard the form's changes.
Speech rate, pitch, and volume use a scale from -100 to 100. Sound volume is
between 0.0 and 1.0. Backend, device, keyboard layout, sound theme, and other
launch-only settings take effect on the next start.
Quick settings work without opening the form:
| Shortcut | Action |
| --- | --- |
| Fenrir+Space | Announce current setting |
| Fenrir+Left / Right | Previous / next setting |
| Fenrir+Control+Left / Right | Previous / next category |
| Fenrir+Up / Down | Change value |
| Fenrir+Control+S | Save settings |
Quick changes apply to the current session. Save to retain them for future
launches. Session controls such as mute and temporary silence are separate
from saved startup preferences.
## Clipboard
Use review navigation to place two marks around text with **Fenrir+X**, then
press **Fenrir+C** to copy it. In table mode Fenrir+X sets headers instead.
| Shortcut | Action |
| --- | --- |
| Fenrir+Shift+X | Read marked text |
| Fenrir+Control+Shift+X | Remove marks |
| Fenrir+C | Copy marked text |
| Fenrir+Control+C | Copy last presented text |
| Fenrir+Shift+C | Read selected clipboard |
| Fenrir+Control+Shift+C | Clear selected clipboard |
| Fenrir+Home / End | First / last clipboard |
| Fenrir+PageUp / PageDown | Previous / next clipboard |
| Fenrir+V | Paste selected clipboard |
| Fenrir+F5 / F6 | Import from / export to file |
| Fenrir+F7 / F8 | Import from / export to X clipboard |
`clipboard.number_of_clipboards` limits history; the default is 200.
`clipboard.export_path` selects the import/export file, initially
`/tmp/fenrirClipboard`. `clipboard.sync` enables automatic X clipboard
synchronization, which requires `xclip` and access to the X display.
Persistent history is optional and off by default. Set `clipboard.persistent`
to retain unencrypted history across launches. Terminal sessions use
`~/.local/state/fenrir/clipboard/terminal.json`; console sessions use
`/var/lib/fenrir/clipboard/tty.json`. Same-user readers of the same mode share
persistent history.
## Spell checking and emoji
Automatic spell checking uses Enchant 2 and a dictionary matching the session
language. Reading settings control word repetition, spoken misspelling feedback,
and misspelling sounds.
Press **Fenrir+S** to open the Spellcheck Menu for the current word. The
Suggestions tab lets you edit the word and choose a suggested spelling.
The Exceptions tab lets you add words, remove selected entries with Delete,
and undo a removal. Selecting a suggestion copies it to Fenrir's clipboard;
paste it with Fenrir+V.
Press **Fenrir+Control+E** to open the Emoji Menu. Choose a category at the tab
selector, then Tab to the emoji grid. Arrows navigate, typing filters by name
prefix, and Control+Backspace clears the filter. Enter copies the selected
emoji to Fenrir's clipboard. Escape closes the menu.
## Reading windows and live regions
Reading regions belong to the current terminal and last for the reader session.
A reading window restricts automatic reading to a rectangle. Live regions
exclude changing areas from automatic speech while allowing explicit reading
and progress monitoring.
Outside table mode, place two marks with **Fenrir+X** to define opposite corners.
Press **Fenrir+Keypad Slash** on desktop or **Fenrir+Control+8** on laptop to set a
reading window. Press the shortcut twice quickly to clear it.
To create a live region, place one mark for the rest of that line, or two marks
for a rectangle, then press **Fenrir+B**. Fenrir assigns the first free slot,
up to ten. Region creation clears the marks.
- **Fenrir+1** through **Fenrir+0** read live regions 1 through 10.
- **Fenrir+Shift+1** through **Fenrir+Shift+0** clear individual live regions.
- **Fenrir+Shift+B** lists the current reading regions.
- **Fenrir+Control+B** clears all live regions.
- **Fenrir+Control+Shift+B** clears both the reading window and live regions.
## Sound and progress
Sound output uses `pulse` for PulseAudio or PipeWire's PulseAudio service, or
`alsa` for an ALSA PCM device. Select the driver and theme in Sound settings.
The ALSA device name is set with `presentation.sound.device` in the file.
Personal themes are searched under `~/.local/stormux/fenrir/sounds/` before
bundled and system themes. A theme contains WAV files and `soundicons.toml`.
Progress tones are controlled separately from speech and automatic reading.
Press **Keypad Plus** on desktop or **Fenrir+Shift+P** on laptop to toggle them.
Fenrir recognizes percentages, fractions, progress bars, and activity indicators;
tones rise with progress. Startup behavior is set by
`presentation.progress.enabled`.
## Configuration files
Settings are TOML. The annotated
[settings.toml](../config/settings/settings.toml) lists all available options.
Fenrir loads one settings file rather than merging files.
- Regular users: first
`~/.local/share/stormux/fenrirscreenreader/settings/settings.toml`, then
`/etc/fenrirscreenreader/settings/settings.toml`.
- Root: `/etc/fenrirscreenreader/settings/settings.toml`.
- `--config PATH`: use that file explicitly.
If default files are absent, Fenrir uses built-in defaults. An explicitly
selected file must exist. Saving targets the selected file; if startup used
built-in defaults, saving creates the first default path for that account.
Create a personal settings file and validate it without opening devices:
```bash
./fenrir --write-config ~/.local/share/stormux/fenrirscreenreader/settings/settings.toml
./fenrir --config ~/.local/share/stormux/fenrirscreenreader/settings/settings.toml --check-config
```
Named keyboard layouts are searched in the regular user's
`~/.local/share/stormux/fenrirscreenreader/keyboard/`, then
`/etc/fenrirscreenreader/keyboard/`, then `config/keyboard/` beside the binary.
Root searches the system and bundled locations. An explicit TOML path selects
one file. Custom layouts replace the complete layout; they are not overlays.
See the [keyboard guide](../config/keyboard/Readme.md) for an example.
## Speech synthesizers
The default driver is `speech-dispatcher`. Select its module, language, and
voice in Speech settings; empty values use server defaults.
Hardware driver IDs are `dectalk`, `dectalk-usb`, `litetalk`, `doubletalk`, and
`tripletalk`. Serial drivers require `presentation.speech.hardware_device`,
for example `/dev/ttyUSB0`, and default to 9600 baud when
`hardware_baud_rate = 0`. DoubleTalk support targets the external LT device.
TripleTalk USB devices need to expose a serial tty.
Native `dectalk-usb` uses libusb 1.0 and ignores serial settings. The process
needs access to the device. The Speech form's **Test synthesizer** button
previews hardware choices before saving. Driver and port changes apply at launch.
## Script shortcuts
Fenrir searches `~/.local/stormux/fenrir/`, the configured
`general.script_path` (default `/usr/share/fenrirscreenreader/scripts`), and the
bundled `config/scripts/` beside the binary. Earlier directories take priority.
A filename such as `example__-__key_y.sh` assigns the script key plus Y.
The script key defaults to Compose and is configured by `general.script_keys`.
Scripts must be executable to run. Fenrir invokes them directly and passes the
current account name as the first argument. Commands appear in the tutorial's
script list.
## Remote control
Unix socket remote control is enabled by default. The standard socket is
`/tmp/fenrirscreenreader-daemon.sock`, and instances also have private sockets
`/tmp/fenrirscreenreader-<pid>.sock`. Set `remote.socket_file` for a custom socket.
Use `ls` to discover instances. Use a private socket to address one reader:
### Fenrir Key
The Fenrir Key is used to invoke screen reader commands. By default:
- Insert
- Keypad Insert
- Meta (Super/Windows key)
You can configure multiple Fenrir keys to suit your preference.
### Keyboard Layouts
- **Desktop Layout**: Uses numeric keypad (recommended for desktop users)
- **Laptop Layout**: Alternative bindings for keyboards without numeric keypad
### Review Mode
Navigate the screen without moving the text cursor. Essential for examining content without disrupting your workflow.
## Essential Commands
### Navigation (Desktop Layout)
- `Ctrl` - Stop speech (shut up)
- `Fenrir + H` - Tutorial mode
- `Fenrir + Ctrl + H` - Speech history
- `Fenrir + Q` - Quit Fenrir
- `Fenrir + Keypad 5` - Read current screen
- `Keypad 8` - Read current line
- `Keypad 5` - Read current word
- `Keypad 2` - Read current character
### Review Commands
- `Keypad 7/9` - Previous/next line
- `Keypad 4/6` - Previous/next word
- `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
- `Keypad dot` - Cursor position
### Clipboard
- `Fenrir + X` - Set mark
- `Fenrir + C` - Copy marked text
- `Fenrir + V` - Paste clipboard
- `Fenrir + Shift + C` - Read current clipboard
### Settings (Runtime Changes)
- `Fenrir + F3` - Toggle sound
- `Fenrir + F4` - Toggle speech
- `Fenrir + Up/Down` - Speech volume
- `Fenrir + Right/Left` - Speech rate
- `Fenrir + Ctrl + P` - Punctuation level
## Configuration
### Settings File
Main configuration:
- **System-wide**: `/etc/fenrirscreenreader/settings/settings.conf`
- **User-local**: `~/.local/share/stormux/fenrirscreenreader/settings/settings.conf`
When Fenrir runs as a regular user, including `fenrir -x`, it loads the user-local settings file if it exists. If it does not exist, Fenrir falls back to the system settings file, but saving settings creates or updates the user-local file. When Fenrir runs as root, settings save to the system-wide file by default.
User sound themes are loaded before system themes from `~/.local/stormux/fenrir/sounds/`. User scripts are loaded before configured system scripts from `~/.local/stormux/fenrir/`.
### Key Sections
- `[speech]` - Speech synthesis settings
- `[sound]` - Sound output and icons
- `[keyboard]` - Input and key bindings
- `[screen]` - Screen reading configuration
- `[general]` - General settings
### Common Settings
```ini
[speech]
enabled=True
driver=speechdDriver
rate=0.5
pitch=0.5
volume=1.0
hardware_device=/dev/ttyS0
hardware_baud_rate=9600
history_size=50
[sound]
enabled=True
driver=genericDriver
volume=0.7
[keyboard]
driver=evdevDriver
keyboard_layout=desktop
[screen]
driver=vcsaDriver
ignore_screen=
[remote]
enable=True
driver=unixDriver
enable_settings_remote=True
enable_command_remote=True
```
## Remote Control
Fenrir includes a powerful remote control system for automation and integration with external applications.
### Configuration
Enable remote control in settings:
```ini
[remote]
enable=True
driver=unixDriver # or tcpDriver
port=22447 # for TCP driver
enable_settings_remote=True # allow settings changes
enable_command_remote=True # allow command execution
```
### Basic Usage with socat
#### Instance Discovery
```bash ```bash
# List registered Fenrir instances and their socket paths
echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
``` echo "command say Hello" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
In X terminal mode (`fenrir -x`), multiple Fenrir instances can run at the same
time. Each instance has a private socket at `/tmp/fenrirscreenreader-<pid>.sock`,
and one instance may also own the standard control socket. Use `ls` or
`command ls` on the standard socket to find the private socket for a specific
instance. Commands sent to the standard socket are handled by its owner when
possible; otherwise they are forwarded to a registered private socket,
preferring the sender's Fenrir ancestor when one can be found.
#### Speech Control
```bash
# Interrupt current speech
echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock echo "command interrupt" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set reading#mode=highlight" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Speak custom text
echo "command say Hello, this is a test" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Temporarily disable speech
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
``` ```
#### Settings Control Remote requests use `section#setting=value` syntax independently of TOML.
```bash Remote speech rate, pitch, and volume use **0.0 to 1.0**, mapped to the
# Enable highlight tracking -100 to 100 scale used in settings files and menus. For example,
echo "setting set focus#highlight=True" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock `speech#rate=0.8` selects rate 60.
# Change speech parameters Other supported operations include:
echo "setting set speech#rate=0.8" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#pitch=0.6" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
echo "setting set speech#volume=0.9" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Change punctuation level (none/some/most/all) - `command tempdisablespeech`: temporarily silence speech.
echo "setting set general#punctuation_level=all" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock - `command clipboard TEXT`: add text to the clipboard.
- `command exportclipboard`: export the selected clipboard.
- `command window X1 Y1 X2 Y2`: set a reading window, using zero-based,
inclusive coordinates.
- `command resetwindow`: clear the window.
- `setting save`: save settings.
- `setting reset`: clear quick and remote overrides back to the last loaded or
successfully saved settings.
- `command quitapplication`: quit Fenrir.
# Voice and TTS control Set `remote.enabled = false` to disable the service. Command and setting
echo "setting set speech#voice=en-us+f3" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock requests have separate switches, `enable_command_remote` and
echo "setting set speech#module=espeak-ng" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock `enable_settings_remote`. Requests are authorized against the caller's account;
console self-voicing also allows an account logged into the active console.
# Multiple settings at once ## Command-line options
echo "setting set speech#rate=0.8;sound#volume=0.7;general#punctuation_level=most" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Reset all settings Fenrir runs as `fenrir [options]`. Use `--help` for the built-in reference.
echo "setting reset" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
# Save settings | Option | Purpose |
echo "setting save" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock | --- | --- |
echo "setting saveas /tmp/my-settings.conf" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock | `--help` | Show usage and options |
``` | `--version` | Print version and release channel |
| `-x` | Start a shell inside this X11 terminal |
#### Clipboard Operations | `--config PATH` | Load a specific TOML settings file |
```bash | `--keyboard-layout NAME_OR_PATH` | Select a named layout or TOML file |
# Add text to clipboard | `--punctuation-profile NAME_OR_PATH` | Select a punctuation profile |
echo "command clipboard Text to copy" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock | `--check-config` | Validate settings, layout, and punctuation without opening devices |
| `--write-config PATH` | Write effective settings to a file and exit |
# Export clipboard to file | `--speech-driver ID` | Override the configured speech driver |
echo "command exportclipboard" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock | `--speech-rate NUMBER` | Override rate, from -100 to 100 |
``` | `--speech-module NAME` | Select a Speech Dispatcher module |
| `--input-device PATH` | Select a console keyboard event device; may be repeated |
#### Application Control | `--debug-file PATH` | Append timing and reader-decision diagnostics |
```bash
# Quit Fenrir
echo "command quitapplication" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
```
### Command Reference
**Speech Commands:**
- `command say <text>` - Speak text
- `command interrupt` - Stop speech
- `command tempdisablespeech` - Disable until next key
- `ls` / `list` / `command ls` / `command list` - List registered Fenrir instances
**Settings Commands:**
- `setting set <section>#<key>=<value>` - Change setting
- `setting reset` - Reset to defaults
- `setting save` - Save current settings
**Other Commands:**
- `command clipboard <text>` - Add to clipboard
- `command exportclipboard` - Export clipboard to file
- `command window <x1> <y1> <x2> <y2>` - Define window
- `command resetwindow` - Reset window
- `command vmenu <menu_path>` - Set virtual menu
- `command resetvmenu` - Reset virtual menu
**Key Settings You Can Change:**
- Punctuation level: `setting set general#punctuation_level=all`
- Speech parameters: `setting set speech#rate=0.8;speech#pitch=0.6`
- Voice selection: `setting set speech#voice=en-us+f3`
- Character echo: `setting set keyboard#char_echo_mode=1`
- Screen ignore: `setting set screen#ignore_screen=1,2,3`
### Scripting Integration
#### Bash Helper Function
```bash
fenrir_say() {
echo "command say $1" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-daemon.sock
}
# Usage
fenrir_say "Build completed"
```
#### Python Integration
```python
import socket
import os
def send_fenrir_command(command):
socket_path = "/tmp/fenrirscreenreader-daemon.sock"
if os.path.exists(socket_path):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(socket_path)
sock.send(command.encode('utf-8'))
finally:
sock.close()
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
Fenrir detects stable progress structures rather than application-specific
status formats, which change too frequently to support reliably.
### Spell Checking
- `Fenrir + S` - Spell check current word
- `Fenrir + S S` - Add word to dictionary
- `Fenrir + Shift + S` - Remove word from dictionary
### Bookmarks (1-10)
- `Fenrir + Shift + [1-0]` - Set bookmark
- `Fenrir + [1-0]` - Go to bookmark
- `Fenrir + Ctrl + [1-0]` - Clear bookmark
### Multiple Clipboards
- `Fenrir + Home/End` - First/last clipboard
- `Fenrir + PageUp/PageDown` - Previous/next clipboard
### Window Mode
1. Set marks to define window area
2. `Fenrir + Keypad /` - Set window
3. `Fenrir + Keypad / /` - Clear window
## Drivers
### Speech Drivers
- **speechdDriver** - Speech-dispatcher (recommended)
- **genericDriver** - Command-line TTS (espeak, etc.)
- **dectalkDriver** - Serial DECtalk-compatible hardware speech
- **dectalkUsbDriver** - Native Access Solutions DECtalk USB (`0dd0:2001`)
- **litetalkDriver** - Serial LiteTalk-compatible hardware speech
- **doubletalkDriver** - Serial DoubleTalk LT-compatible hardware speech
- **tripletalkDriver** - Serial TripleTalk-compatible hardware speech
For serial hardware speech, set `speech#hardware_device` to an explicit path.
RPITalk gadget mode usually appears as `/dev/ttyACM0`; USB serial adapters
usually appear as `/dev/ttyUSB0`; built-in serial ports may be `/dev/ttyS0`.
The default baud rate is `9600`. `doubletalkDriver` targets
DoubleTalk LT-style serial devices, not the internal DoubleTalk PC ISA card.
The `dectalkUsbDriver` autodetects the native Access Solutions DECtalk USB and
ignores both serial settings. It requires PyUSB and a libusb backend. From a
source checkout, install them with
`python3 -m pip install '.[dectalk_usb]'`. A non-root Fenrir process also needs
a local udev rule granting its account read/write access to USB ID
`0dd0:2001`; do not make the device world-writable. USB TripleTalk devices work
only if Linux exposes them as a serial tty such as `/dev/ttyACM0` or
`/dev/ttyUSB0`.
### Sound Drivers
- **genericDriver** - Sox-based (default)
- **gstreamerDriver** - GStreamer-based
### Input Drivers
- **evdevDriver** - Linux evdev (recommended for Linux)
- **x11Driver** - X11 terminal-scoped input for `fenrir -x`
### Screen Drivers
- **vcsaDriver** - Linux VCSA devices (TTY)
- **ptyDriver** - Terminal emulation
### Remote Drivers
- **unixDriver** - Unix socket remote control (recommended)
- **tcpDriver** - TCP socket remote control (localhost only)
## Command Line Options
```
fenrir [OPTIONS]
```
- `-h, --help` - Show help
- `-v, --version` - Show version
- `-f, --foreground` - Run in foreground
- `-s, --setting FILE` - Custom settings file
- `-o, --options SECTION#SETTING=VALUE;..` - Override settings
- `-d, --debug` - Enable debug mode
- `-p, --print` - Print debug to screen
- `-x, --x11` - PTY + X11 keyboard input scoped to the terminal window
- `--x11-window-id WINDOWID` - X11 window id to use for `--x11` terminal mode
- `-F, --force-all-screens` - Ignore ignore_screen setting
- `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s), can be used multiple times
### X11 Terminal Mode
Use `fenrir -x` to run Fenrir in an X11 terminal as a normal user:
```bash
fenrir -x
```
This mode uses PTY screen monitoring and X11 keyboard input. It is intended for GUI terminals such as xterm and VTE-based terminals, while keeping Fenrir key handling scoped to that terminal window. Desktop screen readers keep their global bindings outside the focused terminal.
Fenrir normally detects the target terminal from `WINDOWID` or the active X11 window. If needed, pass a window id explicitly:
```bash
fenrir -x --x11-window-id 0x123456
```
X11 terminal mode uses the same keyboard layout files as TTY Fenrir. Supported Fenrir keys include numpad Insert/`KEY_KP0`, CapsLock/`KEY_CAPSLOCK`, Insert/`KEY_INSERT`, and Super/Windows/`KEY_META`. Compose script keys are exposed as `KEY_COMPOSE`.
In this mode Fenrir runs as a regular user. Settings are loaded from `~/.local/share/stormux/fenrirscreenreader/settings/settings.conf` when present, otherwise from `/etc/fenrirscreenreader/settings/settings.conf`; saving settings creates or updates the user-local file. User sound themes and scripts are read first from `~/.local/stormux/fenrir/sounds/` and `~/.local/stormux/fenrir/`.
This mode requires `python-xlib`.
For a dedicated PTY/terminal screen reader, see TDSR: https://github.com/tspivey/tdsr
## Troubleshooting ## Troubleshooting
### No Speech ### Speech or sound is missing
1. Test speech-dispatcher: `sudo spd-say "hello"`
2. Check driver setting in configuration
3. Verify speech-dispatcher is running
### No Sound Check that speech and sound are enabled, and that the selected backend is
1. Run audio configuration script: `configure_pulse.sh` or `configure_pipewire.sh` available. Speech Dispatcher needs a configured synthesizer. Pulse output needs
2. Check sound driver setting access to a running PulseAudio service, including PipeWire's PulseAudio service;
3. Verify sox installation ALSA needs access to its selected PCM device. Root console sessions may need
separate audio configuration; see the README's setup section.
### No Input Response For hardware speech, check the driver, device path, device permissions, and
1. Check permissions: `/dev/input/*` and `/dev/uinput` serial baud rate. Use **Test synthesizer** before saving a new hardware selection.
2. Verify evdev driver setting
3. Run as root for testing
### Debug Mode ### Keyboard input or X11 mode fails
```bash
sudo fenrir -f -d
# Debug output goes to /tmp/fenrir.log
```
## Getting Help In console mode, check access to keyboard event devices and `/dev/uinput`.
In X11 mode, check `DISPLAY`, `WINDOWID`, and competing grabs on the terminal.
Use NumLock off for bare keypad review. A configured `keyboard.devices` list
or `--input-device` applies to console mode and cannot be used with `-x`.
- **Tutorial Mode**: `Fenrir + H` (interactive help) ### Check configuration and collect diagnostics
- **Wiki**: https://git.stormux.org/storm/fenrir/wiki
- **Email**: stormux+subscribe@groups.io
- **IRC**: irc.stormux.org #stormux
## See Also Validate an edited settings file with `--config PATH --check-config`. For
reader diagnostics, add `--debug-file /tmp/fenrir-debug.log` to the launch
command. This appends timing and reader decisions without screen text or key
values. `./fenrir --version` identifies the build when reporting a problem.
- [README.md](../README.md) - Installation and basic setup ## Documentation maintenance and support
- [diff_review_mode.md](./diff_review_mode.md) - Diff review workflow and key bindings
- [settings.conf](../config/settings/settings.conf) - Configuration reference `docs/user.md` is the shared manual source. With Pandoc installed, run
- `man fenrir` - Manual page `./docs/create_manpage.sh` to regenerate `docs/user.txt`, `docs/fenrir.adoc`, and
`docs/fenrir.1`. The script can run from any working directory.
Please review documentation and test instructions for the affected workflow
before submitting a change, as described in the README's contribution policy.
- [Project repository and issue tracker](https://git.stormux.org/storm/fenrir)
- [Fenrir Wiki](https://git.stormux.org/storm/fenrir/wiki)
- Email list: `stormux+subscribe@groups.io`, with the subject `subscribe`.
- IRC: `irc.stormux.org`, channel `#stormux`.
+433 -1329
View File
File diff suppressed because it is too large. Load diff
+46
View File
@@ -7,6 +7,7 @@ import (
"context" "context"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"git.stormux.org/storm/fenrir/internal/config" "git.stormux.org/storm/fenrir/internal/config"
@@ -15,6 +16,51 @@ import (
"git.stormux.org/storm/fenrir/internal/screen" "git.stormux.org/storm/fenrir/internal/screen"
) )
func TestLoginPromptStartupAndDelayedActivation(t *testing.T) {
for _, terminal := range []int{1, 2} {
// tty1 already has its boot login prompt. tty2's login program
// opens the console later, without a keyboard or screen event.
prompt := screen.Snapshot{Terminal: terminal, Active: true, Rows: 1, Cols: 7, Cells: []rune("login: ")}
source := &fakeSource{snapshot: prompt}
if terminal == 2 {
source.snapshot = screen.Snapshot{Terminal: terminal}
}
voice := &fakeVoice{}
var modes []bool
keys := &fakeKeys{poll: func(active bool, _ func(input.Decision)) error {
modes = append(modes, active)
return nil
}}
a, err := new_application(context.Background(), source, keys, voice, RunOptions{Settings: config.Defaults()})
if err != nil {
t.Fatal(err)
}
t.Cleanup(a.ui.close)
if err := a.read_screen(); err != nil {
t.Fatal(err)
}
if err := a.poll_input(true); err != nil {
t.Fatal(err)
}
if terminal == 2 {
if a.lastActive || len(voice.messages) != 0 {
t.Fatal("unused console was captured or read", a.lastActive, voice.messages)
}
source.snapshot = prompt
if err := a.poll_input(false); err != nil {
t.Fatal(err)
}
}
wantModes := []bool{true}
if terminal == 2 {
wantModes = []bool{false, true}
}
if !a.lastActive || !reflect.DeepEqual(modes, wantModes) || !strings.Contains(strings.Join(voice.messages, "|"), "login:") {
t.Fatal("login prompt did not resume input and speech", terminal, modes, voice.messages)
}
}
}
// Model the console's policy boundary independently of the shared application. // Model the console's policy boundary independently of the shared application.
type policySource struct { type policySource struct {
fakeSource fakeSource
+21
View File
@@ -25,6 +25,7 @@ package console
#include <libevdev/libevdev.h> #include <libevdev/libevdev.h>
#include <libevdev/libevdev-uinput.h> #include <libevdev/libevdev-uinput.h>
#include <linux/kd.h> #include <linux/kd.h>
#include <linux/vt.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
@@ -36,6 +37,13 @@ static int console_state(int fd, int *mode, unsigned char *leds) {
return 0; return 0;
} }
static int console_usage(int fd, unsigned short *mask) {
struct vt_stat state;
if (ioctl(fd, VT_GETSTATE, &state) < 0) return -errno;
*mask = state.v_state;
return 0;
}
static int read_feedback(const struct libevdev_uinput *device, struct input_event *event) { static int read_feedback(const struct libevdev_uinput *device, struct input_event *event) {
ssize_t count = read(libevdev_uinput_get_fd(device), event, sizeof(*event)); ssize_t count = read(libevdev_uinput_get_fd(device), event, sizeof(*event));
if (count < 0) return -errno; if (count < 0) return -errno;
@@ -69,6 +77,19 @@ func kernel_error(code C.int) error {
return syscall.Errno(-code) return syscall.Errno(-code)
} }
func query_usage(path string) (uint16, error) {
file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOCTTY, 0)
if err != nil {
return 0, err
}
defer file.Close()
var mask C.ushort
if err := kernel_error(C.console_usage(C.int(file.Fd()), &mask)); err != nil {
return 0, err
}
return uint16(mask), nil
}
func query_state(path string) (bool, bool, bool, error) { func query_state(path string) (bool, bool, bool, error) {
file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOCTTY, 0) file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOCTTY, 0)
if err != nil { if err != nil {
+12
View File
@@ -41,6 +41,18 @@ import (
// mode and the active VT before each byte; never follow a VT switch mid-paste. // mode and the active VT before each byte; never follow a VT switch mid-paste.
// Kernels that deny TIOCSTI return their error without reporting success. // Kernels that deny TIOCSTI return their error without reporting success.
func (s *Source) Paste(ctx context.Context, text string, terminal int) error { func (s *Source) Paste(ctx context.Context, text string, terminal int) error {
if err := ctx.Err(); err != nil {
return err
}
// Check before opening the target: otherwise our own descriptor would
// make an unused console pass the occupancy check below.
number, textMode, _, _, err := s.active()
if err != nil {
return err
}
if number != terminal || !textMode {
return fmt.Errorf("clipboard paste stopped: active VT changed")
}
path := filepath.Join(s.root, "dev", fmt.Sprintf("tty%d", terminal)) path := filepath.Join(s.root, "dev", fmt.Sprintf("tty%d", terminal))
file, err := os.OpenFile(path, os.O_WRONLY|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0) file, err := os.OpenFile(path, os.O_WRONLY|syscall.O_NOCTTY|syscall.O_NONBLOCK, 0)
if err != nil { if err != nil {
+13
View File
@@ -60,4 +60,17 @@ func TestConsolePasteRejectsChangedTargetAndFailedInjection(t *testing.T) {
if err != nil || len(data) != 0 { if err != nil || len(data) != 0 {
t.Fatal("paste wrote to terminal output", data, err) t.Fatal("paste wrote to terminal output", data, err)
} }
source.Configure_ignored_vts(nil)
source.usage = func(string) (uint16, error) { return 3, nil }
source.state = func(string) (bool, bool, bool, error) {
t.Fatal("paste queried unused target")
return true, false, false, nil
}
// Missing target distinguishes a pre-open rejection from an open failure.
if err := os.Remove(filepath.Join(root, "dev/tty2")); err != nil {
t.Fatal(err)
}
if err := source.Paste(context.Background(), "text", 2); err == nil || !strings.Contains(err.Error(), "active VT changed") {
t.Fatal("paste opened unused console before checking usage", err)
}
} }
+17 -1
View File
@@ -38,6 +38,7 @@ import (
type Source struct { type Source struct {
root string root string
state func(string) (bool, bool, bool, error) state func(string) (bool, bool, bool, error)
usage func(string) (uint16, error)
changes *os.File changes *os.File
watcher *readinessWatcher watcher *readinessWatcher
ignoredVTs atomic.Uint64 // Paste checks policy from its background worker. ignoredVTs atomic.Uint64 // Paste checks policy from its background worker.
@@ -64,7 +65,7 @@ func OpenSource() (*Source, error) {
changes.Close() changes.Close()
return nil, err return nil, err
} }
source := &Source{root: "/", state: query_state, changes: changes, watcher: watcher} source := &Source{root: "/", state: query_state, usage: query_usage, changes: changes, watcher: watcher}
if err := watcher.add(int(changes.Fd())); err != nil { if err := watcher.add(int(changes.Fd())); err != nil {
source.Close() source.Close()
return nil, fmt.Errorf("watch foreground console: %w", err) return nil, fmt.Errorf("watch foreground console: %w", err)
@@ -94,6 +95,21 @@ func (s *Source) active() (int, bool, bool, bool, error) {
if s.ignoredVTs.Load()&(uint64(1)<<uint(number)) != 0 { if s.ignoredVTs.Load()&(uint64(1)<<uint(number)) != 0 {
return number, false, false, false, nil return number, false, false, false, nil
} }
// Opening an unused ttyN, even with O_NOCTTY, marks it occupied and can
// race an on-demand login service's VT_GETSTATE check. Query through tty1
// first, leaving the target untouched until another program has opened it.
// Do not use tty0: that alias opens the foreground console itself.
// tty1 retains its boot-console behavior; the kernel's bitmap cannot
// describe tty16..tty63, so those also retain their existing behavior.
if s.usage != nil && number > 1 && number < 16 {
mask, err := s.usage(filepath.Join(s.root, "dev/tty1"))
if err != nil {
return number, false, false, false, fmt.Errorf("query console use: %w", err)
}
if mask&(uint16(1)<<uint(number)) == 0 {
return number, false, false, false, nil
}
}
textMode, numLock, capsLock, err := s.state(filepath.Join(s.root, "dev", name)) textMode, numLock, capsLock, err := s.state(filepath.Join(s.root, "dev", name))
return number, textMode, numLock, capsLock, err return number, textMode, numLock, capsLock, err
} }
+117
View File
@@ -21,6 +21,7 @@ package console
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -28,6 +29,122 @@ import (
"testing" "testing"
) )
func TestSourceWaitsForConsoleOwner(t *testing.T) {
for _, terminal := range []int{2, 15} {
t.Run(fmt.Sprint(terminal), func(t *testing.T) {
root := t.TempDir()
activePath := filepath.Join(root, "sys/devices/virtual/tty/tty0/active")
if err := os.MkdirAll(filepath.Dir(activePath), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(activePath, fmt.Appendf(nil, "tty%d\n", terminal), 0600); err != nil {
t.Fatal(err)
}
// Other occupied consoles (including tty1) must not make this one
// appear occupied. There are deliberately no target device files yet.
mask := uint16(3)
stateCalls := 0
source := &Source{
root: root,
usage: func(path string) (uint16, error) {
if path != filepath.Join(root, "dev/tty1") {
t.Fatalf("usage query opened target or foreground alias: %s", path)
}
return mask, nil
},
state: func(path string) (bool, bool, bool, error) {
stateCalls++
if mask&(uint16(1)<<uint(terminal)) == 0 {
t.Fatal("opened unused console")
}
if path != filepath.Join(root, "dev", fmt.Sprintf("tty%d", terminal)) {
t.Fatal("queried wrong console", path)
}
return true, true, true, nil
},
}
for range 2 {
if number, active, _, err := source.State(); err != nil || active || number != terminal {
t.Fatal("unused console accepted input", number, active, err)
}
if snapshot, err := source.Read(); err != nil || snapshot.Active || snapshot.Terminal != terminal || len(snapshot.Cells) != 0 {
t.Fatal("unused console read screen devices", snapshot, err)
}
}
if stateCalls != 0 {
t.Fatal("probed unused console", stateCalls)
}
if err := os.Mkdir(filepath.Join(root, "dev"), 0700); err != nil {
t.Fatal(err)
}
text := "login: "
attributes := append([]byte{1, byte(len(text)), 0, 0}, make([]byte, len(text)*2)...)
if err := os.WriteFile(filepath.Join(root, "dev", fmt.Sprintf("vcsa%d", terminal)), attributes, 0600); err != nil {
t.Fatal(err)
}
var data []byte
for _, value := range text {
data = binary.NativeEndian.AppendUint32(data, uint32(value))
}
if err := os.WriteFile(filepath.Join(root, "dev", fmt.Sprintf("vcsu%d", terminal)), data, 0600); err != nil {
t.Fatal(err)
}
// A login prompt is enough; there is no authenticated user/session.
mask |= uint16(1) << uint(terminal)
if number, active, numLock, err := source.State(); err != nil || !active || !numLock || number != terminal {
t.Fatal("login prompt did not activate input", number, active, numLock, err)
}
if snapshot, err := source.Read(); err != nil || !snapshot.Active || string(snapshot.Cells) != text || !snapshot.NumLock || !snapshot.CapsLock {
t.Fatal("login prompt was not readable", snapshot, err)
}
mask = 3
if _, active, _, err := source.State(); err != nil || active {
t.Fatal("console owner disappeared but input stayed active", active, err)
}
problem := errors.New("console usage unavailable")
source.usage = func(string) (uint16, error) { return 0, problem }
if _, active, _, err := source.State(); !errors.Is(err, problem) || active {
t.Fatal("usage failure permitted a target probe", active, err)
}
source.Configure_ignored_vts([]int{terminal})
if _, active, _, err := source.State(); err != nil || active {
t.Fatal("ignored console queried usage", active, err)
}
})
}
}
func TestSourcePreservesBootAndHighConsoleProbes(t *testing.T) {
for _, terminal := range []int{1, 16, 63} {
t.Run(fmt.Sprint(terminal), func(t *testing.T) {
root := t.TempDir()
activePath := filepath.Join(root, "sys/devices/virtual/tty/tty0/active")
if err := os.MkdirAll(filepath.Dir(activePath), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(activePath, fmt.Appendf(nil, "tty%d\n", terminal), 0600); err != nil {
t.Fatal(err)
}
source := &Source{
root: root,
usage: func(string) (uint16, error) {
t.Fatal("boot/high console gated on truncated bitmap")
return 0, nil
},
state: func(path string) (bool, bool, bool, error) {
if path != filepath.Join(root, "dev", fmt.Sprintf("tty%d", terminal)) {
t.Fatal("queried wrong console", path)
}
return true, false, false, nil
},
}
if number, active, _, err := source.State(); err != nil || !active || number != terminal {
t.Fatal("boot/high console lost input", number, active, err)
}
})
}
}
func TestSourceRetriesUnicodeSizeChange(t *testing.T) { func TestSourceRetriesUnicodeSizeChange(t *testing.T) {
for _, cols := range []int{1, 3} { for _, cols := range []int{1, 3} {
t.Run(fmt.Sprint(cols), func(t *testing.T) { t.Run(fmt.Sprint(cols), func(t *testing.T) {