From e59101fd155b57fc30e3960752f0953c8c5481fb Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Tue, 8 Oct 2024 12:02:18 -0500 Subject: [PATCH 1/2] Refactor CSI param parsing Now we do the math as we find each digit, rather than at the end after we have collected each digit. It's possible this is faster than extending a str and doing an `int(...)` conversion at the end (I haven't profiled it), but that's not why I made this change. I made this change to support an upcoming change to support SGR parameter "context", which is when a SGR CSI parameter is actually a list of integers: . It's nicer to be able to collect the integers as we go, rather than having to process them all at the end. --- pyte/streams.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyte/streams.py b/pyte/streams.py index cd0f51c..69eb1b2 100644 --- a/pyte/streams.py +++ b/pyte/streams.py @@ -319,7 +319,7 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non # For details on the characters valid for use as # arguments. params = [] - current = "" + current = 0 private = False while True: char = yield None @@ -337,17 +337,18 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non draw(char) break elif char.isdigit(): - current += char + digit_value = ord(char) - ord("0") + current = min(current * 10 + digit_value, 9999) elif char == "$": # XTerm-specific ESC]...$[a-z] sequences are not # currently supported. yield None break else: - params.append(min(int(current or 0), 9999)) + params.append(current) if char == ";": - current = "" + current = 0 else: if private: csi_dispatch[char](*params, private=True) From 80915c73fcf2855cf72257a366d1bc97a9c39d8a Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Tue, 8 Oct 2024 11:58:20 -0500 Subject: [PATCH 2/2] Rework CSI parsing to be aware of subparameters This fixes https://github.com/selectel/pyte/issues/178. Before, we assumed that CSI parameters are only integers. However, that caused us to barf in weird ways when presented with CSI parameters that contain `:`-delimited subparameters. This update to the parsing code causes us to parse those subparameters, and then immediately discard them. That may seem kind of weird, but I'm laying the groundwork for a followup change to the SGR handling code to actually be aware of subparameters (https://github.com/selectel/pyte/issues/179). I just didn't want to muddy this diff with that change as well. --- pyte/streams.py | 25 ++++++++++++++++++++----- tests/test_stream.py | 13 +++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/pyte/streams.py b/pyte/streams.py index 69eb1b2..4269373 100644 --- a/pyte/streams.py +++ b/pyte/streams.py @@ -94,6 +94,7 @@ class Stream: } #: CSI escape sequences -- ``CSI P1;P2;...;Pn ``. + # Note that Pn can contain digits or `:` csi = { esc.ICH: "insert_characters", esc.CUU: "cursor_up", @@ -306,10 +307,14 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non basic_dispatch[char]() elif char == CSI_C1: # All parameters are unsigned, positive decimal integers, with - # the most significant digit sent first. Any parameter greater + # the most significant digit sent first*. Any parameter greater # than 9999 is set to 9999. If you do not specify a value, a 0 # value is assumed. # + # *Not entirely true: Some SGR parameters allow `:`-delimited additional + # subparameters. These additional subparameters are a list of positive decimal integers + # following the above rules. + # # .. seealso:: # # `VT102 User Guide `_ @@ -318,8 +323,12 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non # `VT220 Programmer Ref. `_ # For details on the characters valid for use as # arguments. + # + # `XTerm `_ + # "Using semicolon was incorrect because [...]" + # params = [] - current = 0 + param_with_subparameters = [0] private = False while True: char = yield None @@ -338,17 +347,23 @@ def create_dispatcher(mapping: Mapping[str, str]) -> Dict[str, Callable[..., Non break elif char.isdigit(): digit_value = ord(char) - ord("0") - current = min(current * 10 + digit_value, 9999) + param_with_subparameters[-1] = min(param_with_subparameters[-1] * 10 + digit_value, 9999) + elif char == ":": + param_with_subparameters.append(0) elif char == "$": # XTerm-specific ESC]...$[a-z] sequences are not # currently supported. yield None break else: - params.append(current) + # Note: pyte currently doesn't support subparameters. + # Ideally, we'd update SGR handling to be aware of it. + # That's tracked by . + current_param, *_subparameters = param_with_subparameters + params.append(current_param) if char == ";": - current = 0 + param_with_subparameters = [0] else: if private: csi_dispatch[char](*params, private=True) diff --git a/tests/test_stream.py b/tests/test_stream.py index 7a3ad92..d46ed60 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -70,6 +70,19 @@ def test_unknown_sequences(): assert handler.args == (6, 0) assert handler.kwargs == {} +def test_csi_param_with_colon_context(): + handler = argcheck() + screen = pyte.Screen(80, 24) + screen.debug = handler + + stream = pyte.Stream(screen) + stream.feed(ctrl.CSI + "6:4Z") + assert handler.count == 1 + # Note: currently pyte doesn't actually do anything with `:`-delimited context, + # which is why the `4` disappears here. + assert handler.args == ((6),) + assert handler.kwargs == {} + def test_non_csi_sequences(): for cmd, event in pyte.Stream.csi.items():