Improved progress bar detections.
This commit is contained in:
@@ -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,34 +266,21 @@ 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
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
)
|
"found curl classic percentage: " + str(percentage),
|
||||||
if curl_classic_match:
|
debug.DebugLevel.INFO,
|
||||||
# Use the first percentage (total progress)
|
)
|
||||||
percentage = float(curl_classic_match.group(1))
|
if percentage != self.env["commandBuffer"]["lastProgressValue"]:
|
||||||
if 0 <= percentage <= 100:
|
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||||
"found curl classic percentage: " + str(percentage),
|
"Playing tone for curl: " + str(percentage),
|
||||||
debug.DebugLevel.INFO,
|
debug.DebugLevel.INFO,
|
||||||
)
|
)
|
||||||
if (
|
self.play_progress_tone(percentage)
|
||||||
percentage
|
self.env["commandBuffer"]["lastProgressValue"] = percentage
|
||||||
!= self.env["commandBuffer"]["lastProgressValue"]
|
self.env["commandBuffer"]["lastProgressTime"] = current_time
|
||||||
):
|
return
|
||||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
|
||||||
"Playing tone for curl: " + str(percentage),
|
|
||||||
debug.DebugLevel.INFO,
|
|
||||||
)
|
|
||||||
self.play_progress_tone(percentage)
|
|
||||||
self.env["commandBuffer"][
|
|
||||||
"lastProgressValue"
|
|
||||||
] = percentage
|
|
||||||
self.env["commandBuffer"][
|
|
||||||
"lastProgressTime"
|
|
||||||
] = current_time
|
|
||||||
return
|
|
||||||
|
|
||||||
# Pattern 1b: Time/token activity (not percentage-based, so use single
|
# Pattern 1b: Time/token activity (not percentage-based, so use single
|
||||||
# beep)
|
# beep)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user