Fixed a regression in reading dialog based TUI.
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from fenrirscreenreader.core.i18n import _
|
from fenrirscreenreader.core.i18n import _
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ class command:
|
|||||||
self._batched_text = []
|
self._batched_text = []
|
||||||
self._last_flush_time = 0
|
self._last_flush_time = 0
|
||||||
self._in_flood_mode = False
|
self._in_flood_mode = False
|
||||||
|
self._pending_focus_text = ""
|
||||||
|
self._pending_focus_time = 0
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
@@ -115,6 +118,62 @@ class command:
|
|||||||
def _normalize_line(self, line):
|
def _normalize_line(self, line):
|
||||||
return " ".join(line.split())
|
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):
|
def _is_subsequence(self, subset_lines, source_lines):
|
||||||
subset_index = 0
|
subset_index = 0
|
||||||
for source_line in source_lines:
|
for source_line in source_lines:
|
||||||
@@ -161,6 +220,10 @@ class command:
|
|||||||
|
|
||||||
if not top_lines_changed or not inserted_lines:
|
if not top_lines_changed or not inserted_lines:
|
||||||
return delta_text
|
return delta_text
|
||||||
|
if all(
|
||||||
|
self._is_focus_control_line(line) for line in inserted_lines
|
||||||
|
):
|
||||||
|
return delta_text
|
||||||
|
|
||||||
normalized_delta_lines = [
|
normalized_delta_lines = [
|
||||||
self._normalize_line(line) for line in 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():
|
if self.env["runtime"]["ScreenManager"].is_screen_change():
|
||||||
self._reset_flood_state()
|
self._reset_flood_state()
|
||||||
|
self._pending_focus_text = ""
|
||||||
|
self._pending_focus_time = 0
|
||||||
|
|
||||||
# Skip if tab completion already handled this delta
|
# Skip if tab completion already handled this delta
|
||||||
if self._was_handled_by_tab_completion(delta_text):
|
if self._was_handled_by_tab_completion(delta_text):
|
||||||
@@ -214,6 +279,10 @@ class command:
|
|||||||
if "\n" not in delta_text:
|
if "\n" not in delta_text:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self._is_transient_focus_only_update(delta_text):
|
||||||
|
self._store_pending_focus_text(delta_text)
|
||||||
|
return
|
||||||
|
|
||||||
rapid = self._is_rapid_updates()
|
rapid = self._is_rapid_updates()
|
||||||
high_volume = self._is_high_volume(delta_text)
|
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']))
|
# 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)
|
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(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
delta_text, interrupt=False, flush=False
|
delta_text, interrupt=False, flush=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
version = "2026.05.12"
|
version = "2026.05.13"
|
||||||
code_name = "testing"
|
code_name = "testing"
|
||||||
|
|||||||
@@ -148,3 +148,95 @@ class TestIncomingCommand:
|
|||||||
command.run()
|
command.run()
|
||||||
|
|
||||||
output_manager.present_text.assert_not_called()
|
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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user