Fixed a regression in reading dialog based TUI.

This commit is contained in:
Storm Dragon
2026-05-13 18:31:11 -04:00
parent 6876547590
commit c48a9a6731
3 changed files with 165 additions and 1 deletions
@@ -5,6 +5,7 @@
# By Chrys, Storm Dragon, and contributors.
import difflib
import re
import time
from fenrirscreenreader.core.i18n import _
@@ -16,6 +17,8 @@ class command:
self._batched_text = []
self._last_flush_time = 0
self._in_flood_mode = False
self._pending_focus_text = ""
self._pending_focus_time = 0
def initialize(self, environment):
self.env = environment
@@ -115,6 +118,62 @@ class command:
def _normalize_line(self, line):
return " ".join(line.split())
def _is_focus_control_line(self, text):
stripped = self._normalize_line(text)
if not stripped:
return False
if re.fullmatch(r"<\s*[A-Za-z][A-Za-z0-9 _.-]{0,30}\s*>", stripped):
return True
return stripped.lower() in {
"ok",
"cancel",
"yes",
"no",
"retry",
"abort",
"ignore",
"continue",
}
def _is_transient_focus_only_update(self, delta_text):
delta_lines = [
self._normalize_line(line)
for line in delta_text.splitlines()
if line.strip()
]
if len(delta_lines) != 1:
return False
if not self._is_focus_control_line(delta_lines[0]):
return False
screen_lines = [
self._normalize_line(line)
for line in self.env["screen"]["new_content_text"].split("\n")
if line.strip()
]
return screen_lines == delta_lines
def _store_pending_focus_text(self, delta_text):
delta_lines = [
self._normalize_line(line)
for line in delta_text.splitlines()
if line.strip()
]
self._pending_focus_text = "\n".join(delta_lines)
self._pending_focus_time = time.time()
def _take_pending_focus_text(self):
if not self._pending_focus_text:
return ""
if time.time() - self._pending_focus_time > 0.5:
self._pending_focus_text = ""
self._pending_focus_time = 0
return ""
pending = self._pending_focus_text
self._pending_focus_text = ""
self._pending_focus_time = 0
return pending
def _is_subsequence(self, subset_lines, source_lines):
subset_index = 0
for source_line in source_lines:
@@ -161,6 +220,10 @@ class command:
if not top_lines_changed or not inserted_lines:
return delta_text
if all(
self._is_focus_control_line(line) for line in inserted_lines
):
return delta_text
normalized_delta_lines = [
self._normalize_line(line) for line in delta_lines
@@ -192,6 +255,8 @@ class command:
if self.env["runtime"]["ScreenManager"].is_screen_change():
self._reset_flood_state()
self._pending_focus_text = ""
self._pending_focus_time = 0
# Skip if tab completion already handled this delta
if self._was_handled_by_tab_completion(delta_text):
@@ -214,6 +279,10 @@ class command:
if "\n" not in delta_text:
return
if self._is_transient_focus_only_update(delta_text):
self._store_pending_focus_text(delta_text)
return
rapid = self._is_rapid_updates()
high_volume = self._is_high_volume(delta_text)
@@ -238,6 +307,9 @@ class command:
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
delta_text = self._prefer_inserted_lower_screen_text(delta_text)
pending_focus_text = self._take_pending_focus_text()
if pending_focus_text:
delta_text = "\n".join([delta_text, pending_focus_text])
self.env["runtime"]["OutputManager"].present_text(
delta_text, interrupt=False, flush=False
)
+1 -1
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2026.05.12"
version = "2026.05.13"
code_name = "testing"
+92
View File
@@ -148,3 +148,95 @@ class TestIncomingCommand:
command.run()
output_manager.present_text.assert_not_called()
def test_dialog_button_paint_is_read_after_following_message(
self, incoming_command
):
command, env, output_manager = incoming_command
env["screen"]["new_content_text"] = "\n".join(
[
"".ljust(20),
"< OK >".center(20),
"".ljust(20),
]
)
env["screen"]["new_delta"] = "\n".join(
[
" ",
" < OK >",
" ",
]
)
command.run()
output_manager.present_text.assert_not_called()
env["screen"]["old_content_text"] = env["screen"]["new_content_text"]
env["screen"]["new_content_text"] = "\n".join(
[
"This is a test".ljust(20),
"< OK >".center(20),
"".ljust(20),
]
)
env["screen"]["new_delta"] = "This is a test"
command.run()
output_manager.present_text.assert_called_once_with(
"This is a test\n< OK >", interrupt=False, flush=False
)
def test_single_word_output_is_not_treated_as_dialog_button(
self, incoming_command
):
command, env, output_manager = incoming_command
env["screen"]["new_content_text"] = "Ready"
env["screen"]["new_delta"] = "Ready"
command.run()
output_manager.present_text.assert_called_once_with(
"Ready", interrupt=False, flush=False
)
def test_dialog_button_is_not_preferred_over_message(
self, incoming_command
):
command, env, output_manager = incoming_command
env["screen"]["old_content_text"] = "\n".join(
[
'bash-5.3$ dialog --msgbox "This is a test" -1 -1'.ljust(80),
"".ljust(80),
"".ljust(80),
"".ljust(80),
"".ljust(80),
"".ljust(80),
]
)
env["screen"]["new_content_text"] = "\n".join(
[
"".ljust(80),
" This is a test".ljust(80),
"".ljust(80),
"".ljust(80),
" < OK >".ljust(80),
"".ljust(80),
]
)
env["screen"]["new_delta"] = "\n".join(
[
" ",
" This is a test ",
" < OK > ",
]
)
command.run()
output_manager.present_text.assert_called_once_with(
" \n This is a test \n < OK > ",
interrupt=False,
flush=False,
)