urwid 2.6.15__py3-none-any.whl → 3.0.5__py3-none-any.whl
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/__init__.py +30 -20
- urwid/canvas.py +34 -53
- urwid/command_map.py +6 -4
- urwid/container.py +1 -1
- urwid/decoration.py +1 -1
- urwid/display/__init__.py +53 -48
- urwid/display/_posix_raw_display.py +20 -8
- urwid/display/_raw_display_base.py +21 -16
- urwid/display/_win32_raw_display.py +16 -17
- urwid/display/common.py +45 -74
- urwid/display/curses.py +3 -5
- urwid/display/escape.py +28 -13
- urwid/display/lcd.py +8 -10
- urwid/display/web.py +11 -16
- urwid/event_loop/asyncio_loop.py +35 -15
- urwid/event_loop/main_loop.py +18 -23
- urwid/event_loop/tornado_loop.py +4 -5
- urwid/event_loop/trio_loop.py +1 -1
- urwid/font.py +19 -22
- urwid/numedit.py +65 -65
- urwid/signals.py +19 -27
- urwid/split_repr.py +9 -3
- urwid/str_util.py +105 -60
- urwid/text_layout.py +14 -13
- urwid/util.py +8 -19
- urwid/version.py +22 -4
- urwid/vterm.py +20 -47
- urwid/widget/__init__.py +0 -6
- urwid/widget/attr_map.py +10 -10
- urwid/widget/attr_wrap.py +11 -13
- urwid/widget/bar_graph.py +3 -8
- urwid/widget/big_text.py +8 -9
- urwid/widget/box_adapter.py +6 -6
- urwid/widget/columns.py +52 -83
- urwid/widget/container.py +29 -75
- urwid/widget/divider.py +6 -6
- urwid/widget/edit.py +50 -50
- urwid/widget/filler.py +14 -14
- urwid/widget/frame.py +31 -40
- urwid/widget/grid_flow.py +25 -110
- urwid/widget/line_box.py +31 -18
- urwid/widget/listbox.py +16 -51
- urwid/widget/monitored_list.py +75 -49
- urwid/widget/overlay.py +4 -37
- urwid/widget/padding.py +31 -68
- urwid/widget/pile.py +179 -158
- urwid/widget/popup.py +2 -2
- urwid/widget/progress_bar.py +17 -18
- urwid/widget/scrollable.py +26 -34
- urwid/widget/solid_fill.py +3 -3
- urwid/widget/text.py +44 -30
- urwid/widget/treetools.py +27 -48
- urwid/widget/widget.py +13 -130
- urwid/widget/widget_decoration.py +6 -35
- urwid/widget/wimp.py +61 -61
- urwid/wimp.py +1 -1
- {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/METADATA +24 -24
- urwid-3.0.5.dist-info/RECORD +74 -0
- {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/WHEEL +1 -1
- urwid-2.6.15.dist-info/RECORD +0 -74
- {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info/licenses}/COPYING +0 -0
- {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/top_level.txt +0 -0
urwid/signals.py
CHANGED
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import abc
|
|
23
24
|
import itertools
|
|
24
25
|
import typing
|
|
25
|
-
import warnings
|
|
26
26
|
import weakref
|
|
27
27
|
|
|
28
28
|
if typing.TYPE_CHECKING:
|
|
29
29
|
from collections.abc import Callable, Collection, Container, Hashable, Iterable
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class MetaSignals(
|
|
32
|
+
class MetaSignals(abc.ABCMeta):
|
|
33
33
|
"""
|
|
34
34
|
register the list of signals in the class variable signals,
|
|
35
35
|
including signals in superclasses.
|
|
@@ -98,9 +98,9 @@ class Signals:
|
|
|
98
98
|
:type name: signal name
|
|
99
99
|
:param callback: the function to call when that signal is sent
|
|
100
100
|
:type callback: function
|
|
101
|
-
:param user_arg:
|
|
102
|
-
after the arguments passed when the signal is
|
|
103
|
-
|
|
101
|
+
:param user_arg: additional argument to callback
|
|
102
|
+
(appended after the arguments passed when the signal is emitted).
|
|
103
|
+
If None no arguments will be added.
|
|
104
104
|
Don't use this argument, use user_args instead.
|
|
105
105
|
:param weak_args: additional arguments passed to the callback
|
|
106
106
|
(before any arguments passed when the signal
|
|
@@ -132,11 +132,11 @@ class Signals:
|
|
|
132
132
|
As an example of using weak_args, consider the following snippet:
|
|
133
133
|
|
|
134
134
|
>>> import urwid
|
|
135
|
-
>>> debug = urwid.Text(
|
|
135
|
+
>>> debug = urwid.Text("")
|
|
136
136
|
>>> def handler(widget, newtext):
|
|
137
|
-
...
|
|
138
|
-
>>> edit = urwid.Edit(
|
|
139
|
-
>>> key = urwid.connect_signal(edit,
|
|
137
|
+
... debug.set_text("Edit widget changed to %s" % newtext)
|
|
138
|
+
>>> edit = urwid.Edit("")
|
|
139
|
+
>>> key = urwid.connect_signal(edit, "change", handler)
|
|
140
140
|
|
|
141
141
|
If you now build some interface using "edit" and "debug", the
|
|
142
142
|
"debug" widget will show whatever you type in the "edit" widget.
|
|
@@ -148,11 +148,11 @@ class Signals:
|
|
|
148
148
|
(it's not really a closure, since it doesn't reference any
|
|
149
149
|
outside variables, so it's just a dynamic function):
|
|
150
150
|
|
|
151
|
-
>>> debug = urwid.Text(
|
|
151
|
+
>>> debug = urwid.Text("")
|
|
152
152
|
>>> def handler(weak_debug, widget, newtext):
|
|
153
|
-
...
|
|
154
|
-
>>> edit = urwid.Edit(
|
|
155
|
-
>>> key = urwid.connect_signal(edit,
|
|
153
|
+
... weak_debug.set_text("Edit widget changed to %s" % newtext)
|
|
154
|
+
>>> edit = urwid.Edit("")
|
|
155
|
+
>>> key = urwid.connect_signal(edit, "change", handler, weak_args=[debug])
|
|
156
156
|
|
|
157
157
|
Here the weak_debug parameter in print_debug is the value passed
|
|
158
158
|
in the weak_args list to connect_signal. Note that the
|
|
@@ -166,12 +166,6 @@ class Signals:
|
|
|
166
166
|
handler can also be disconnected by calling
|
|
167
167
|
urwid.disconnect_signal, which doesn't need this key.
|
|
168
168
|
"""
|
|
169
|
-
if user_arg is not None:
|
|
170
|
-
warnings.warn(
|
|
171
|
-
"Don't use user_arg argument, use user_args instead.",
|
|
172
|
-
DeprecationWarning,
|
|
173
|
-
stacklevel=2,
|
|
174
|
-
)
|
|
175
169
|
|
|
176
170
|
sig_cls = obj.__class__
|
|
177
171
|
if name not in self._supported.get(sig_cls, ()):
|
|
@@ -194,9 +188,8 @@ class Signals:
|
|
|
194
188
|
# their callbacks) from existing.
|
|
195
189
|
obj_weak = weakref.ref(obj)
|
|
196
190
|
|
|
197
|
-
def weakref_callback(weakref)
|
|
198
|
-
o
|
|
199
|
-
if o:
|
|
191
|
+
def weakref_callback(_ref: weakref.ReferenceType[typing.Any]) -> None:
|
|
192
|
+
if o := obj_weak():
|
|
200
193
|
self.disconnect_by_key(o, name, key)
|
|
201
194
|
|
|
202
195
|
user_args = self._prepare_user_args(weak_args, user_args, weakref_callback)
|
|
@@ -208,7 +201,7 @@ class Signals:
|
|
|
208
201
|
self,
|
|
209
202
|
weak_args: Iterable[typing.Any] = (),
|
|
210
203
|
user_args: Iterable[typing.Any] = (),
|
|
211
|
-
callback: Callable[
|
|
204
|
+
callback: Callable[[weakref.ReferenceType[typing.Any]], typing.Any] | None = None,
|
|
212
205
|
) -> tuple[Collection[weakref.ReferenceType], Collection[typing.Any]]:
|
|
213
206
|
# Turn weak_args into weakrefs and prepend them to user_args
|
|
214
207
|
w_args = tuple(weakref.ref(w_arg, callback) for w_arg in weak_args)
|
|
@@ -302,14 +295,13 @@ class Signals:
|
|
|
302
295
|
self,
|
|
303
296
|
callback,
|
|
304
297
|
user_arg: typing.Any,
|
|
305
|
-
weak_args:
|
|
306
|
-
user_args:
|
|
298
|
+
weak_args: Iterable[weakref.ReferenceType],
|
|
299
|
+
user_args: Iterable[typing.Any],
|
|
307
300
|
emit_args: Iterable[typing.Any],
|
|
308
301
|
) -> bool:
|
|
309
302
|
args_to_pass = []
|
|
310
303
|
for w_arg in weak_args:
|
|
311
|
-
real_arg
|
|
312
|
-
if real_arg is not None:
|
|
304
|
+
if (real_arg := w_arg()) is not None:
|
|
313
305
|
args_to_pass.append(real_arg)
|
|
314
306
|
else:
|
|
315
307
|
# de-referenced
|
urwid/split_repr.py
CHANGED
|
@@ -32,15 +32,18 @@ def split_repr(self):
|
|
|
32
32
|
|
|
33
33
|
>>> class Foo(object):
|
|
34
34
|
... __repr__ = split_repr
|
|
35
|
+
...
|
|
35
36
|
... def _repr_words(self):
|
|
36
37
|
... return ["words", "here"]
|
|
38
|
+
...
|
|
37
39
|
... def _repr_attrs(self):
|
|
38
|
-
... return {
|
|
40
|
+
... return {"attrs": "appear too"}
|
|
39
41
|
>>> Foo()
|
|
40
42
|
<Foo words here attrs='appear too'>
|
|
41
43
|
>>> class Bar(Foo):
|
|
42
44
|
... def _repr_words(self):
|
|
43
45
|
... return Foo._repr_words(self) + ["too"]
|
|
46
|
+
...
|
|
44
47
|
... def _repr_attrs(self):
|
|
45
48
|
... return dict(Foo._repr_attrs(self), barttr=42)
|
|
46
49
|
>>> Bar()
|
|
@@ -62,9 +65,9 @@ def normalize_repr(v):
|
|
|
62
65
|
"""
|
|
63
66
|
Return dictionary repr sorted by keys, leave others unchanged
|
|
64
67
|
|
|
65
|
-
>>> normalize_repr({1:2,3:4,5:6,7:8})
|
|
68
|
+
>>> normalize_repr({1: 2, 3: 4, 5: 6, 7: 8})
|
|
66
69
|
'{1: 2, 3: 4, 5: 6, 7: 8}'
|
|
67
|
-
>>> normalize_repr(
|
|
70
|
+
>>> normalize_repr("foo")
|
|
68
71
|
"'foo'"
|
|
69
72
|
"""
|
|
70
73
|
if isinstance(v, dict):
|
|
@@ -88,9 +91,12 @@ def remove_defaults(d, fn):
|
|
|
88
91
|
>>> class Foo(object):
|
|
89
92
|
... def __init__(self, a=1, b=2):
|
|
90
93
|
... self.values = a, b
|
|
94
|
+
...
|
|
91
95
|
... __repr__ = split_repr
|
|
96
|
+
...
|
|
92
97
|
... def _repr_words(self):
|
|
93
98
|
... return ["object"]
|
|
99
|
+
...
|
|
94
100
|
... def _repr_attrs(self):
|
|
95
101
|
... d = dict(a=self.values[0], b=self.values[1])
|
|
96
102
|
... return remove_defaults(d, Foo.__init__)
|
urwid/str_util.py
CHANGED
|
@@ -29,22 +29,61 @@ import wcwidth
|
|
|
29
29
|
if typing.TYPE_CHECKING:
|
|
30
30
|
from typing_extensions import Literal
|
|
31
31
|
|
|
32
|
-
SAFE_ASCII_RE = re.compile("^[ -~]*$")
|
|
33
|
-
SAFE_ASCII_BYTES_RE = re.compile(
|
|
32
|
+
SAFE_ASCII_RE = re.compile(r"^[ -~]*$")
|
|
33
|
+
SAFE_ASCII_BYTES_RE = re.compile(rb"^[ -~]*$")
|
|
34
34
|
|
|
35
35
|
_byte_encoding: Literal["utf8", "narrow", "wide"] = "narrow"
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def get_char_width(char: str) -> Literal[0, 1, 2]:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
"""
|
|
40
|
+
Return the screen column width for a single character.
|
|
41
|
+
|
|
42
|
+
.. deprecated:: 3.0.4
|
|
43
|
+
"""
|
|
44
|
+
warnings.warn(
|
|
45
|
+
"get_char_width is deprecated in favor of wcwidth.width",
|
|
46
|
+
DeprecationWarning,
|
|
47
|
+
stacklevel=2,
|
|
48
|
+
)
|
|
49
|
+
if (width := wcwidth.wcwidth(char)) >= 0:
|
|
50
|
+
return width
|
|
51
|
+
|
|
52
|
+
return 0
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
def get_width(o: int) -> Literal[0, 1, 2]:
|
|
46
|
-
"""
|
|
47
|
-
|
|
56
|
+
"""
|
|
57
|
+
Return the screen column width for unicode ordinal o.
|
|
58
|
+
|
|
59
|
+
.. deprecated:: 3.0.4
|
|
60
|
+
"""
|
|
61
|
+
warnings.warn(
|
|
62
|
+
"get_width is deprecated in favor of wcwidth.width",
|
|
63
|
+
DeprecationWarning,
|
|
64
|
+
stacklevel=2,
|
|
65
|
+
)
|
|
66
|
+
if (width := wcwidth.wcwidth(chr(o))) >= 0:
|
|
67
|
+
return width
|
|
68
|
+
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _decode_grapheme_at(text: bytes, start: int, end: int) -> tuple[str, int]:
|
|
73
|
+
"""
|
|
74
|
+
Decode bytes starting at `start` to get the first grapheme cluster.
|
|
75
|
+
|
|
76
|
+
:param text: UTF-8 encoded bytes
|
|
77
|
+
:param start: starting byte position
|
|
78
|
+
:param end: ending byte position
|
|
79
|
+
:returns: (grapheme_string, next_byte_position)
|
|
80
|
+
|
|
81
|
+
Assumes caller provides valid UTF-8 byte boundaries.
|
|
82
|
+
"""
|
|
83
|
+
decoded = text[start:end].decode("utf-8")
|
|
84
|
+
grapheme = next(wcwidth.iter_graphemes(decoded), "")
|
|
85
|
+
grapheme_bytes = grapheme.encode("utf-8")
|
|
86
|
+
return grapheme, start + len(grapheme_bytes)
|
|
48
87
|
|
|
49
88
|
|
|
50
89
|
def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
|
|
@@ -86,10 +125,9 @@ def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
|
|
|
86
125
|
if b1 & 0xE0 == 0xC0:
|
|
87
126
|
if b2 & 0xC0 != 0x80:
|
|
88
127
|
return error
|
|
89
|
-
o
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return o, pos + 2
|
|
128
|
+
if (o := ((b1 & 0x1F) << 6) | (b2 & 0x3F)) >= 0x80:
|
|
129
|
+
return o, pos + 2
|
|
130
|
+
return error
|
|
93
131
|
if lt < 3:
|
|
94
132
|
return error
|
|
95
133
|
if b1 & 0xF0 == 0xE0:
|
|
@@ -97,10 +135,9 @@ def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
|
|
|
97
135
|
return error
|
|
98
136
|
if b3 & 0xC0 != 0x80:
|
|
99
137
|
return error
|
|
100
|
-
o
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return o, pos + 3
|
|
138
|
+
if (o := ((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)) >= 0x800:
|
|
139
|
+
return o, pos + 3
|
|
140
|
+
return error
|
|
104
141
|
if lt < 4:
|
|
105
142
|
return error
|
|
106
143
|
if b1 & 0xF8 == 0xF0:
|
|
@@ -110,10 +147,9 @@ def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
|
|
|
110
147
|
return error
|
|
111
148
|
if b4 & 0xC0 != 0x80:
|
|
112
149
|
return error
|
|
113
|
-
o
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return o, pos + 4
|
|
150
|
+
if (o := ((b1 & 0x07) << 18) | ((b2 & 0x3F) << 12) | ((b3 & 0x3F) << 6) | (b4 & 0x3F)) >= 0x10000:
|
|
151
|
+
return o, pos + 4
|
|
152
|
+
return error
|
|
117
153
|
return error
|
|
118
154
|
|
|
119
155
|
|
|
@@ -160,23 +196,26 @@ def calc_string_text_pos(text: str, start_offs: int, end_offs: int, pref_col: in
|
|
|
160
196
|
where start_offs is the offset into text assumed to be screen column 0
|
|
161
197
|
and end_offs is the end of the range to search.
|
|
162
198
|
|
|
199
|
+
Iterates by grapheme clusters for emoji ZWJ sequences, flags,
|
|
200
|
+
combining characters, and other multi-codepoint unicode sequences.
|
|
201
|
+
|
|
163
202
|
:param text: string
|
|
164
203
|
:param start_offs: starting text position
|
|
165
204
|
:param end_offs: ending text position
|
|
166
205
|
:param pref_col: target column
|
|
167
206
|
:returns: (position, actual_col)
|
|
168
|
-
|
|
169
|
-
..note:: this method is a simplified version of `wcwidth.wcswidth` and ideally should be in wcwidth package.
|
|
170
207
|
"""
|
|
171
208
|
if start_offs > end_offs:
|
|
172
209
|
raise ValueError((start_offs, end_offs))
|
|
173
210
|
|
|
174
211
|
cols = 0
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
212
|
+
pos = start_offs
|
|
213
|
+
for grapheme in wcwidth.iter_graphemes(text[start_offs:end_offs]):
|
|
214
|
+
grapheme_width = wcwidth.width(grapheme, control_codes="ignore")
|
|
215
|
+
if grapheme_width + cols > pref_col:
|
|
216
|
+
return pos, cols
|
|
217
|
+
cols += grapheme_width
|
|
218
|
+
pos += len(grapheme)
|
|
180
219
|
|
|
181
220
|
return end_offs, cols
|
|
182
221
|
|
|
@@ -201,16 +240,10 @@ def calc_text_pos(text: str | bytes, start_offs: int, end_offs: int, pref_col: i
|
|
|
201
240
|
raise TypeError(text)
|
|
202
241
|
|
|
203
242
|
if _byte_encoding == "utf8":
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
w = get_width(o)
|
|
209
|
-
if w + sc > pref_col:
|
|
210
|
-
return i, sc
|
|
211
|
-
i = n
|
|
212
|
-
sc += w
|
|
213
|
-
return i, sc
|
|
243
|
+
decoded = text[start_offs:end_offs].decode("utf-8")
|
|
244
|
+
str_pos, cols = calc_string_text_pos(decoded, 0, len(decoded), pref_col)
|
|
245
|
+
byte_offset = len(decoded[:str_pos].encode("utf-8"))
|
|
246
|
+
return start_offs + byte_offset, cols
|
|
214
247
|
|
|
215
248
|
# "wide" and "narrow"
|
|
216
249
|
i = start_offs + pref_col
|
|
@@ -228,19 +261,20 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
228
261
|
text may be unicode or a byte string in the target _byte_encoding
|
|
229
262
|
|
|
230
263
|
Some characters are wide (take two columns) and others affect the
|
|
231
|
-
previous character (take zero columns)
|
|
232
|
-
|
|
264
|
+
previous character (take zero columns), while others are grouped
|
|
265
|
+
in sequence by "grapheme boundaries" (Emoji, Skin tones, flags, etc).
|
|
233
266
|
"""
|
|
234
267
|
|
|
235
268
|
if start_offs > end_offs:
|
|
236
|
-
|
|
269
|
+
msg = f"{start_offs=} > {end_offs=}"
|
|
270
|
+
raise ValueError(msg)
|
|
237
271
|
|
|
238
272
|
if isinstance(text, str):
|
|
239
|
-
return
|
|
273
|
+
return wcwidth.width(text[start_offs:end_offs], control_codes="ignore")
|
|
240
274
|
|
|
241
275
|
if _byte_encoding == "utf8":
|
|
242
276
|
try:
|
|
243
|
-
return
|
|
277
|
+
return wcwidth.width(text[start_offs:end_offs].decode("utf-8"), control_codes="ignore")
|
|
244
278
|
except UnicodeDecodeError as exc:
|
|
245
279
|
warnings.warn(
|
|
246
280
|
"`calc_width` with text encoded to bytes can produce incorrect results"
|
|
@@ -253,8 +287,8 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
253
287
|
sc = 0
|
|
254
288
|
while i < end_offs:
|
|
255
289
|
o, i = decode_one(text, i)
|
|
256
|
-
w
|
|
257
|
-
|
|
290
|
+
if (w := wcwidth.wcwidth(chr(o))) > 0:
|
|
291
|
+
sc += w
|
|
258
292
|
return sc
|
|
259
293
|
# "wide", "narrow" or all printable ASCII, just return the character count
|
|
260
294
|
return end_offs - start_offs
|
|
@@ -262,17 +296,22 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
262
296
|
|
|
263
297
|
def is_wide_char(text: str | bytes, offs: int) -> bool:
|
|
264
298
|
"""
|
|
265
|
-
Test if the
|
|
299
|
+
Test if the grapheme cluster at offs within text is wide (2 columns).
|
|
300
|
+
|
|
301
|
+
For Unicode strings, extracts the full grapheme cluster starting at offs
|
|
302
|
+
and checks if it renders as wide. This correctly handles multi-codepoint
|
|
303
|
+
graphemes like emoji ZWJ sequences and flags.
|
|
266
304
|
|
|
267
305
|
text may be unicode or a byte string in the target _byte_encoding
|
|
268
306
|
"""
|
|
269
307
|
if isinstance(text, str):
|
|
270
|
-
|
|
308
|
+
grapheme = next(wcwidth.iter_graphemes(text[offs:]))
|
|
309
|
+
return wcwidth.width(grapheme, control_codes="ignore") == 2
|
|
271
310
|
if not isinstance(text, bytes):
|
|
272
311
|
raise TypeError(text)
|
|
273
312
|
if _byte_encoding == "utf8":
|
|
274
|
-
|
|
275
|
-
return
|
|
313
|
+
grapheme, _ = _decode_grapheme_at(text, offs, len(text))
|
|
314
|
+
return wcwidth.width(grapheme, control_codes="ignore") == 2
|
|
276
315
|
if _byte_encoding == "wide":
|
|
277
316
|
return within_double_byte(text, offs, offs) == 1
|
|
278
317
|
return False
|
|
@@ -280,19 +319,23 @@ def is_wide_char(text: str | bytes, offs: int) -> bool:
|
|
|
280
319
|
|
|
281
320
|
def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
282
321
|
"""
|
|
283
|
-
Return the position of the
|
|
322
|
+
Return the position of the grapheme cluster before end_offs.
|
|
323
|
+
|
|
324
|
+
For Unicode strings, handle multi-codepoint, "grapheme clusters",
|
|
325
|
+
to better measure emoji ZWJ, flags, combining characters, skin tones.
|
|
284
326
|
"""
|
|
285
327
|
if start_offs >= end_offs:
|
|
286
328
|
raise ValueError((start_offs, end_offs))
|
|
287
329
|
if isinstance(text, str):
|
|
288
|
-
return end_offs
|
|
330
|
+
return wcwidth.grapheme_boundary_before(text, end_offs)
|
|
289
331
|
if not isinstance(text, bytes):
|
|
290
332
|
raise TypeError(text)
|
|
291
333
|
if _byte_encoding == "utf8":
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
334
|
+
decoded = text[start_offs:end_offs].decode("utf-8")
|
|
335
|
+
str_pos = len(decoded)
|
|
336
|
+
prev_str_pos = wcwidth.grapheme_boundary_before(decoded, str_pos)
|
|
337
|
+
prefix = decoded[:prev_str_pos]
|
|
338
|
+
return start_offs + len(prefix.encode("utf-8"))
|
|
296
339
|
if _byte_encoding == "wide" and within_double_byte(text, start_offs, end_offs - 1) == 2:
|
|
297
340
|
return end_offs - 2
|
|
298
341
|
return end_offs - 1
|
|
@@ -300,19 +343,21 @@ def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
300
343
|
|
|
301
344
|
def move_next_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
302
345
|
"""
|
|
303
|
-
Return the position of the
|
|
346
|
+
Return the position of the next grapheme cluster after start_offs.
|
|
347
|
+
|
|
348
|
+
For Unicode strings, handle multi-codepoint, "grapheme clusters",
|
|
349
|
+
to better measure emoji ZWJ, flags, combining characters, skin tones.
|
|
304
350
|
"""
|
|
305
351
|
if start_offs >= end_offs:
|
|
306
352
|
raise ValueError((start_offs, end_offs))
|
|
307
353
|
if isinstance(text, str):
|
|
308
|
-
|
|
354
|
+
grapheme = next(wcwidth.iter_graphemes(text[start_offs:end_offs]))
|
|
355
|
+
return start_offs + len(grapheme)
|
|
309
356
|
if not isinstance(text, bytes):
|
|
310
357
|
raise TypeError(text)
|
|
311
358
|
if _byte_encoding == "utf8":
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
o += 1
|
|
315
|
-
return o
|
|
359
|
+
_, next_pos = _decode_grapheme_at(text, start_offs, end_offs)
|
|
360
|
+
return next_pos
|
|
316
361
|
if _byte_encoding == "wide" and within_double_byte(text, start_offs, start_offs) == 1:
|
|
317
362
|
return start_offs + 2
|
|
318
363
|
return start_offs + 1
|
urwid/text_layout.py
CHANGED
|
@@ -23,7 +23,9 @@ from __future__ import annotations
|
|
|
23
23
|
import functools
|
|
24
24
|
import typing
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
import wcwidth
|
|
27
|
+
|
|
28
|
+
from urwid.str_util import calc_text_pos, calc_width, is_wide_char, move_next_char, move_prev_char
|
|
27
29
|
from urwid.util import calc_trim_text, get_encoding
|
|
28
30
|
|
|
29
31
|
if typing.TYPE_CHECKING:
|
|
@@ -44,7 +46,7 @@ def get_ellipsis_string(encoding: str) -> str:
|
|
|
44
46
|
@functools.lru_cache(maxsize=4)
|
|
45
47
|
def _get_width(string) -> int:
|
|
46
48
|
"""Get ellipsis character width for given encoding."""
|
|
47
|
-
return
|
|
49
|
+
return wcwidth.width(string, control_codes="ignore")
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class TextLayout:
|
|
@@ -176,7 +178,7 @@ class StandardTextLayout(TextLayout):
|
|
|
176
178
|
self,
|
|
177
179
|
text: str | bytes,
|
|
178
180
|
width: int,
|
|
179
|
-
wrap: Literal["
|
|
181
|
+
wrap: Literal["clip", "ellipsis", WrapMode.CLIP, WrapMode.ELLIPSIS],
|
|
180
182
|
) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
|
|
181
183
|
"""Calculate text segments for cases of a text trimmed (wrap is clip or ellipsis)."""
|
|
182
184
|
segments = []
|
|
@@ -185,8 +187,8 @@ class StandardTextLayout(TextLayout):
|
|
|
185
187
|
encoding = get_encoding()
|
|
186
188
|
ellipsis_string = get_ellipsis_string(encoding)
|
|
187
189
|
ellipsis_width = _get_width(ellipsis_string)
|
|
188
|
-
|
|
189
|
-
ellipsis_string = ellipsis_string[
|
|
190
|
+
if (extra := width - ellipsis_width - 1) < 0:
|
|
191
|
+
ellipsis_string = ellipsis_string[:extra]
|
|
190
192
|
ellipsis_width = _get_width(ellipsis_string)
|
|
191
193
|
|
|
192
194
|
ellipsis_char = ellipsis_string.encode(encoding)
|
|
@@ -236,7 +238,7 @@ class StandardTextLayout(TextLayout):
|
|
|
236
238
|
"""
|
|
237
239
|
Calculate the segments of text to display given width screen columns to display them.
|
|
238
240
|
|
|
239
|
-
text -
|
|
241
|
+
text - Unicode text or byte string to display
|
|
240
242
|
width - number of available screen columns
|
|
241
243
|
wrap - wrapping mode used
|
|
242
244
|
|
|
@@ -461,7 +463,7 @@ def shift_line(
|
|
|
461
463
|
# existing shift
|
|
462
464
|
amount += segs[0][0]
|
|
463
465
|
if amount:
|
|
464
|
-
return [(amount, None)
|
|
466
|
+
return [(amount, None), *segs[1:]]
|
|
465
467
|
return segs[1:]
|
|
466
468
|
|
|
467
469
|
if amount:
|
|
@@ -581,8 +583,7 @@ def calc_pos(
|
|
|
581
583
|
if row < 0 or row >= len(layout):
|
|
582
584
|
raise ValueError("calculate_pos: out of layout row range")
|
|
583
585
|
|
|
584
|
-
pos
|
|
585
|
-
if pos is not None:
|
|
586
|
+
if (pos := calc_line_pos(text, layout[row], pref_col)) is not None:
|
|
586
587
|
return pos
|
|
587
588
|
|
|
588
589
|
rows_above = list(range(row - 1, -1, -1))
|
|
@@ -590,14 +591,14 @@ def calc_pos(
|
|
|
590
591
|
while rows_above and rows_below:
|
|
591
592
|
if rows_above:
|
|
592
593
|
r = rows_above.pop(0)
|
|
593
|
-
pos
|
|
594
|
-
if pos is not None:
|
|
594
|
+
if (pos := calc_line_pos(text, layout[r], pref_col)) is not None:
|
|
595
595
|
return pos
|
|
596
|
+
|
|
596
597
|
if rows_below:
|
|
597
598
|
r = rows_below.pop(0)
|
|
598
|
-
pos
|
|
599
|
-
if pos is not None:
|
|
599
|
+
if (pos := calc_line_pos(text, layout[r], pref_col)) is not None:
|
|
600
600
|
return pos
|
|
601
|
+
|
|
601
602
|
return 0
|
|
602
603
|
|
|
603
604
|
|
urwid/util.py
CHANGED
|
@@ -143,7 +143,7 @@ def get_encoding() -> str:
|
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
@contextlib.contextmanager
|
|
146
|
-
def set_temporary_encoding(encoding_name: str) -> Generator[None
|
|
146
|
+
def set_temporary_encoding(encoding_name: str) -> Generator[None]:
|
|
147
147
|
"""Internal helper for encoding specific validation in unittests/doctests.
|
|
148
148
|
|
|
149
149
|
Not exported globally.
|
|
@@ -493,24 +493,13 @@ def is_mouse_press(ev: str) -> bool:
|
|
|
493
493
|
|
|
494
494
|
|
|
495
495
|
class MetaSuper(type):
|
|
496
|
-
"""
|
|
496
|
+
"""Deprecated metaclass.
|
|
497
497
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
raise AttributeError("Class has same name as one of its super classes")
|
|
502
|
-
|
|
503
|
-
@property
|
|
504
|
-
def _super(self):
|
|
505
|
-
warnings.warn(
|
|
506
|
-
f"`{name}.__super` was a deprecated feature for old python versions."
|
|
507
|
-
f"Please use `super()` call instead.",
|
|
508
|
-
DeprecationWarning,
|
|
509
|
-
stacklevel=3,
|
|
510
|
-
)
|
|
511
|
-
return super(cls, self)
|
|
498
|
+
Present only for code compatibility, all logic has been removed.
|
|
499
|
+
Please move to the last position in the class bases to allow future changes.
|
|
500
|
+
"""
|
|
512
501
|
|
|
513
|
-
|
|
502
|
+
__slots__ = ()
|
|
514
503
|
|
|
515
504
|
|
|
516
505
|
def int_scale(val: int, val_range: int, out_range: int) -> int:
|
|
@@ -521,7 +510,7 @@ def int_scale(val: int, val_range: int, out_range: int) -> int:
|
|
|
521
510
|
|
|
522
511
|
>>> "%x" % int_scale(0x7, 0x10, 0x10000)
|
|
523
512
|
'7777'
|
|
524
|
-
>>> "%x" % int_scale(
|
|
513
|
+
>>> "%x" % int_scale(0x5F, 0x100, 0x10)
|
|
525
514
|
'6'
|
|
526
515
|
>>> int_scale(2, 6, 101)
|
|
527
516
|
40
|
|
@@ -534,7 +523,7 @@ def int_scale(val: int, val_range: int, out_range: int) -> int:
|
|
|
534
523
|
return num // dem
|
|
535
524
|
|
|
536
525
|
|
|
537
|
-
class StoppingContext(
|
|
526
|
+
class StoppingContext(contextlib.AbstractContextManager["StoppingContext"]):
|
|
538
527
|
"""Context manager that calls ``stop`` on a given object on exit. Used to
|
|
539
528
|
make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as
|
|
540
529
|
context managers.
|
urwid/version.py
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
3
13
|
TYPE_CHECKING = False
|
|
4
14
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
6
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
7
20
|
else:
|
|
8
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
9
23
|
|
|
10
24
|
version: str
|
|
11
25
|
__version__: str
|
|
12
26
|
__version_tuple__: VERSION_TUPLE
|
|
13
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '3.0.5'
|
|
32
|
+
__version_tuple__ = version_tuple = (3, 0, 5)
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
__version_tuple__ = version_tuple = (2, 6, 15)
|
|
34
|
+
__commit_id__ = commit_id = None
|