diff --git a/README.md b/README.md index 2a07be1f..fbb75339 100644 --- a/README.md +++ b/README.md @@ -653,6 +653,10 @@ Building... - **Non-blocking**: Progress tones don't interrupt speech or other functionality - **Configurable**: Can be enabled/disabled as needed +Fenrir detects stable progress structures rather than application-specific +status formats. Application-specific formats change too frequently to support +reliably. + ### Usage Examples ```bash diff --git a/docs/user.md b/docs/user.md index 9f70fcab..bece798e 100644 --- a/docs/user.md +++ b/docs/user.md @@ -312,6 +312,9 @@ Fenrir automatically detects and provides audio feedback for progress indicators - **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 diff --git a/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py b/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py index f97270fc..6374ad15 100644 --- a/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py +++ b/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py @@ -81,7 +81,7 @@ class command: delta_length = len(delta_text) if ( delta_length > 200 - ): # Allow longer progress lines like Claude Code's status + ): # Allow longer progress lines such as terminal status output if not self.is_explicit_progress_delta(delta_text): self.env["runtime"]["DebugManager"].write_debug_out( f"Progress filter: delta too long ({delta_length})", @@ -326,43 +326,27 @@ class command: self.env["commandBuffer"]["lastProgressTime"] = current_time return - # Pattern 6: Claude Code working indicators (various symbols + activity text + "esc/ctrl+c to interrupt") - # Matches any: [symbol] [Task description]… (... to interrupt ...) + # Pattern 6: Interruptible terminal activity indicators + # Matches any: [symbol] [Task description][…] (... to interrupt ...) # Symbols include: * ✢ ✽ ✶ ✻ · • ◦ ○ ● ◆ and similar decorative characters - # Example: ✽ Reviewing script for issues… (ctrl+c to interrupt · 33s · ↑ 1.6k tokens · thought for 4s) - claude_progress_match = re.search( - r'[*✢✽✶✻·•◦○●◆]\s+\w+.*?…\s*\(.*(?:esc|ctrl\+c) to interrupt.*\)', + # Keep this structural rather than adding application-specific formats, + # which change too frequently to support reliably. + interruptible_activity_match = re.search( + r'[*✢✽✶✻·•◦○●◆]\s+\w+.*?(?:…\s*)?\(.*(?:esc|ctrl\+c) to interrupt.*\)', text, re.IGNORECASE, ) - if claude_progress_match: + if interruptible_activity_match: if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0: self.env["runtime"]["DebugManager"].write_debug_out( - "Playing Claude Code activity beep", + "Playing interruptible activity beep", debug.DebugLevel.INFO, ) self.play_activity_beep() self.env["commandBuffer"]["lastProgressTime"] = current_time return - # Pattern 6b: Claude Code tool invocation indicators (● Tool Name(...)) - # Example: ● Web Search("query here") - tool_invocation_match = re.search( - r'[●○◉•◦]\s+(?:Web\s*Search|Read|Write|Edit|Bash|Glob|Grep|Task|WebFetch)\s*\(', - text, - re.IGNORECASE, - ) - if tool_invocation_match: - if current_time - self.env["commandBuffer"]["lastProgressTime"] >= 1.0: - self.env["runtime"]["DebugManager"].write_debug_out( - "Playing Claude Code tool invocation beep", - debug.DebugLevel.INFO, - ) - self.play_activity_beep() - self.env["commandBuffer"]["lastProgressTime"] = current_time - return - - # Pattern 6c: Bullet/white bullet activity lines (•/◦ ...) + # Pattern 6b: Bullet/white bullet activity lines (•/◦ ...) bullet_activity_match = re.search( ( r'^\s*[•◦]\s+.*(?:…|\.{3,}|\b(?:thinking|working|processing|' diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index 9bd83cd7..3f4944d9 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -4,5 +4,5 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. -version = "2026.05.31" +version = "2026.06.01" code_name = "master" diff --git a/tests/unit/test_progress_detector.py b/tests/unit/test_progress_detector.py index 0a77d57d..d0ec90be 100644 --- a/tests/unit/test_progress_detector.py +++ b/tests/unit/test_progress_detector.py @@ -109,3 +109,34 @@ def test_progress_detector_beeps_for_long_tqdm_transfer_delta(): command.play_progress_tone.assert_called_once_with(90.0) assert command.env["commandBuffer"]["lastProgressValue"] == 90.0 + + +@pytest.mark.unit +def test_progress_detector_beeps_for_interruptible_status_without_ellipsis(): + progress_module = _load_progress_module() + command = progress_module.command() + sample = "◦ Files: (1m 04s • esc to interrupt)" + command.env = { + "commandBuffer": { + "progress_monitoring": True, + "lastProgressValue": -1, + "lastProgressTime": 0, + }, + "runtime": { + "DebugManager": Mock(write_debug_out=Mock()), + "ScreenManager": Mock(is_screen_change=Mock(return_value=False)), + "CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)), + }, + "screen": { + "new_delta": sample, + "new_delta_is_typing": False, + "new_content_text": sample, + "old_cursor": {"x": 0, "y": 0}, + "new_cursor": {"x": 0, "y": 0}, + }, + } + command.play_activity_beep = Mock() + + command.run() + + command.play_activity_beep.assert_called_once_with()