Improved progress bar detections.

This commit is contained in:
Storm Dragon
2026-08-06 13:49:53 -04:00
parent 7593c8d0b3
commit ee441c319a
3 changed files with 129 additions and 26 deletions
@@ -113,6 +113,40 @@ class command:
return True return True
def get_curl_classic_percentage(self, text):
"""Return progress from a structurally valid curl classic row."""
import re
size_field = r"\d+(?:\.\d+)?(?:[kKmMgGtT](?:i?[bB])?)?"
time_field = r"(?:(?:\d+:)?\d{2}:\d{2}|--:--:--)"
curl_classic_match = re.match(
rf"^\s*(\d+)\s+{size_field}\s+"
rf"(\d+)\s+{size_field}\s+"
rf"(\d+)\s+{size_field}\s+"
rf"{size_field}\s+{size_field}\s+"
rf"{time_field}\s+{time_field}\s+{time_field}\s+"
rf"{size_field}(?=\s|$)",
text,
)
if not curl_classic_match:
return None
total_percentage = int(curl_classic_match.group(1))
received_percentage = int(curl_classic_match.group(2))
uploaded_percentage = int(curl_classic_match.group(3))
if not all(
0 <= percentage <= 100
for percentage in (
total_percentage,
received_percentage,
uploaded_percentage,
)
):
return None
if total_percentage != received_percentage:
return None
return float(total_percentage)
def is_explicit_progress_delta(self, text): def is_explicit_progress_delta(self, text):
"""Allow long single-line deltas that still look like progress output.""" """Allow long single-line deltas that still look like progress output."""
import re import re
@@ -120,6 +154,9 @@ class command:
if "\n" in text or self.contains_url(text): if "\n" in text or self.contains_url(text):
return False return False
if self.get_curl_classic_percentage(text) is not None:
return True
has_percentage = re.search(r"(^|\s)\d+(?:\.\d+)?\s*%", text) has_percentage = re.search(r"(^|\s)\d+(?:\.\d+)?\s*%", text)
if not has_percentage: if not has_percentage:
return self.has_progress_status_fields(text) return self.has_progress_status_fields(text)
@@ -229,33 +266,20 @@ class command:
return return
# Pattern 1a2: Curl classic progress format (percentage without % symbol) # Pattern 1a2: Curl classic progress format (percentage without % symbol)
# Extract percentage from curl's classic format percentage = self.get_curl_classic_percentage(text)
curl_classic_match = re.search( if percentage is not None:
r"^\s*(\d+)\s+\d+[kMGT]?\s+(\d+)\s+\d+[kMGT]?\s+\d+\s+\d+\s+\d+[kMGT]?\s+\d+\s+\d+:\d+:\d+\s+\d+:\d+:\d+\s+\d+:\d+:\d+\s+\d+[kMGT]?\s*$", text
)
if curl_classic_match:
# Use the first percentage (total progress)
percentage = float(curl_classic_match.group(1))
if 0 <= percentage <= 100:
self.env["runtime"]["DebugManager"].write_debug_out( self.env["runtime"]["DebugManager"].write_debug_out(
"found curl classic percentage: " + str(percentage), "found curl classic percentage: " + str(percentage),
debug.DebugLevel.INFO, debug.DebugLevel.INFO,
) )
if ( if percentage != self.env["commandBuffer"]["lastProgressValue"]:
percentage
!= self.env["commandBuffer"]["lastProgressValue"]
):
self.env["runtime"]["DebugManager"].write_debug_out( self.env["runtime"]["DebugManager"].write_debug_out(
"Playing tone for curl: " + str(percentage), "Playing tone for curl: " + str(percentage),
debug.DebugLevel.INFO, debug.DebugLevel.INFO,
) )
self.play_progress_tone(percentage) self.play_progress_tone(percentage)
self.env["commandBuffer"][ self.env["commandBuffer"]["lastProgressValue"] = percentage
"lastProgressValue" self.env["commandBuffer"]["lastProgressTime"] = current_time
] = percentage
self.env["commandBuffer"][
"lastProgressTime"
] = current_time
return return
# Pattern 1b: Time/token activity (not percentage-based, so use single # Pattern 1b: Time/token activity (not percentage-based, so use single
+1 -1
View File
@@ -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.07.30" version = "2026.08.06"
code_name = "testing" code_name = "testing"
+79
View File
@@ -290,3 +290,82 @@ def test_explicit_progress_delta_ignores_percentage_in_bracketed_prose():
assert not command.is_explicit_progress_delta( assert not command.is_explicit_progress_delta(
"Release notes [issue #749] are 41% complete" "Release notes [issue #749] are 41% complete"
) )
@pytest.mark.unit
def test_progress_detector_beeps_for_curl_classic_repaint_with_trailing_text():
progress_module = _load_progress_module()
command = progress_module.command()
sample = (
"53 537.8M 53 285.8M 0 0 9.48M 0 00:56 00:30 00:26 9.94M "
"Executing cabextract -q -d /home/Username/.local/wine64/dosdevices/"
"c:/windows/syswow64 -F dx8vb.dll /home/Username/.cache/winetricks/"
"dx8vb/DX81NTger.exe"
)
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_progress_tone = Mock()
assert len(sample) > 200
command.run()
command.play_progress_tone.assert_called_once_with(53.0)
assert command.env["commandBuffer"]["lastProgressValue"] == 53.0
@pytest.mark.unit
def test_progress_detector_ignores_curl_like_fields_with_mismatched_percentages():
progress_module = _load_progress_module()
command = progress_module.command()
sample = (
"53 537.8M 52 285.8M 0 0 9.48M 0 00:56 00:30 00:26 9.94M "
"ordinary numeric report with enough trailing text to cross the long "
"delta filter without representing a coherent download progress row "
"or providing trustworthy percentage information"
)
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.play_progress_tone = Mock()
assert len(sample) > 200
command.run()
command.play_activity_beep.assert_not_called()
command.play_progress_tone.assert_not_called()