urwid 4.2.4__tar.gz → 4.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {urwid-4.2.4/urwid.egg-info → urwid-4.2.5}/PKG-INFO +2 -2
- urwid-4.2.5/mypy-requirements.txt +1 -0
- urwid-4.2.5/requirements.txt +2 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/ansi_parser.py +134 -39
- {urwid-4.2.4 → urwid-4.2.5}/urwid/canvas.py +163 -30
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/_posix_raw_display.py +77 -62
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/_raw_display_base.py +35 -16
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/_win32.py +8 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/_win32_raw_display.py +52 -25
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/common.py +3 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/escape.py +25 -22
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/html_fragment.py +1 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/web.py +1 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/asyncio_loop.py +6 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/main_loop.py +38 -20
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/select_loop.py +6 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/trio_loop.py +18 -5
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/zmq_loop.py +23 -10
- {urwid-4.2.4 → urwid-4.2.5}/urwid/signals.py +15 -2
- {urwid-4.2.4 → urwid-4.2.5}/urwid/str_util.py +74 -27
- {urwid-4.2.4 → urwid-4.2.5}/urwid/text_layout.py +56 -26
- {urwid-4.2.4 → urwid-4.2.5}/urwid/util.py +20 -3
- {urwid-4.2.4 → urwid-4.2.5}/urwid/version.py +3 -3
- {urwid-4.2.4 → urwid-4.2.5}/urwid/vterm.py +7 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/bar_graph.py +44 -5
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/columns.py +54 -16
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/edit.py +30 -5
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/grid_flow.py +17 -7
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/listbox.py +74 -13
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/monitored_deque.py +108 -33
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/monitored_list.py +43 -29
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/pile.py +58 -19
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/progress_bar.py +20 -3
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/treetools.py +84 -24
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/wimp.py +5 -4
- {urwid-4.2.4 → urwid-4.2.5/urwid.egg-info}/PKG-INFO +2 -2
- {urwid-4.2.4 → urwid-4.2.5}/urwid.egg-info/requires.txt +1 -1
- {urwid-4.2.4 → urwid-4.2.5}/urwid.egg-info/scm_file_list.json +2 -0
- urwid-4.2.5/urwid.egg-info/scm_version.json +8 -0
- urwid-4.2.4/mypy-requirements.txt +0 -1
- urwid-4.2.4/requirements.txt +0 -2
- urwid-4.2.4/urwid.egg-info/scm_version.json +0 -8
- {urwid-4.2.4 → urwid-4.2.5}/.devcontainer/dev.dockerfile +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.devcontainer/devcontainer.json +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.dockerignore +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.editorconfig +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/CODE_OF_CONDUCT.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/ISSUE_TEMPLATE/docs.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/dependabot.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/labeler.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/release.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/workflows/documentation.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/workflows/isolated_mypy.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/workflows/isolated_pylint.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/workflows/labels.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/workflows/pythonpackage.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.github/workflows/yamllint.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.gitignore +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.pre-commit-config.yaml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/.yamllint.yml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/COPYING +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/MANIFEST.in +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/README.rst +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/SECURITY.md +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/classifiers.txt +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/conftest.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/pylint-requirements.txt +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/pyproject.toml +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/pytest_requirements.txt +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/setup.cfg +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/tox.ini +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/__init__.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/command_map.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/__init__.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/_web.css +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/_web.js +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/curses.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/lcd.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/display/raw.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/__init__.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/abstract_loop.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/glib_loop.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/tornado_loop.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/event_loop/twisted_loop.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/font.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/graphics.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/numedit.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/py.typed +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/split_repr.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/__init__.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/ansi_text.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/attr_map.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/attr_wrap.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/big_text.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/box_adapter.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/constants.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/container.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/deque_walker.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/divider.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/filler.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/frame.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/line_box.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/list_walker.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/overlay.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/padding.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/popup.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/scrollable.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/solid_fill.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/text.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/widget.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid/widget/widget_decoration.py +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid.egg-info/SOURCES.txt +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid.egg-info/dependency_links.txt +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid.egg-info/not-zip-safe +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/urwid.egg-info/top_level.txt +0 -0
- {urwid-4.2.4 → urwid-4.2.5}/yamllint-requirements.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: urwid
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.5
|
|
4
4
|
Summary: A full-featured console (xterm et al.) user interface library
|
|
5
5
|
Author-email: Ian Ward <ian@excess.org>
|
|
6
6
|
License-Expression: LGPL-2.1-only
|
|
@@ -38,7 +38,7 @@ Classifier: Typing :: Typed
|
|
|
38
38
|
Requires-Python: >=3.9.0
|
|
39
39
|
Description-Content-Type: text/x-rst
|
|
40
40
|
License-File: COPYING
|
|
41
|
-
Requires-Dist: wcwidth
|
|
41
|
+
Requires-Dist: wcwidth!=0.9.0,!=0.9.1,>=0.4
|
|
42
42
|
Requires-Dist: typing-extensions
|
|
43
43
|
Provides-Extra: curses
|
|
44
44
|
Requires-Dist: windows-curses; sys_platform == "win32" and extra == "curses"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mypy==2.4.0
|
|
@@ -89,6 +89,34 @@ _TERMINAL_SETTING_CSI = frozenset("hlrJKsu@LMPXcng")
|
|
|
89
89
|
#: Line-end byte pairs that collapse into a single structural split point.
|
|
90
90
|
_LINE_END_PAIR: dict[str, str] = {"\r": "\n", "\n": "\r"}
|
|
91
91
|
|
|
92
|
+
#: The furthest a relative cursor move advances, and the furthest 1-based row or column an absolute move reaches.
|
|
93
|
+
#: The input is untrusted, and a few bytes such as ``CSI 300000000 C`` would otherwise pad the grid with that many
|
|
94
|
+
#: cells. 1024 exceeds the width and height of practical terminals, and caps what one movement sequence adds to the
|
|
95
|
+
#: grid at 1024 cells.
|
|
96
|
+
_MAX_CURSOR_POSITION = 1024
|
|
97
|
+
|
|
98
|
+
#: CSI parameter strings of this length or longer are skipped unparsed. Real sequences are far shorter (a truecolour
|
|
99
|
+
#: foreground and background SGR is under 50 characters). Staying below 640, the lowest limit
|
|
100
|
+
#: :func:`sys.set_int_max_str_digits` accepts, guarantees ``int()`` never rejects a parameter.
|
|
101
|
+
_MAX_CSI_LENGTH = 256
|
|
102
|
+
|
|
103
|
+
#: OSC strings of this length or longer are skipped, and the rest of the string up to its terminator, or up to an ESC
|
|
104
|
+
#: starting a new sequence, is discarded unbuffered, so an unterminated OSC costs neither memory nor quadratic string
|
|
105
|
+
#: growth.
|
|
106
|
+
_MAX_OSC_LENGTH = 4096
|
|
107
|
+
|
|
108
|
+
#: The most entries a ``skipped`` list holds, covering one 64 KiB raw display read of 2-byte sequences.
|
|
109
|
+
_MAX_SKIPPED = 65536
|
|
110
|
+
|
|
111
|
+
#: Padding cells a parser may insert regardless of input length, one full 1024 x 1024 screen.
|
|
112
|
+
_PADDING_BUDGET = _MAX_CURSOR_POSITION * _MAX_CURSOR_POSITION
|
|
113
|
+
|
|
114
|
+
#: Padding cells each fed character adds to the budget, as many as a tab writes.
|
|
115
|
+
_PADDING_PER_CHARACTER = 8
|
|
116
|
+
|
|
117
|
+
#: Padding cells an empty row inserted by a vertical move costs, about what the row allocates.
|
|
118
|
+
_PADDING_PER_ROW = 8
|
|
119
|
+
|
|
92
120
|
|
|
93
121
|
@dataclasses.dataclass(frozen=True)
|
|
94
122
|
class SkippedOp:
|
|
@@ -98,7 +126,8 @@ class SkippedOp:
|
|
|
98
126
|
full two-dimensional terminal canvas.
|
|
99
127
|
|
|
100
128
|
:ivar kind: one of ``"vertical-move"``, ``"terminal-setting"`` or ``"unknown"``.
|
|
101
|
-
:ivar raw: the raw sequence text as encountered (including its ``ESC``/``CSI`` framing)
|
|
129
|
+
:ivar raw: the raw sequence text as encountered (including its ``ESC``/``CSI`` framing), truncated for a sequence
|
|
130
|
+
skipped for its length.
|
|
102
131
|
:ivar reason: a human-readable explanation, used verbatim in the log message emitted alongside this entry.
|
|
103
132
|
"""
|
|
104
133
|
|
|
@@ -125,8 +154,8 @@ class ParsedLine:
|
|
|
125
154
|
:ivar bel: the number of BEL (``\a``) characters seen on this line (``0`` if none were seen).
|
|
126
155
|
:ivar title: the last OSC window-title string seen on this line, or ``None`` if none was seen.
|
|
127
156
|
:ivar leds: the last keyboard-LED state requested on this line (``CSI n q``), or ``None`` if none was seen.
|
|
128
|
-
:ivar skipped:
|
|
129
|
-
inspection independent of the log.
|
|
157
|
+
:ivar skipped: the :class:`SkippedOp` entries stripped while parsing this line, in encounter order, for
|
|
158
|
+
programmatic inspection independent of the log. It holds at most the first 65536 of them.
|
|
130
159
|
"""
|
|
131
160
|
|
|
132
161
|
text: str
|
|
@@ -168,8 +197,8 @@ class AnsiParser:
|
|
|
168
197
|
(``"progress: 10%\rprogress: 20%"``) resolve correctly onto a single row, mirroring real terminal behaviour.
|
|
169
198
|
Vertical cursor movement genuinely repositions the cursor within the grid instead of being stripped. The side
|
|
170
199
|
channels are aggregated over the *entire* input rather than reset per row: ``bel`` is the total BEL count,
|
|
171
|
-
``title``/``leds`` are the last value seen anywhere, and ``skipped`` lists
|
|
172
|
-
encounter order.
|
|
200
|
+
``title``/``leds`` are the last value seen anywhere, and ``skipped`` lists the :class:`SkippedOp`
|
|
201
|
+
entries in encounter order.
|
|
173
202
|
|
|
174
203
|
Deliberately out of scope in both modes (stripped and logged as :class:`SkippedOp` with
|
|
175
204
|
``kind="terminal-setting"`` or ``kind="unknown"``): erase (``CSI J``/``K``), scroll regions, save/restore
|
|
@@ -177,6 +206,12 @@ class AnsiParser:
|
|
|
177
206
|
(``ESC D``/``M``/``E``) -- a genuine two-dimensional buffer would technically allow implementing erase and
|
|
178
207
|
save/restore cursor, but that is a candidate follow-up, kept out of scope here.
|
|
179
208
|
|
|
209
|
+
The padding inserted to reach a moved cursor -- blank cells before a written character and empty rows below the
|
|
210
|
+
last one -- is limited to ``1024 * 1024 + 8 * n`` cells over the parser's lifetime, ``n`` being the number of
|
|
211
|
+
characters fed so far, and an empty row counts as 8 cells. Once the limit is reached, a character is written at the
|
|
212
|
+
end of its row instead, a vertical move past the last row stays on the last row, and each such cut is recorded as
|
|
213
|
+
a :class:`SkippedOp` with ``kind="unknown"``.
|
|
214
|
+
|
|
180
215
|
:meth:`finalize` returns a uniform ``tuple[ParsedLine, ...]`` in both modes: in ``one_line=True`` mode, every
|
|
181
216
|
line completed since construction (queue plus a final flush of whatever remains, possibly empty or
|
|
182
217
|
unterminated); in ``one_line=False`` mode, always exactly one :class:`ParsedLine` -- built from the whole grid,
|
|
@@ -210,6 +245,10 @@ class AnsiParser:
|
|
|
210
245
|
self._row = 0
|
|
211
246
|
self._col = 0
|
|
212
247
|
|
|
248
|
+
# padding cells still allowed, and the last cursor move a cut padding is recorded against
|
|
249
|
+
self._padding_budget = _PADDING_BUDGET
|
|
250
|
+
self._last_move = ""
|
|
251
|
+
|
|
213
252
|
# side channels -- reset per completed line in one_line mode (see
|
|
214
253
|
# _complete_line()), aggregated over the entire input otherwise
|
|
215
254
|
self._bel_count = 0
|
|
@@ -220,7 +259,8 @@ class AnsiParser:
|
|
|
220
259
|
# completed-lines queue, used only in one_line mode
|
|
221
260
|
self._completed: list[ParsedLine] = []
|
|
222
261
|
|
|
223
|
-
# escape-framing state: 0 none / 1 CSI / 2 OSC / 3 two-char intermediate
|
|
262
|
+
# escape-framing state: 0 none / 1 CSI / 2 OSC / 3 two-char intermediate /
|
|
263
|
+
# 4 the discarded remainder of an over-long OSC string
|
|
224
264
|
self._within_escape = False
|
|
225
265
|
self._parsestate = 0
|
|
226
266
|
self._escbuf = ""
|
|
@@ -240,6 +280,7 @@ class AnsiParser:
|
|
|
240
280
|
"""
|
|
241
281
|
if not text:
|
|
242
282
|
return
|
|
283
|
+
self._padding_budget += _PADDING_PER_CHARACTER * len(text)
|
|
243
284
|
|
|
244
285
|
if self._pending_line_end is not None:
|
|
245
286
|
pending = self._pending_line_end
|
|
@@ -366,6 +407,20 @@ class AnsiParser:
|
|
|
366
407
|
while len(self._rows) <= row:
|
|
367
408
|
self._rows.append([])
|
|
368
409
|
|
|
410
|
+
def _move_to_row(self, row: int, raw: str) -> None:
|
|
411
|
+
"""Move the cursor to ``row``, inserting the missing empty rows up to it within the padding budget.
|
|
412
|
+
|
|
413
|
+
Without enough budget the cursor moves to the last existing row instead, and ``raw`` is recorded as skipped.
|
|
414
|
+
"""
|
|
415
|
+
cost = (row + 1 - len(self._rows)) * _PADDING_PER_ROW
|
|
416
|
+
if cost > self._padding_budget:
|
|
417
|
+
self._skip("unknown", raw, "padding limit reached, cursor kept on the last row")
|
|
418
|
+
row = min(row, len(self._rows) - 1)
|
|
419
|
+
elif cost > 0:
|
|
420
|
+
self._padding_budget -= cost
|
|
421
|
+
self._ensure_row(row)
|
|
422
|
+
self._row = row
|
|
423
|
+
|
|
369
424
|
def _newline(self) -> None:
|
|
370
425
|
self._row += 1
|
|
371
426
|
self._ensure_row(self._row)
|
|
@@ -374,7 +429,7 @@ class AnsiParser:
|
|
|
374
429
|
# -- internal: char-by-char state machine ---------------------------
|
|
375
430
|
|
|
376
431
|
def _step(self, ch: str) -> None:
|
|
377
|
-
if self._parsestate
|
|
432
|
+
if self._parsestate in {2, 4}:
|
|
378
433
|
self._handle_osc_char(ch)
|
|
379
434
|
return
|
|
380
435
|
if ch == BEL:
|
|
@@ -413,8 +468,13 @@ class AnsiParser:
|
|
|
413
468
|
elif self._col == len(row):
|
|
414
469
|
row.append((ch, attr))
|
|
415
470
|
else:
|
|
416
|
-
|
|
417
|
-
|
|
471
|
+
gap = self._col - len(row)
|
|
472
|
+
if gap > self._padding_budget:
|
|
473
|
+
self._skip("unknown", self._last_move, "padding limit reached, written at the end of the row")
|
|
474
|
+
self._col = len(row)
|
|
475
|
+
else:
|
|
476
|
+
self._padding_budget -= gap
|
|
477
|
+
row.extend([(" ", None)] * gap)
|
|
418
478
|
row.append((ch, attr))
|
|
419
479
|
self._col += 1
|
|
420
480
|
|
|
@@ -426,17 +486,22 @@ class AnsiParser:
|
|
|
426
486
|
self._escbuf = ""
|
|
427
487
|
|
|
428
488
|
def _skip(self, kind: str, raw: str, reason: str) -> None:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
489
|
+
"""Record a stripped sequence and log it at debug level.
|
|
490
|
+
|
|
491
|
+
Entries past the first :data:`_MAX_SKIPPED` are not recorded.
|
|
492
|
+
Untrusted input can repeat a sequence without limit.
|
|
493
|
+
A warning would reach stderr, over the UI, through Python's last-resort handler.
|
|
494
|
+
"""
|
|
495
|
+
if len(self._skipped) < _MAX_SKIPPED:
|
|
496
|
+
self._skipped.append(SkippedOp(kind=kind, raw=raw, reason=reason))
|
|
497
|
+
LOGGER.debug("%s: %r", reason, raw)
|
|
435
498
|
|
|
436
499
|
def _parse_escape(self, ch: str) -> None:
|
|
437
500
|
if self._parsestate == 1: # within CSI
|
|
438
501
|
if ch in "0123456789;" or (not self._escbuf and ch == "?"):
|
|
439
|
-
|
|
502
|
+
# past the limit the parameters are dropped; _dispatch_csi skips the sequence by its length
|
|
503
|
+
if len(self._escbuf) < _MAX_CSI_LENGTH:
|
|
504
|
+
self._escbuf += ch
|
|
440
505
|
return
|
|
441
506
|
self._dispatch_csi(ch, self._escbuf)
|
|
442
507
|
self._leave_escape()
|
|
@@ -469,23 +534,29 @@ class AnsiParser:
|
|
|
469
534
|
self._leave_escape()
|
|
470
535
|
|
|
471
536
|
def _dispatch_csi(self, final: str, escbuf: str) -> None:
|
|
537
|
+
raw = f"{ESC}[{escbuf}{final}"
|
|
538
|
+
if len(escbuf) >= _MAX_CSI_LENGTH:
|
|
539
|
+
self._skip("unknown", raw, f"CSI parameters of {_MAX_CSI_LENGTH} characters or more, truncated")
|
|
540
|
+
return
|
|
541
|
+
|
|
472
542
|
qmark = escbuf.startswith("?")
|
|
473
543
|
body = escbuf[1:] if qmark else escbuf
|
|
474
544
|
params: list[int | None] = [int(p) if p else None for p in body.split(";")] if body else []
|
|
475
|
-
raw = f"{ESC}[{escbuf}{final}"
|
|
476
545
|
|
|
477
546
|
if final == "m":
|
|
478
547
|
ints = [p if p is not None else 0 for p in params] or [0]
|
|
479
548
|
self._attrspec = sgi_params_to_attrspec(ints, self._attrspec)
|
|
480
549
|
elif final == "C":
|
|
481
550
|
n = params[0] if params and params[0] else 1
|
|
482
|
-
self._col += n
|
|
551
|
+
self._col += min(n, _MAX_CURSOR_POSITION)
|
|
552
|
+
self._last_move = raw
|
|
483
553
|
elif final == "D":
|
|
484
554
|
n = params[0] if params and params[0] else 1
|
|
485
555
|
self._col = max(0, self._col - n)
|
|
486
556
|
elif final == "G":
|
|
487
557
|
n = params[0] if params and params[0] else 1
|
|
488
|
-
self._col =
|
|
558
|
+
self._col = min(n, _MAX_CURSOR_POSITION) - 1
|
|
559
|
+
self._last_move = raw
|
|
489
560
|
elif final == "q":
|
|
490
561
|
mode = params[0] if params and params[0] else 0
|
|
491
562
|
self._leds = led_state(mode)
|
|
@@ -503,6 +574,7 @@ class AnsiParser:
|
|
|
503
574
|
# reinterpreted
|
|
504
575
|
self._skip("vertical-move", raw, f"CSI {final!r} vertical cursor movement is meaningless for a single line")
|
|
505
576
|
return
|
|
577
|
+
self._last_move = raw
|
|
506
578
|
|
|
507
579
|
# these genuinely reposition the cursor within the grid
|
|
508
580
|
if final == "A":
|
|
@@ -510,12 +582,10 @@ class AnsiParser:
|
|
|
510
582
|
self._row = max(0, self._row - n)
|
|
511
583
|
elif final == "B":
|
|
512
584
|
n = params[0] if params and params[0] else 1
|
|
513
|
-
self._row
|
|
514
|
-
self._ensure_row(self._row)
|
|
585
|
+
self._move_to_row(self._row + min(n, _MAX_CURSOR_POSITION), raw)
|
|
515
586
|
elif final == "E":
|
|
516
587
|
n = params[0] if params and params[0] else 1
|
|
517
|
-
self._row
|
|
518
|
-
self._ensure_row(self._row)
|
|
588
|
+
self._move_to_row(self._row + min(n, _MAX_CURSOR_POSITION), raw)
|
|
519
589
|
self._col = 0
|
|
520
590
|
elif final == "F":
|
|
521
591
|
n = params[0] if params and params[0] else 1
|
|
@@ -523,14 +593,12 @@ class AnsiParser:
|
|
|
523
593
|
self._col = 0
|
|
524
594
|
elif final == "d":
|
|
525
595
|
n = params[0] if params and params[0] else 1
|
|
526
|
-
self.
|
|
527
|
-
self._ensure_row(self._row)
|
|
596
|
+
self._move_to_row(min(n, _MAX_CURSOR_POSITION) - 1, raw)
|
|
528
597
|
elif final in ("H", "f"):
|
|
529
598
|
r = params[0] if params and params[0] else 1
|
|
530
599
|
c = params[1] if len(params) > 1 and params[1] else 1
|
|
531
|
-
self.
|
|
532
|
-
self.
|
|
533
|
-
self._col = max(0, c - 1)
|
|
600
|
+
self._move_to_row(min(r, _MAX_CURSOR_POSITION) - 1, raw)
|
|
601
|
+
self._col = min(c, _MAX_CURSOR_POSITION) - 1
|
|
534
602
|
|
|
535
603
|
def _dispatch_noncsi(self, ch: str, mod: str) -> None:
|
|
536
604
|
raw = f"{ESC}{mod}{ch}"
|
|
@@ -558,12 +626,35 @@ class AnsiParser:
|
|
|
558
626
|
self._skip("unknown", raw, f"unrecognised escape sequence {raw!r}")
|
|
559
627
|
|
|
560
628
|
def _handle_osc_char(self, ch: str) -> None:
|
|
629
|
+
if self._escbuf[-1:] == ESC and ch != "\\":
|
|
630
|
+
# ECMA-48 allows no ESC inside a command string other than in ST, so any other ESC ends the string
|
|
631
|
+
# unapplied and starts a new escape sequence
|
|
632
|
+
if self._parsestate == 2:
|
|
633
|
+
self._skip("unknown", f"{ESC}]{self._escbuf[:-1]}", "unterminated OSC string")
|
|
634
|
+
self._leave_escape()
|
|
635
|
+
self._within_escape = True
|
|
636
|
+
self._step(ch)
|
|
637
|
+
return
|
|
638
|
+
if self._parsestate == 4:
|
|
639
|
+
# only the last character is kept, which is enough to recognise either terminator
|
|
640
|
+
if ch == BEL or (self._escbuf == ESC and ch == "\\"):
|
|
641
|
+
self._leave_escape()
|
|
642
|
+
else:
|
|
643
|
+
self._escbuf = ch
|
|
644
|
+
return
|
|
561
645
|
if ch == BEL:
|
|
562
646
|
self._finish_osc(self._escbuf)
|
|
563
647
|
return
|
|
564
648
|
if self._escbuf[-1:] == ESC and ch == "\\":
|
|
565
649
|
self._finish_osc(self._escbuf[:-1])
|
|
566
650
|
return
|
|
651
|
+
if len(self._escbuf) >= _MAX_OSC_LENGTH:
|
|
652
|
+
self._skip(
|
|
653
|
+
"unknown", f"{ESC}]{self._escbuf}", f"OSC string of {_MAX_OSC_LENGTH} characters or more, truncated"
|
|
654
|
+
)
|
|
655
|
+
self._parsestate = 4
|
|
656
|
+
self._escbuf = ch
|
|
657
|
+
return
|
|
567
658
|
if self._escbuf.startswith("P") and len(self._escbuf) == 8:
|
|
568
659
|
self._finish_osc_skip("OSC palette set")
|
|
569
660
|
return
|
|
@@ -687,6 +778,8 @@ def sgi_params_to_attrspec(params: Sequence[int], previous: AttrSpec | None) ->
|
|
|
687
778
|
``vterm.TermCanvas.sgi_to_attrspec``. SGR 10/11/12 (which toggle vterm's charset/display-control state rather
|
|
688
779
|
than colour/attributes) are deliberately ignored here -- a caller that also needs that side effect applies it
|
|
689
780
|
itself; this function never raises for an unrecognised numeric code, it simply has no effect on the result.
|
|
781
|
+
An extended colour (``38``/``48``) whose index or component is outside ``0..255`` is consumed and ignored the same
|
|
782
|
+
way.
|
|
690
783
|
|
|
691
784
|
:param params: the SGR numeric parameters to apply, in encounter order.
|
|
692
785
|
:param previous: the :class:`~urwid.AttrSpec` in effect before ``params`` is applied, or ``None`` if none is
|
|
@@ -739,19 +832,21 @@ def sgi_params_to_attrspec(params: Sequence[int], previous: AttrSpec | None) ->
|
|
|
739
832
|
elif attr in (38, 48):
|
|
740
833
|
if idx + 2 < len(params) and params[idx + 1] == 5:
|
|
741
834
|
color = params[idx + 2]
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
835
|
+
if 0 <= color <= 255:
|
|
836
|
+
colors = max(256, colors)
|
|
837
|
+
if attr == 38:
|
|
838
|
+
fg = color
|
|
839
|
+
else:
|
|
840
|
+
bg = color
|
|
747
841
|
idx += 2
|
|
748
842
|
elif idx + 4 < len(params) and params[idx + 1] == 2:
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
843
|
+
if all(0 <= component <= 255 for component in params[idx + 2 : idx + 5]):
|
|
844
|
+
color = (params[idx + 2] << 16) + (params[idx + 3] << 8) + params[idx + 4]
|
|
845
|
+
colors = 16777216 # 2 ** 24
|
|
846
|
+
if attr == 38:
|
|
847
|
+
fg = color
|
|
848
|
+
else:
|
|
849
|
+
bg = color
|
|
755
850
|
idx += 4
|
|
756
851
|
elif attr == 39:
|
|
757
852
|
fg = None
|