Updated some documentation.
This commit is contained in:
1 parent
0e50de4c76
commit
4f5a875d0e
17 files changed
+2416
-6207
No files matched your search
@@ -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
|
|
||||||
@@ -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
|
|
||||||
+74
-579
@@ -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
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
@@ -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
File diff suppressed because it is too large.
Load diff
+410
-2246
File diff suppressed because it is too large.
Load diff
+352
-426
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in new issue
Block a user