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.
Files changed (62) hide show
  1. urwid/__init__.py +30 -20
  2. urwid/canvas.py +34 -53
  3. urwid/command_map.py +6 -4
  4. urwid/container.py +1 -1
  5. urwid/decoration.py +1 -1
  6. urwid/display/__init__.py +53 -48
  7. urwid/display/_posix_raw_display.py +20 -8
  8. urwid/display/_raw_display_base.py +21 -16
  9. urwid/display/_win32_raw_display.py +16 -17
  10. urwid/display/common.py +45 -74
  11. urwid/display/curses.py +3 -5
  12. urwid/display/escape.py +28 -13
  13. urwid/display/lcd.py +8 -10
  14. urwid/display/web.py +11 -16
  15. urwid/event_loop/asyncio_loop.py +35 -15
  16. urwid/event_loop/main_loop.py +18 -23
  17. urwid/event_loop/tornado_loop.py +4 -5
  18. urwid/event_loop/trio_loop.py +1 -1
  19. urwid/font.py +19 -22
  20. urwid/numedit.py +65 -65
  21. urwid/signals.py +19 -27
  22. urwid/split_repr.py +9 -3
  23. urwid/str_util.py +105 -60
  24. urwid/text_layout.py +14 -13
  25. urwid/util.py +8 -19
  26. urwid/version.py +22 -4
  27. urwid/vterm.py +20 -47
  28. urwid/widget/__init__.py +0 -6
  29. urwid/widget/attr_map.py +10 -10
  30. urwid/widget/attr_wrap.py +11 -13
  31. urwid/widget/bar_graph.py +3 -8
  32. urwid/widget/big_text.py +8 -9
  33. urwid/widget/box_adapter.py +6 -6
  34. urwid/widget/columns.py +52 -83
  35. urwid/widget/container.py +29 -75
  36. urwid/widget/divider.py +6 -6
  37. urwid/widget/edit.py +50 -50
  38. urwid/widget/filler.py +14 -14
  39. urwid/widget/frame.py +31 -40
  40. urwid/widget/grid_flow.py +25 -110
  41. urwid/widget/line_box.py +31 -18
  42. urwid/widget/listbox.py +16 -51
  43. urwid/widget/monitored_list.py +75 -49
  44. urwid/widget/overlay.py +4 -37
  45. urwid/widget/padding.py +31 -68
  46. urwid/widget/pile.py +179 -158
  47. urwid/widget/popup.py +2 -2
  48. urwid/widget/progress_bar.py +17 -18
  49. urwid/widget/scrollable.py +26 -34
  50. urwid/widget/solid_fill.py +3 -3
  51. urwid/widget/text.py +44 -30
  52. urwid/widget/treetools.py +27 -48
  53. urwid/widget/widget.py +13 -130
  54. urwid/widget/widget_decoration.py +6 -35
  55. urwid/widget/wimp.py +61 -61
  56. urwid/wimp.py +1 -1
  57. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/METADATA +24 -24
  58. urwid-3.0.5.dist-info/RECORD +74 -0
  59. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/WHEEL +1 -1
  60. urwid-2.6.15.dist-info/RECORD +0 -74
  61. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info/licenses}/COPYING +0 -0
  62. {urwid-2.6.15.dist-info → urwid-3.0.5.dist-info}/top_level.txt +0 -0
urwid/__init__.py CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
- import importlib
24
+ import importlib.util
25
25
  import sys
26
26
  import types
27
27
  import typing
@@ -150,7 +150,6 @@ from .widget import (
150
150
  BigText,
151
151
  BoxAdapter,
152
152
  BoxAdapterError,
153
- BoxWidget,
154
153
  Button,
155
154
  CheckBox,
156
155
  CheckBoxError,
@@ -161,8 +160,6 @@ from .widget import (
161
160
  EditError,
162
161
  Filler,
163
162
  FillerError,
164
- FixedWidget,
165
- FlowWidget,
166
163
  Frame,
167
164
  FrameError,
168
165
  GraphVScale,
@@ -242,7 +239,7 @@ except ImportError:
242
239
 
243
240
  # OS Specific
244
241
  if sys.platform != "win32":
245
- from .vterm import TermCanvas, TermCharset, Terminal, TermModes, TermScroller
242
+ from .vterm import TermCanvas, TermCharset, Terminal, TermModes
246
243
 
247
244
  # ZMQEventLoop cause interpreter crash on windows
248
245
  try:
@@ -268,12 +265,28 @@ _moved_warn: dict[str, str] = {
268
265
  }
269
266
  # Backward compatible lazy load without any warnings
270
267
  # Before DeprecationWarning need to start PendingDeprecationWarning process.
271
- _moved_no_warn: dict[str, str] = {
272
- "display_common": "urwid.display.common",
273
- "raw_display": "urwid.display.raw",
274
- "curses_display": "urwid.display.curses",
275
- "escape": "urwid.display.escape",
276
- }
268
+
269
+
270
+ def lazy_import(name: str, package: str | None = None) -> types.ModuleType:
271
+ """Lazy import implementation from Python documentation.
272
+
273
+ Useful for cases where no warnings expected for moved modules.
274
+ """
275
+ spec = importlib.util.find_spec(name, package)
276
+ if not spec:
277
+ raise ImportError(f"No module named {name!r}")
278
+ if not spec.loader:
279
+ raise ImportError(f"Module named {name!r} is invalid")
280
+
281
+ loader = importlib.util.LazyLoader(spec.loader)
282
+ spec.loader = loader
283
+ module = importlib.util.module_from_spec(spec)
284
+ if not package:
285
+ sys.modules[name] = module
286
+ else:
287
+ sys.modules[f"{package.rstrip('.')}.{name.lstrip('.')}"] = module
288
+ loader.exec_module(module)
289
+ return module
277
290
 
278
291
 
279
292
  class _MovedModule(types.ModuleType):
@@ -296,6 +309,12 @@ class _MovedModule(types.ModuleType):
296
309
  return getattr(real_module, name)
297
310
 
298
311
 
312
+ display_common = lazy_import("urwid.display.common")
313
+ raw_display = lazy_import("urwid.display.raw")
314
+ curses_display = lazy_import("urwid.display.curses")
315
+ escape = lazy_import("urwid.display.escape")
316
+
317
+
299
318
  class _MovedModuleWarn(_MovedModule):
300
319
  """Special class to handle moved modules.
301
320
 
@@ -313,10 +332,6 @@ class _MovedModuleWarn(_MovedModule):
313
332
  return super().__getattr__(name)
314
333
 
315
334
 
316
- for _name, _module in _moved_no_warn.items():
317
- _module_path = f"{__name__}.{_name}"
318
- sys.modules[_module_path] = _MovedModule(_module_path, _module)
319
-
320
335
  for _name, _module in _moved_warn.items():
321
336
  _module_path = f"{__name__}.{_name}"
322
337
  sys.modules[_module_path] = _MovedModuleWarn(_module_path, _module)
@@ -328,11 +343,6 @@ def __getattr__(name: str) -> typing.Any:
328
343
  :return: attribute by name
329
344
  :raises AttributeError: attribute is not defined for lazy load
330
345
  """
331
- if name in _moved_no_warn:
332
- mod = importlib.import_module(_moved_no_warn[name])
333
- __locals[name] = mod
334
- return mod
335
-
336
346
  if name in _moved_warn:
337
347
  warnings.warn(
338
348
  f"{name} is moved to {_moved_warn[name]}",
urwid/canvas.py CHANGED
@@ -23,7 +23,6 @@ from __future__ import annotations
23
23
  import contextlib
24
24
  import dataclasses
25
25
  import typing
26
- import warnings
27
26
  import weakref
28
27
  from contextlib import suppress
29
28
 
@@ -40,7 +39,7 @@ from urwid.util import (
40
39
  )
41
40
 
42
41
  if typing.TYPE_CHECKING:
43
- from collections.abc import Hashable, Iterable, Sequence
42
+ from collections.abc import Hashable, Iterable, Iterator, Sequence
44
43
 
45
44
  from typing_extensions import Literal
46
45
 
@@ -85,7 +84,7 @@ class CanvasCache:
85
84
  cleanups = 0
86
85
 
87
86
  @classmethod
88
- def store(cls, wcls, canvas: Canvas) -> None:
87
+ def store(cls, wcls: type[Widget], canvas: Canvas) -> None:
89
88
  """
90
89
  Store a weakref to canvas in the cache.
91
90
 
@@ -99,7 +98,7 @@ class CanvasCache:
99
98
  raise TypeError("Can't store canvas without widget_info")
100
99
  widget, size, focus = canvas.widget_info
101
100
 
102
- def walk_depends(canv):
101
+ def walk_depends(canv: Canvas) -> list[Widget]:
103
102
  """
104
103
  Collect all child widgets for determining who we
105
104
  depend on.
@@ -126,10 +125,10 @@ class CanvasCache:
126
125
 
127
126
  ref = weakref.ref(canvas, cls.cleanup)
128
127
  cls._refs[ref] = (widget, wcls, size, focus)
129
- cls._widgets.setdefault(widget, {})[(wcls, size, focus)] = ref
128
+ cls._widgets.setdefault(widget, {})[wcls, size, focus] = ref
130
129
 
131
130
  @classmethod
132
- def fetch(cls, widget, wcls, size, focus) -> Canvas | None:
131
+ def fetch(cls, widget: Widget, wcls: type[Widget], size, focus: bool) -> Canvas | None:
133
132
  """
134
133
  Return the cached canvas or None.
135
134
 
@@ -151,7 +150,7 @@ class CanvasCache:
151
150
  return canv
152
151
 
153
152
  @classmethod
154
- def invalidate(cls, widget):
153
+ def invalidate(cls, widget: Widget) -> None:
155
154
  """
156
155
  Remove all canvases cached for widget.
157
156
  """
@@ -182,7 +181,7 @@ class CanvasCache:
182
181
  if not sizes:
183
182
  return
184
183
  with suppress(KeyError):
185
- del sizes[(wcls, size, focus)]
184
+ del sizes[wcls, size, focus]
186
185
  if not sizes:
187
186
  with contextlib.suppress(KeyError):
188
187
  del cls._widgets[widget]
@@ -215,7 +214,7 @@ class Canvas:
215
214
 
216
215
  def __init__(self) -> None:
217
216
  """Base Canvas class"""
218
- self._widget_info = None
217
+ self._widget_info: tuple[Widget, tuple[[]] | tuple[int] | tuple[int, int], bool] | None = None
219
218
  self.coords: dict[str, tuple[int, int, tuple[Widget, int, int]] | tuple[int, int, None]] = {}
220
219
  self.shortcuts: dict[str, str] = {}
221
220
 
@@ -241,18 +240,9 @@ class Canvas:
241
240
  self._widget_info = widget, size, focus
242
241
 
243
242
  @property
244
- def widget_info(self):
243
+ def widget_info(self) -> tuple[Widget, tuple[[]] | tuple[int] | tuple[int, int], bool] | None:
245
244
  return self._widget_info
246
245
 
247
- def _get_widget_info(self):
248
- warnings.warn(
249
- f"Method `{self.__class__.__name__}._get_widget_info` is deprecated, "
250
- f"please use property `{self.__class__.__name__}.widget_info`",
251
- DeprecationWarning,
252
- stacklevel=2,
253
- )
254
- return self.widget_info
255
-
256
246
  @property
257
247
  def text(self) -> list[bytes]:
258
248
  """
@@ -266,15 +256,6 @@ class Canvas:
266
256
  encoding = get_encoding()
267
257
  return tuple(line.decode(encoding) for line in self.text)
268
258
 
269
- def _text_content(self):
270
- warnings.warn(
271
- f"Method `{self.__class__.__name__}._text_content` is deprecated, "
272
- f"please use property `{self.__class__.__name__}.text`",
273
- DeprecationWarning,
274
- stacklevel=2,
275
- )
276
- return self.text
277
-
278
259
  def content(
279
260
  self,
280
261
  trim_left: int = 0,
@@ -282,7 +263,7 @@ class Canvas:
282
263
  cols: int | None = None,
283
264
  rows: int | None = None,
284
265
  attr=None,
285
- ) -> Iterable[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
266
+ ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
286
267
  raise NotImplementedError()
287
268
 
288
269
  def cols(self) -> int:
@@ -295,10 +276,10 @@ class Canvas:
295
276
  raise NotImplementedError()
296
277
 
297
278
  def get_cursor(self) -> tuple[int, int] | None:
298
- c = self.coords.get("cursor", None)
299
- if not c:
300
- return None
301
- return c[:2] # trim off data part
279
+ if c := self.coords.get("cursor", None):
280
+ return c[:2] # trim off data part
281
+
282
+ return None
302
283
 
303
284
  def set_cursor(self, c: tuple[int, int] | None) -> None:
304
285
  if self.widget_info and self.cacheable:
@@ -312,10 +293,10 @@ class Canvas:
312
293
  cursor = property(get_cursor, set_cursor)
313
294
 
314
295
  def get_pop_up(self) -> tuple[int, int, tuple[Widget, int, int]] | None:
315
- c = self.coords.get("pop up", None)
316
- if not c:
317
- return None
318
- return c
296
+ if c := self.coords.get("pop up", None):
297
+ return c
298
+
299
+ return None
319
300
 
320
301
  def set_pop_up(self, w: Widget, left: int, top: int, overlay_width: int, overlay_height: int) -> None:
321
302
  """
@@ -376,7 +357,7 @@ class TextCanvas(Canvas):
376
357
 
377
358
  def __init__(
378
359
  self,
379
- text: Sequence[bytes] | None = None,
360
+ text: list[bytes] | None = None,
380
361
  attr: list[list[tuple[Hashable | None, int]]] | None = None,
381
362
  cs: list[list[tuple[Literal["0", "U"] | None, int]]] | None = None,
382
363
  cursor: tuple[int, int] | None = None,
@@ -477,7 +458,7 @@ class TextCanvas(Canvas):
477
458
  cols: int | None = 0,
478
459
  rows: int | None = 0,
479
460
  attr=None,
480
- ) -> Iterable[tuple[object, Literal["0", "U"] | None, bytes]]:
461
+ ) -> Iterator[tuple[object, Literal["0", "U"] | None, bytes]]:
481
462
  """
482
463
  Return the canvas content as a list of rows where each row
483
464
  is a list of (attr, cs, text) tuples.
@@ -551,7 +532,7 @@ class BlankCanvas(Canvas):
551
532
  cols: int | None = 0,
552
533
  rows: int | None = 0,
553
534
  attr=None,
554
- ) -> Iterable[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
535
+ ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
555
536
  """
556
537
  return (cols, rows) of spaces with default attributes.
557
538
  """
@@ -603,7 +584,7 @@ class SolidCanvas(Canvas):
603
584
  cols: int | None = None,
604
585
  rows: int | None = None,
605
586
  attr=None,
606
- ) -> Iterable[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
587
+ ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
607
588
  if cols is None:
608
589
  cols = self.size[0]
609
590
  if rows is None:
@@ -704,7 +685,7 @@ class CompositeCanvas(Canvas):
704
685
  cols: int | None = None,
705
686
  rows: int | None = None,
706
687
  attr=None,
707
- ) -> Iterable[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
688
+ ) -> Iterator[list[tuple[object, Literal["0", "U"] | None, bytes]]]:
708
689
  """
709
690
  Return the canvas content as a list of rows where each row
710
691
  is a list of (attr, cs, text) tuples.
@@ -808,7 +789,7 @@ class CompositeCanvas(Canvas):
808
789
 
809
790
  if right > 0:
810
791
  new_top_cviews.append((0, 0, right, rows, None, blank_canvas))
811
- shards = [(top_rows, new_top_cviews)] + shards[1:]
792
+ shards = [(top_rows, new_top_cviews), *shards[1:]]
812
793
 
813
794
  self.coords = self.translate_coords(left, 0)
814
795
  self.shards = shards
@@ -833,7 +814,7 @@ class CompositeCanvas(Canvas):
833
814
 
834
815
  if bottom > 0:
835
816
  if orig_shards is self.shards:
836
- self.shards = self.shards[:]
817
+ self.shards = self.shards.copy()
837
818
  self.shards.append((bottom, [(0, 0, cols, bottom, None, blank_canvas)]))
838
819
 
839
820
  def overlay(self, other: CompositeCanvas, left: int, top: int) -> None:
@@ -902,11 +883,11 @@ class CompositeCanvas(Canvas):
902
883
  for cv in original_cviews:
903
884
  # cv[4] == attr_map
904
885
  if cv[4] is None:
905
- new_cviews.append(cv[:4] + (mapping,) + cv[5:])
886
+ new_cviews.append((*cv[:4], mapping, *cv[5:]))
906
887
  else:
907
888
  combined = mapping.copy()
908
889
  combined.update([(k, mapping.get(v, v)) for k, v in cv[4].items()])
909
- new_cviews.append(cv[:4] + (combined,) + cv[5:])
890
+ new_cviews.append((*cv[:4], combined, *cv[5:]))
910
891
  shards.append((num_rows, new_cviews))
911
892
  self.shards = shards
912
893
 
@@ -1002,7 +983,7 @@ def shard_cviews_delta(cviews, other_cviews):
1002
983
  continue
1003
984
  # top-left-aligned cviews, compare them
1004
985
  if cv[5] is other_cv[5] and cv[:5] == other_cv[:5]:
1005
- yield cv[:5] + (None,) + cv[6:]
986
+ yield (*cv[:5], None, *cv[6:])
1006
987
  else:
1007
988
  yield cv
1008
989
  other_cols += other_cv[2]
@@ -1185,19 +1166,19 @@ def shards_join(shard_lists):
1185
1166
 
1186
1167
 
1187
1168
  def cview_trim_rows(cv, rows: int):
1188
- return cv[:3] + (rows,) + cv[4:]
1169
+ return (*cv[:3], rows, *cv[4:])
1189
1170
 
1190
1171
 
1191
1172
  def cview_trim_top(cv, trim: int):
1192
- return (cv[0], trim + cv[1], cv[2], cv[3] - trim) + cv[4:]
1173
+ return (cv[0], trim + cv[1], cv[2], cv[3] - trim, *cv[4:])
1193
1174
 
1194
1175
 
1195
1176
  def cview_trim_left(cv, trim: int):
1196
- return (cv[0] + trim, cv[1], cv[2] - trim) + cv[3:]
1177
+ return (cv[0] + trim, cv[1], cv[2] - trim, *cv[3:])
1197
1178
 
1198
1179
 
1199
1180
  def cview_trim_cols(cv, cols: int):
1200
- return cv[:2] + (cols,) + cv[3:]
1181
+ return (*cv[:2], cols, *cv[3:])
1201
1182
 
1202
1183
 
1203
1184
  def CanvasCombine(canvas_info: Iterable[tuple[Canvas, typing.Any, bool]]) -> CompositeCanvas:
@@ -1229,7 +1210,7 @@ def CanvasCombine(canvas_info: Iterable[tuple[Canvas, typing.Any, bool]]) -> Com
1229
1210
  row += canv.rows()
1230
1211
 
1231
1212
  if focus_index:
1232
- children = [children[focus_index]] + children[:focus_index] + children[focus_index + 1 :]
1213
+ children = [children[focus_index], *children[:focus_index], *children[focus_index + 1 :]]
1233
1214
 
1234
1215
  combined_canvas.shards = shards
1235
1216
  combined_canvas.children = children
@@ -1295,7 +1276,7 @@ def CanvasJoin(canvas_info: Iterable[tuple[Canvas, typing.Any, bool, int]]) -> C
1295
1276
  col += composite_canvas.cols()
1296
1277
 
1297
1278
  if focus_item:
1298
- children = [children[focus_item]] + children[:focus_item] + children[focus_item + 1 :]
1279
+ children = [children[focus_item], *children[:focus_item], *children[focus_item + 1 :]]
1299
1280
 
1300
1281
  joined_canvas.shards = shards_join(shard_lists)
1301
1282
  joined_canvas.children = children
urwid/command_map.py CHANGED
@@ -21,9 +21,11 @@ from __future__ import annotations
21
21
 
22
22
  import enum
23
23
  import typing
24
- from typing import Iterator
24
+ from collections.abc import MutableMapping
25
25
 
26
26
  if typing.TYPE_CHECKING:
27
+ from collections.abc import Iterator
28
+
27
29
  from typing_extensions import Self
28
30
 
29
31
 
@@ -55,7 +57,7 @@ CURSOR_MAX_RIGHT = Command.MAX_RIGHT
55
57
  ACTIVATE = Command.ACTIVATE
56
58
 
57
59
 
58
- class CommandMap(typing.Mapping[str, typing.Union[str, Command, None]]):
60
+ class CommandMap(MutableMapping[str, typing.Union[str, Command, None]]):
59
61
  """
60
62
  dict-like object for looking up commands from keystrokes
61
63
 
@@ -105,10 +107,10 @@ class CommandMap(typing.Mapping[str, typing.Union[str, Command, None]]):
105
107
  }
106
108
 
107
109
  def __init__(self) -> None:
108
- self._command = dict(self._command_defaults)
110
+ self._command = self._command_defaults.copy()
109
111
 
110
112
  def restore_defaults(self) -> None:
111
- self._command = dict(self._command_defaults)
113
+ self._command = self._command_defaults.copy()
112
114
 
113
115
  def __getitem__(self, key: str) -> str | Command | None:
114
116
  return self._command.get(key, None)
urwid/container.py CHANGED
@@ -53,7 +53,7 @@ __all__ = (
53
53
  warnings.warn(
54
54
  f"{__name__!r} is not expected to be imported directly. "
55
55
  'Please use public access from "urwid" package. '
56
- f"Module {__name__!r} is deprecated and will be removed in the future.",
56
+ f"Module {__name__!r} is deprecated and will be removed in the version 4.0.",
57
57
  DeprecationWarning,
58
58
  stacklevel=3,
59
59
  )
urwid/decoration.py CHANGED
@@ -59,7 +59,7 @@ __all__ = (
59
59
  warnings.warn(
60
60
  f"{__name__!r} is not expected to be imported directly. "
61
61
  'Please use public access from "urwid" package. '
62
- f"Module {__name__!r} is deprecated and will be removed in the future.",
62
+ f"Module {__name__!r} is deprecated and will be removed in the version 4.0.",
63
63
  DeprecationWarning,
64
64
  stacklevel=3,
65
65
  )
urwid/display/__init__.py CHANGED
@@ -2,9 +2,40 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import importlib
5
+ import importlib.util
6
+ import sys
6
7
  import typing
7
8
 
9
+ from . import raw
10
+ from .common import (
11
+ BLACK,
12
+ BROWN,
13
+ DARK_BLUE,
14
+ DARK_CYAN,
15
+ DARK_GRAY,
16
+ DARK_GREEN,
17
+ DARK_MAGENTA,
18
+ DARK_RED,
19
+ DEFAULT,
20
+ LIGHT_BLUE,
21
+ LIGHT_CYAN,
22
+ LIGHT_GRAY,
23
+ LIGHT_GREEN,
24
+ LIGHT_MAGENTA,
25
+ LIGHT_RED,
26
+ UPDATE_PALETTE_ENTRY,
27
+ WHITE,
28
+ YELLOW,
29
+ AttrSpec,
30
+ AttrSpecError,
31
+ BaseScreen,
32
+ RealTerminal,
33
+ ScreenError,
34
+ )
35
+
36
+ if typing.TYPE_CHECKING:
37
+ import types
38
+
8
39
  __all__ = (
9
40
  "BLACK",
10
41
  "BROWN",
@@ -36,33 +67,6 @@ __all__ = (
36
67
  "web",
37
68
  )
38
69
 
39
- from . import raw
40
- from .common import (
41
- BLACK,
42
- BROWN,
43
- DARK_BLUE,
44
- DARK_CYAN,
45
- DARK_GRAY,
46
- DARK_GREEN,
47
- DARK_MAGENTA,
48
- DARK_RED,
49
- DEFAULT,
50
- LIGHT_BLUE,
51
- LIGHT_CYAN,
52
- LIGHT_GRAY,
53
- LIGHT_GREEN,
54
- LIGHT_MAGENTA,
55
- LIGHT_RED,
56
- UPDATE_PALETTE_ENTRY,
57
- WHITE,
58
- YELLOW,
59
- AttrSpec,
60
- AttrSpecError,
61
- BaseScreen,
62
- RealTerminal,
63
- ScreenError,
64
- )
65
-
66
70
  try:
67
71
  from . import curses
68
72
 
@@ -70,28 +74,29 @@ try:
70
74
  except ImportError:
71
75
  pass
72
76
 
73
- # Moved modules handling
74
- __locals: dict[str, typing.Any] = locals() # use mutable access for pure lazy loading
75
77
 
76
- # Lazy load modules
77
- _lazy_load: frozenset[str] = frozenset(
78
- (
79
- "html_fragment",
80
- "lcd",
81
- "web",
82
- )
83
- )
78
+ def lazy_import(name: str, package: str | None = None) -> types.ModuleType:
79
+ """Lazy import implementation from Python documentation.
84
80
 
81
+ Useful for cases where no warnings expected for moved modules.
82
+ """
83
+ spec = importlib.util.find_spec(name, package)
84
+ if not spec:
85
+ raise ImportError(f"No module named {name!r}")
86
+ if not spec.loader:
87
+ raise ImportError(f"Module named {name!r} is invalid")
85
88
 
86
- def __getattr__(name: str) -> typing.Any:
87
- """Get attributes lazy.
89
+ loader = importlib.util.LazyLoader(spec.loader)
90
+ spec.loader = loader
91
+ module = importlib.util.module_from_spec(spec)
92
+ if not package:
93
+ sys.modules[name] = module
94
+ else:
95
+ sys.modules[f"{package.rstrip('.')}.{name.lstrip('.')}"] = module
96
+ loader.exec_module(module)
97
+ return module
88
98
 
89
- :return: attribute by name
90
- :raises AttributeError: attribute is not defined for lazy load
91
- """
92
- if name in _lazy_load:
93
- mod = importlib.import_module(f"{__package__}.{name}")
94
- __locals[name] = mod
95
- return mod
96
99
 
97
- raise AttributeError(f"{name} not found in {__package__}")
100
+ html_fragment = lazy_import(".html_fragment", __package__)
101
+ lcd = lazy_import(".lcd", __package__)
102
+ web = lazy_import(".web", __package__)
@@ -56,6 +56,7 @@ class Screen(_raw_display_base.Screen):
56
56
  input: typing.TextIO = sys.stdin, # noqa: A002 # pylint: disable=redefined-builtin
57
57
  output: typing.TextIO = sys.stdout,
58
58
  bracketed_paste_mode=False,
59
+ focus_reporting=False,
59
60
  ) -> None:
60
61
  """Initialize a screen that directly prints escape codes to an output
61
62
  terminal.
@@ -63,11 +64,15 @@ class Screen(_raw_display_base.Screen):
63
64
  bracketed_paste_mode -- enable bracketed paste mode in the host terminal.
64
65
  If the host terminal supports it, the application will receive `begin paste`
65
66
  and `end paste` keystrokes when the user pastes text.
67
+ focus_reporting -- enable focus reporting in the host terminal.
68
+ If the host terminal supports it, the application will receive `focus in`
69
+ and `focus out` keystrokes when the application gains and loses focus.
66
70
  """
67
71
  super().__init__(input, output)
68
72
  self.gpm_mev: Popen | None = None
69
73
  self.gpm_event_pending: bool = False
70
74
  self.bracketed_paste_mode = bracketed_paste_mode
75
+ self.focus_reporting = focus_reporting
71
76
 
72
77
  # These store the previous signal handlers after setting ours
73
78
  self._prev_sigcont_handler = None
@@ -79,7 +84,8 @@ class Screen(_raw_display_base.Screen):
79
84
  f"<{self.__class__.__name__}("
80
85
  f"input={self._term_input_file}, "
81
86
  f"output={self._term_output_file}, "
82
- f"bracketed_paste_mode={self.bracketed_paste_mode})>"
87
+ f"bracketed_paste_mode={self.bracketed_paste_mode}, "
88
+ f"focus_reporting={self.focus_reporting})>"
83
89
  )
84
90
 
85
91
  def _sigwinch_handler(self, signum: int = 28, frame: FrameType | None = None) -> None:
@@ -148,7 +154,7 @@ class Screen(_raw_display_base.Screen):
148
154
  if not os.environ.get("TERM", "").lower().startswith("linux"):
149
155
  return
150
156
 
151
- m = Popen( # noqa: S603 # pylint: disable=consider-using-with
157
+ m = Popen( # pylint: disable=consider-using-with
152
158
  ["/usr/bin/mev", "-e", "158"],
153
159
  stdin=PIPE,
154
160
  stdout=PIPE,
@@ -169,7 +175,7 @@ class Screen(_raw_display_base.Screen):
169
175
  """
170
176
  Initialize the screen and input mode.
171
177
 
172
- alternate_buffer -- use alternate screen buffer
178
+ alternate_buffer -- use an alternate screen buffer
173
179
  """
174
180
  if alternate_buffer:
175
181
  self.write(escape.SWITCH_TO_ALTERNATE_BUFFER)
@@ -180,6 +186,9 @@ class Screen(_raw_display_base.Screen):
180
186
  if self.bracketed_paste_mode:
181
187
  self.write(escape.ENABLE_BRACKETED_PASTE_MODE)
182
188
 
189
+ if self.focus_reporting:
190
+ self.write(escape.ENABLE_FOCUS_REPORTING)
191
+
183
192
  fd = self._input_fileno()
184
193
  if fd is not None and os.isatty(fd):
185
194
  self._old_termios_settings = termios.tcgetattr(fd)
@@ -207,15 +216,18 @@ class Screen(_raw_display_base.Screen):
207
216
  if self.bracketed_paste_mode:
208
217
  self.write(escape.DISABLE_BRACKETED_PASTE_MODE)
209
218
 
219
+ if self.focus_reporting:
220
+ self.write(escape.DISABLE_FOCUS_REPORTING)
221
+
210
222
  signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED)
211
223
 
212
224
  self.signal_restore()
213
225
 
226
+ self._stop_mouse_restore_buffer()
227
+
214
228
  fd = self._input_fileno()
215
229
  if fd is not None and os.isatty(fd):
216
- termios.tcsetattr(fd, termios.TCSADRAIN, self._old_termios_settings)
217
-
218
- self._stop_mouse_restore_buffer()
230
+ termios.tcsetattr(fd, termios.TCSAFLUSH, self._old_termios_settings)
219
231
 
220
232
  if self._old_signal_keys:
221
233
  self.tty_signal_keys(*self._old_signal_keys, fd)
@@ -391,8 +403,8 @@ class Screen(_raw_display_base.Screen):
391
403
  y, x = super().get_cols_rows()
392
404
  with contextlib.suppress(OSError): # Term size could not be determined
393
405
  if hasattr(self._term_output_file, "fileno"):
394
- buf = fcntl.ioctl(self._term_output_file.fileno(), termios.TIOCGWINSZ, b" " * 4)
395
- y, x = struct.unpack("hh", buf)
406
+ buf = fcntl.ioctl(self._term_output_file.fileno(), termios.TIOCGWINSZ, b" " * 8)
407
+ y, x, _, _ = struct.unpack("hhhh", buf)
396
408
 
397
409
  # Provide some lightweight fallbacks in case the TIOCWINSZ doesn't
398
410
  # give sane answers