urwid 3.0.3__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 +29 -16
- urwid/display/__init__.py +53 -48
- urwid/display/_raw_display_base.py +2 -2
- urwid/display/common.py +17 -17
- urwid/display/curses.py +1 -3
- urwid/display/web.py +1 -1
- urwid/event_loop/asyncio_loop.py +3 -1
- urwid/event_loop/main_loop.py +5 -5
- urwid/font.py +6 -4
- urwid/numedit.py +65 -65
- urwid/signals.py +17 -26
- urwid/split_repr.py +9 -3
- urwid/str_util.py +90 -42
- urwid/text_layout.py +8 -6
- urwid/util.py +1 -1
- urwid/version.py +2 -2
- urwid/vterm.py +2 -2
- urwid/widget/attr_map.py +10 -10
- urwid/widget/attr_wrap.py +3 -3
- urwid/widget/big_text.py +0 -3
- urwid/widget/box_adapter.py +2 -2
- urwid/widget/divider.py +6 -6
- urwid/widget/edit.py +42 -42
- urwid/widget/filler.py +8 -8
- urwid/widget/frame.py +3 -3
- urwid/widget/line_box.py +19 -19
- urwid/widget/listbox.py +1 -1
- urwid/widget/monitored_list.py +69 -45
- urwid/widget/padding.py +20 -20
- urwid/widget/progress_bar.py +7 -7
- urwid/widget/solid_fill.py +3 -3
- urwid/widget/text.py +44 -30
- urwid/widget/widget.py +6 -6
- urwid/widget/widget_decoration.py +2 -2
- urwid/widget/wimp.py +61 -61
- {urwid-3.0.3.dist-info → urwid-3.0.5.dist-info}/METADATA +6 -5
- urwid-3.0.5.dist-info/RECORD +74 -0
- {urwid-3.0.3.dist-info → urwid-3.0.5.dist-info}/WHEEL +1 -1
- urwid-3.0.3.dist-info/RECORD +0 -74
- {urwid-3.0.3.dist-info → urwid-3.0.5.dist-info}/licenses/COPYING +0 -0
- {urwid-3.0.3.dist-info → urwid-3.0.5.dist-info}/top_level.txt +0 -0
urwid/signals.py
CHANGED
|
@@ -23,7 +23,6 @@ from __future__ import annotations
|
|
|
23
23
|
import abc
|
|
24
24
|
import itertools
|
|
25
25
|
import typing
|
|
26
|
-
import warnings
|
|
27
26
|
import weakref
|
|
28
27
|
|
|
29
28
|
if typing.TYPE_CHECKING:
|
|
@@ -99,9 +98,9 @@ class Signals:
|
|
|
99
98
|
:type name: signal name
|
|
100
99
|
:param callback: the function to call when that signal is sent
|
|
101
100
|
:type callback: function
|
|
102
|
-
:param user_arg:
|
|
103
|
-
after the arguments passed when the signal is
|
|
104
|
-
|
|
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.
|
|
105
104
|
Don't use this argument, use user_args instead.
|
|
106
105
|
:param weak_args: additional arguments passed to the callback
|
|
107
106
|
(before any arguments passed when the signal
|
|
@@ -133,11 +132,11 @@ class Signals:
|
|
|
133
132
|
As an example of using weak_args, consider the following snippet:
|
|
134
133
|
|
|
135
134
|
>>> import urwid
|
|
136
|
-
>>> debug = urwid.Text(
|
|
135
|
+
>>> debug = urwid.Text("")
|
|
137
136
|
>>> def handler(widget, newtext):
|
|
138
|
-
...
|
|
139
|
-
>>> edit = urwid.Edit(
|
|
140
|
-
>>> 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)
|
|
141
140
|
|
|
142
141
|
If you now build some interface using "edit" and "debug", the
|
|
143
142
|
"debug" widget will show whatever you type in the "edit" widget.
|
|
@@ -149,11 +148,11 @@ class Signals:
|
|
|
149
148
|
(it's not really a closure, since it doesn't reference any
|
|
150
149
|
outside variables, so it's just a dynamic function):
|
|
151
150
|
|
|
152
|
-
>>> debug = urwid.Text(
|
|
151
|
+
>>> debug = urwid.Text("")
|
|
153
152
|
>>> def handler(weak_debug, widget, newtext):
|
|
154
|
-
...
|
|
155
|
-
>>> edit = urwid.Edit(
|
|
156
|
-
>>> 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])
|
|
157
156
|
|
|
158
157
|
Here the weak_debug parameter in print_debug is the value passed
|
|
159
158
|
in the weak_args list to connect_signal. Note that the
|
|
@@ -167,12 +166,6 @@ class Signals:
|
|
|
167
166
|
handler can also be disconnected by calling
|
|
168
167
|
urwid.disconnect_signal, which doesn't need this key.
|
|
169
168
|
"""
|
|
170
|
-
if user_arg is not None:
|
|
171
|
-
warnings.warn(
|
|
172
|
-
"Don't use user_arg argument, use user_args instead.",
|
|
173
|
-
DeprecationWarning,
|
|
174
|
-
stacklevel=2,
|
|
175
|
-
)
|
|
176
169
|
|
|
177
170
|
sig_cls = obj.__class__
|
|
178
171
|
if name not in self._supported.get(sig_cls, ()):
|
|
@@ -195,9 +188,8 @@ class Signals:
|
|
|
195
188
|
# their callbacks) from existing.
|
|
196
189
|
obj_weak = weakref.ref(obj)
|
|
197
190
|
|
|
198
|
-
def weakref_callback(weakref)
|
|
199
|
-
o
|
|
200
|
-
if o:
|
|
191
|
+
def weakref_callback(_ref: weakref.ReferenceType[typing.Any]) -> None:
|
|
192
|
+
if o := obj_weak():
|
|
201
193
|
self.disconnect_by_key(o, name, key)
|
|
202
194
|
|
|
203
195
|
user_args = self._prepare_user_args(weak_args, user_args, weakref_callback)
|
|
@@ -209,7 +201,7 @@ class Signals:
|
|
|
209
201
|
self,
|
|
210
202
|
weak_args: Iterable[typing.Any] = (),
|
|
211
203
|
user_args: Iterable[typing.Any] = (),
|
|
212
|
-
callback: Callable[
|
|
204
|
+
callback: Callable[[weakref.ReferenceType[typing.Any]], typing.Any] | None = None,
|
|
213
205
|
) -> tuple[Collection[weakref.ReferenceType], Collection[typing.Any]]:
|
|
214
206
|
# Turn weak_args into weakrefs and prepend them to user_args
|
|
215
207
|
w_args = tuple(weakref.ref(w_arg, callback) for w_arg in weak_args)
|
|
@@ -303,14 +295,13 @@ class Signals:
|
|
|
303
295
|
self,
|
|
304
296
|
callback,
|
|
305
297
|
user_arg: typing.Any,
|
|
306
|
-
weak_args:
|
|
307
|
-
user_args:
|
|
298
|
+
weak_args: Iterable[weakref.ReferenceType],
|
|
299
|
+
user_args: Iterable[typing.Any],
|
|
308
300
|
emit_args: Iterable[typing.Any],
|
|
309
301
|
) -> bool:
|
|
310
302
|
args_to_pass = []
|
|
311
303
|
for w_arg in weak_args:
|
|
312
|
-
real_arg
|
|
313
|
-
if real_arg is not None:
|
|
304
|
+
if (real_arg := w_arg()) is not None:
|
|
314
305
|
args_to_pass.append(real_arg)
|
|
315
306
|
else:
|
|
316
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
|
@@ -36,6 +36,16 @@ _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
|
+
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
|
+
)
|
|
39
49
|
if (width := wcwidth.wcwidth(char)) >= 0:
|
|
40
50
|
return width
|
|
41
51
|
|
|
@@ -43,8 +53,37 @@ def get_char_width(char: str) -> Literal[0, 1, 2]:
|
|
|
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]:
|
|
@@ -157,23 +196,26 @@ def calc_string_text_pos(text: str, start_offs: int, end_offs: int, pref_col: in
|
|
|
157
196
|
where start_offs is the offset into text assumed to be screen column 0
|
|
158
197
|
and end_offs is the end of the range to search.
|
|
159
198
|
|
|
199
|
+
Iterates by grapheme clusters for emoji ZWJ sequences, flags,
|
|
200
|
+
combining characters, and other multi-codepoint unicode sequences.
|
|
201
|
+
|
|
160
202
|
:param text: string
|
|
161
203
|
:param start_offs: starting text position
|
|
162
204
|
:param end_offs: ending text position
|
|
163
205
|
:param pref_col: target column
|
|
164
206
|
:returns: (position, actual_col)
|
|
165
|
-
|
|
166
|
-
..note:: this method is a simplified version of `wcwidth.wcswidth` and ideally should be in wcwidth package.
|
|
167
207
|
"""
|
|
168
208
|
if start_offs > end_offs:
|
|
169
209
|
raise ValueError((start_offs, end_offs))
|
|
170
210
|
|
|
171
211
|
cols = 0
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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)
|
|
177
219
|
|
|
178
220
|
return end_offs, cols
|
|
179
221
|
|
|
@@ -198,16 +240,10 @@ def calc_text_pos(text: str | bytes, start_offs: int, end_offs: int, pref_col: i
|
|
|
198
240
|
raise TypeError(text)
|
|
199
241
|
|
|
200
242
|
if _byte_encoding == "utf8":
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
w = get_width(o)
|
|
206
|
-
if w + sc > pref_col:
|
|
207
|
-
return i, sc
|
|
208
|
-
i = n
|
|
209
|
-
sc += w
|
|
210
|
-
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
|
|
211
247
|
|
|
212
248
|
# "wide" and "narrow"
|
|
213
249
|
i = start_offs + pref_col
|
|
@@ -225,19 +261,20 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
225
261
|
text may be unicode or a byte string in the target _byte_encoding
|
|
226
262
|
|
|
227
263
|
Some characters are wide (take two columns) and others affect the
|
|
228
|
-
previous character (take zero columns)
|
|
229
|
-
|
|
264
|
+
previous character (take zero columns), while others are grouped
|
|
265
|
+
in sequence by "grapheme boundaries" (Emoji, Skin tones, flags, etc).
|
|
230
266
|
"""
|
|
231
267
|
|
|
232
268
|
if start_offs > end_offs:
|
|
233
|
-
|
|
269
|
+
msg = f"{start_offs=} > {end_offs=}"
|
|
270
|
+
raise ValueError(msg)
|
|
234
271
|
|
|
235
272
|
if isinstance(text, str):
|
|
236
|
-
return
|
|
273
|
+
return wcwidth.width(text[start_offs:end_offs], control_codes="ignore")
|
|
237
274
|
|
|
238
275
|
if _byte_encoding == "utf8":
|
|
239
276
|
try:
|
|
240
|
-
return
|
|
277
|
+
return wcwidth.width(text[start_offs:end_offs].decode("utf-8"), control_codes="ignore")
|
|
241
278
|
except UnicodeDecodeError as exc:
|
|
242
279
|
warnings.warn(
|
|
243
280
|
"`calc_width` with text encoded to bytes can produce incorrect results"
|
|
@@ -250,8 +287,8 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
250
287
|
sc = 0
|
|
251
288
|
while i < end_offs:
|
|
252
289
|
o, i = decode_one(text, i)
|
|
253
|
-
w
|
|
254
|
-
|
|
290
|
+
if (w := wcwidth.wcwidth(chr(o))) > 0:
|
|
291
|
+
sc += w
|
|
255
292
|
return sc
|
|
256
293
|
# "wide", "narrow" or all printable ASCII, just return the character count
|
|
257
294
|
return end_offs - start_offs
|
|
@@ -259,17 +296,22 @@ def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
259
296
|
|
|
260
297
|
def is_wide_char(text: str | bytes, offs: int) -> bool:
|
|
261
298
|
"""
|
|
262
|
-
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.
|
|
263
304
|
|
|
264
305
|
text may be unicode or a byte string in the target _byte_encoding
|
|
265
306
|
"""
|
|
266
307
|
if isinstance(text, str):
|
|
267
|
-
|
|
308
|
+
grapheme = next(wcwidth.iter_graphemes(text[offs:]))
|
|
309
|
+
return wcwidth.width(grapheme, control_codes="ignore") == 2
|
|
268
310
|
if not isinstance(text, bytes):
|
|
269
311
|
raise TypeError(text)
|
|
270
312
|
if _byte_encoding == "utf8":
|
|
271
|
-
|
|
272
|
-
return
|
|
313
|
+
grapheme, _ = _decode_grapheme_at(text, offs, len(text))
|
|
314
|
+
return wcwidth.width(grapheme, control_codes="ignore") == 2
|
|
273
315
|
if _byte_encoding == "wide":
|
|
274
316
|
return within_double_byte(text, offs, offs) == 1
|
|
275
317
|
return False
|
|
@@ -277,19 +319,23 @@ def is_wide_char(text: str | bytes, offs: int) -> bool:
|
|
|
277
319
|
|
|
278
320
|
def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
279
321
|
"""
|
|
280
|
-
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.
|
|
281
326
|
"""
|
|
282
327
|
if start_offs >= end_offs:
|
|
283
328
|
raise ValueError((start_offs, end_offs))
|
|
284
329
|
if isinstance(text, str):
|
|
285
|
-
return end_offs
|
|
330
|
+
return wcwidth.grapheme_boundary_before(text, end_offs)
|
|
286
331
|
if not isinstance(text, bytes):
|
|
287
332
|
raise TypeError(text)
|
|
288
333
|
if _byte_encoding == "utf8":
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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"))
|
|
293
339
|
if _byte_encoding == "wide" and within_double_byte(text, start_offs, end_offs - 1) == 2:
|
|
294
340
|
return end_offs - 2
|
|
295
341
|
return end_offs - 1
|
|
@@ -297,19 +343,21 @@ def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
|
297
343
|
|
|
298
344
|
def move_next_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
|
|
299
345
|
"""
|
|
300
|
-
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.
|
|
301
350
|
"""
|
|
302
351
|
if start_offs >= end_offs:
|
|
303
352
|
raise ValueError((start_offs, end_offs))
|
|
304
353
|
if isinstance(text, str):
|
|
305
|
-
|
|
354
|
+
grapheme = next(wcwidth.iter_graphemes(text[start_offs:end_offs]))
|
|
355
|
+
return start_offs + len(grapheme)
|
|
306
356
|
if not isinstance(text, bytes):
|
|
307
357
|
raise TypeError(text)
|
|
308
358
|
if _byte_encoding == "utf8":
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
o += 1
|
|
312
|
-
return o
|
|
359
|
+
_, next_pos = _decode_grapheme_at(text, start_offs, end_offs)
|
|
360
|
+
return next_pos
|
|
313
361
|
if _byte_encoding == "wide" and within_double_byte(text, start_offs, start_offs) == 1:
|
|
314
362
|
return start_offs + 2
|
|
315
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
|
|
urwid/util.py
CHANGED
|
@@ -510,7 +510,7 @@ def int_scale(val: int, val_range: int, out_range: int) -> int:
|
|
|
510
510
|
|
|
511
511
|
>>> "%x" % int_scale(0x7, 0x10, 0x10000)
|
|
512
512
|
'7777'
|
|
513
|
-
>>> "%x" % int_scale(
|
|
513
|
+
>>> "%x" % int_scale(0x5F, 0x100, 0x10)
|
|
514
514
|
'6'
|
|
515
515
|
>>> int_scale(2, 6, 101)
|
|
516
516
|
40
|
urwid/version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '3.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (3, 0,
|
|
31
|
+
__version__ = version = '3.0.5'
|
|
32
|
+
__version_tuple__ = version_tuple = (3, 0, 5)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
urwid/vterm.py
CHANGED
|
@@ -1418,8 +1418,8 @@ class TermCanvas(Canvas):
|
|
|
1418
1418
|
if self.scrolling_up == 0:
|
|
1419
1419
|
yield from self.term
|
|
1420
1420
|
else:
|
|
1421
|
-
|
|
1422
|
-
yield from
|
|
1421
|
+
viewport_range = slice(-(self.height + self.scrolling_up), -self.scrolling_up)
|
|
1422
|
+
yield from (*self.scrollback_buffer, *self.term)[viewport_range]
|
|
1423
1423
|
|
|
1424
1424
|
def content_delta(self, other: Canvas):
|
|
1425
1425
|
if other is self:
|
urwid/widget/attr_map.py
CHANGED
|
@@ -42,19 +42,19 @@ class AttrMap(delegate_to_widget_mixin("_original_widget"), WidgetDecoration[Wra
|
|
|
42
42
|
:type focus_map: display attribute or dict
|
|
43
43
|
|
|
44
44
|
>>> from urwid import Divider, Edit, Text
|
|
45
|
-
>>> AttrMap(Divider(
|
|
45
|
+
>>> AttrMap(Divider("!"), "bright")
|
|
46
46
|
<AttrMap flow widget <Divider flow widget '!'> attr_map={None: 'bright'}>
|
|
47
|
-
>>> AttrMap(Edit(),
|
|
47
|
+
>>> AttrMap(Edit(), "notfocus", "focus").attr_map
|
|
48
48
|
{None: 'notfocus'}
|
|
49
|
-
>>> AttrMap(Edit(),
|
|
49
|
+
>>> AttrMap(Edit(), "notfocus", "focus").focus_map
|
|
50
50
|
{None: 'focus'}
|
|
51
51
|
>>> size = (5,)
|
|
52
|
-
>>> am = AttrMap(Text(
|
|
53
|
-
>>> next(am.render(size, focus=False).content())
|
|
52
|
+
>>> am = AttrMap(Text("hi"), "greeting", "fgreet")
|
|
53
|
+
>>> next(am.render(size, focus=False).content()) # ... = b in Python 3
|
|
54
54
|
[('greeting', None, ...'hi ')]
|
|
55
55
|
>>> next(am.render(size, focus=True).content())
|
|
56
56
|
[('fgreet', None, ...'hi ')]
|
|
57
|
-
>>> am2 = AttrMap(Text((
|
|
57
|
+
>>> am2 = AttrMap(Text(("word", "hi")), {"word": "greeting", None: "bg"})
|
|
58
58
|
>>> am2
|
|
59
59
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={'word': 'greeting', None: 'bg'}>
|
|
60
60
|
>>> next(am2.render(size).content())
|
|
@@ -94,8 +94,8 @@ class AttrMap(delegate_to_widget_mixin("_original_widget"), WidgetDecoration[Wra
|
|
|
94
94
|
constructor does. You must specify {None: attribute} instead.
|
|
95
95
|
|
|
96
96
|
>>> from urwid import Text
|
|
97
|
-
>>> w = AttrMap(Text(
|
|
98
|
-
>>> w.set_attr_map({
|
|
97
|
+
>>> w = AttrMap(Text("hi"), None)
|
|
98
|
+
>>> w.set_attr_map({"a": "b"})
|
|
99
99
|
>>> w
|
|
100
100
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={'a': 'b'}>
|
|
101
101
|
"""
|
|
@@ -129,8 +129,8 @@ class AttrMap(delegate_to_widget_mixin("_original_widget"), WidgetDecoration[Wra
|
|
|
129
129
|
constructor does. You must specify {None: attribute} instead.
|
|
130
130
|
|
|
131
131
|
>>> from urwid import Text
|
|
132
|
-
>>> w = AttrMap(Text(
|
|
133
|
-
>>> w.set_focus_map({
|
|
132
|
+
>>> w = AttrMap(Text("hi"), {})
|
|
133
|
+
>>> w.set_focus_map({"a": "b"})
|
|
134
134
|
>>> w
|
|
135
135
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={} focus_map={'a': 'b'}>
|
|
136
136
|
>>> w.set_focus_map(None)
|
urwid/widget/attr_wrap.py
CHANGED
|
@@ -25,12 +25,12 @@ class AttrWrap(AttrMap):
|
|
|
25
25
|
new code should use AttrMap instead.
|
|
26
26
|
|
|
27
27
|
>>> from urwid import Divider, Edit, Text
|
|
28
|
-
>>> AttrWrap(Divider(
|
|
28
|
+
>>> AttrWrap(Divider("!"), "bright")
|
|
29
29
|
<AttrWrap flow widget <Divider flow widget '!'> attr='bright'>
|
|
30
|
-
>>> AttrWrap(Edit(),
|
|
30
|
+
>>> AttrWrap(Edit(), "notfocus", "focus")
|
|
31
31
|
<AttrWrap selectable flow widget <Edit selectable flow widget '' edit_pos=0> attr='notfocus' focus_attr='focus'>
|
|
32
32
|
>>> size = (5,)
|
|
33
|
-
>>> aw = AttrWrap(Text(
|
|
33
|
+
>>> aw = AttrWrap(Text("hi"), "greeting", "fgreet")
|
|
34
34
|
>>> next(aw.render(size, focus=False).content())
|
|
35
35
|
[('greeting', None, ...'hi ')]
|
|
36
36
|
>>> next(aw.render(size, focus=True).content())
|
urwid/widget/big_text.py
CHANGED
urwid/widget/box_adapter.py
CHANGED
|
@@ -32,7 +32,7 @@ class BoxAdapter(WidgetDecoration[WrappedWidget]):
|
|
|
32
32
|
:type height: int
|
|
33
33
|
|
|
34
34
|
>>> from urwid import SolidFill
|
|
35
|
-
>>> BoxAdapter(SolidFill(
|
|
35
|
+
>>> BoxAdapter(SolidFill("x"), 5) # 5-rows of x's
|
|
36
36
|
<BoxAdapter flow widget <SolidFill box widget 'x'> height=5>
|
|
37
37
|
"""
|
|
38
38
|
if hasattr(box_widget, "sizing") and Sizing.BOX not in box_widget.sizing():
|
|
@@ -71,7 +71,7 @@ class BoxAdapter(WidgetDecoration[WrappedWidget]):
|
|
|
71
71
|
Return the predetermined height (behave like a flow widget)
|
|
72
72
|
|
|
73
73
|
>>> from urwid import SolidFill
|
|
74
|
-
>>> BoxAdapter(SolidFill(
|
|
74
|
+
>>> BoxAdapter(SolidFill("x"), 5).rows((20,))
|
|
75
75
|
5
|
|
76
76
|
"""
|
|
77
77
|
return self.height
|
urwid/widget/divider.py
CHANGED
|
@@ -59,9 +59,9 @@ class Divider(Widget):
|
|
|
59
59
|
|
|
60
60
|
>>> Divider()
|
|
61
61
|
<Divider flow widget>
|
|
62
|
-
>>> Divider(
|
|
62
|
+
>>> Divider("-")
|
|
63
63
|
<Divider flow widget '-'>
|
|
64
|
-
>>> Divider(
|
|
64
|
+
>>> Divider("x", 1, 2)
|
|
65
65
|
<Divider flow widget 'x' bottom=2 top=1>
|
|
66
66
|
"""
|
|
67
67
|
super().__init__()
|
|
@@ -86,7 +86,7 @@ class Divider(Widget):
|
|
|
86
86
|
|
|
87
87
|
>>> Divider().rows((10,))
|
|
88
88
|
1
|
|
89
|
-
>>> Divider(
|
|
89
|
+
>>> Divider("x", 1, 2).rows((10,))
|
|
90
90
|
4
|
|
91
91
|
"""
|
|
92
92
|
(_maxcol,) = size
|
|
@@ -100,11 +100,11 @@ class Divider(Widget):
|
|
|
100
100
|
"""
|
|
101
101
|
Render the divider as a canvas and return it.
|
|
102
102
|
|
|
103
|
-
>>> Divider().render((10,)).text
|
|
103
|
+
>>> Divider().render((10,)).text # ... = b in Python 3
|
|
104
104
|
[...' ']
|
|
105
|
-
>>> Divider(
|
|
105
|
+
>>> Divider("-", top=1).render((10,)).text
|
|
106
106
|
[...' ', ...'----------']
|
|
107
|
-
>>> Divider(
|
|
107
|
+
>>> Divider("x", bottom=2).render((5,)).text
|
|
108
108
|
[...'xxxxx', ...' ', ...' ']
|
|
109
109
|
"""
|
|
110
110
|
(maxcol,) = size
|