diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..abd3b578 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,128 @@ +## 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). + +## 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. + +## 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-deamon.sock` +TCP: localhost:22447 + +```bash +echo "command say Hello" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock +echo "ls" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock +echo "setting set speech#rate=0.8" | nc localhost 22447 +``` + +In `fenrir -x`, each instance has a private socket: +`/tmp/fenrirscreenreader-.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 diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index a877ff56..8c3ada93 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -5,4 +5,4 @@ # By Chrys, Storm Dragon, and contributors. version = "2026.05.07" -code_name = "testing" +code_name = "master" diff --git a/src/fenrirscreenreader/remoteDriver/unixDriver.py b/src/fenrirscreenreader/remoteDriver/unixDriver.py index b176855b..e9e70826 100644 --- a/src/fenrirscreenreader/remoteDriver/unixDriver.py +++ b/src/fenrirscreenreader/remoteDriver/unixDriver.py @@ -115,14 +115,22 @@ class driver(remoteDriver): def _socket_file_matches_socket(self, fenrir_sock, socket_file): try: - socket_stat = os.stat(socket_file) + if os.stat(socket_file).st_uid != os.getuid(): + return False fd_stat = os.fstat(fenrir_sock.fileno()) + socket_inode = str(fd_stat.st_ino) + with open("/proc/net/unix", "r", encoding="utf-8") as proc_file: + for line in proc_file: + line_parts = line.split() + if ( + len(line_parts) >= 8 + and line_parts[6] == socket_inode + and line_parts[7] == socket_file + ): + return True except OSError: return False - return ( - socket_stat.st_dev == fd_stat.st_dev - and socket_stat.st_ino == fd_stat.st_ino - ) + return False def _cleanup(self): for fenrir_sock in self.fenrirSocks: