urwid 4.2.0__tar.gz → 4.2.2__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.0 → urwid-4.2.2}/.pre-commit-config.yaml +1 -1
- {urwid-4.2.0/urwid.egg-info → urwid-4.2.2}/PKG-INFO +1 -1
- urwid-4.2.2/pylint-requirements.txt +1 -0
- {urwid-4.2.0 → urwid-4.2.2}/pyproject.toml +0 -1
- {urwid-4.2.0 → urwid-4.2.2}/urwid/canvas.py +9 -1
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_posix_raw_display.py +2 -1
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_raw_display_base.py +6 -6
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/common.py +24 -10
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/curses.py +3 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/escape.py +1 -1
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/web.py +21 -10
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/main_loop.py +21 -4
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/trio_loop.py +18 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/text_layout.py +18 -5
- {urwid-4.2.0 → urwid-4.2.2}/urwid/version.py +3 -3
- {urwid-4.2.0 → urwid-4.2.2}/urwid/vterm.py +3 -2
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/ansi_text.py +2 -2
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/attr_map.py +24 -23
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/attr_wrap.py +2 -2
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/edit.py +14 -2
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/line_box.py +1 -1
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/overlay.py +6 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/popup.py +1 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/text.py +2 -0
- {urwid-4.2.0 → urwid-4.2.2/urwid.egg-info}/PKG-INFO +1 -1
- urwid-4.2.2/urwid.egg-info/scm_version.json +8 -0
- urwid-4.2.0/pylint-requirements.txt +0 -1
- urwid-4.2.0/urwid.egg-info/scm_version.json +0 -8
- {urwid-4.2.0 → urwid-4.2.2}/.devcontainer/dev.dockerfile +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.devcontainer/devcontainer.json +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.dockerignore +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.editorconfig +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/CODE_OF_CONDUCT.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/docs.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/ISSUE_TEMPLATE/question.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/dependabot.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/labeler.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/release.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/documentation.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/isolated_mypy.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/isolated_pylint.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/labels.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/pythonpackage.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.github/workflows/yamllint.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.gitignore +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/.yamllint.yml +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/COPYING +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/MANIFEST.in +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/README.rst +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/SECURITY.md +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/classifiers.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/conftest.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/mypy-requirements.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/pytest_requirements.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/requirements.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/setup.cfg +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/tox.ini +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/__init__.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/ansi_parser.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/command_map.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/__init__.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_web.css +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_web.js +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_win32.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/_win32_raw_display.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/html_fragment.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/lcd.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/display/raw.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/__init__.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/abstract_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/asyncio_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/glib_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/select_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/tornado_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/twisted_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/event_loop/zmq_loop.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/font.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/graphics.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/numedit.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/py.typed +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/signals.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/split_repr.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/str_util.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/util.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/__init__.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/bar_graph.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/big_text.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/box_adapter.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/columns.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/constants.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/container.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/deque_walker.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/divider.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/filler.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/frame.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/grid_flow.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/list_walker.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/listbox.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/monitored_deque.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/monitored_list.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/padding.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/pile.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/progress_bar.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/scrollable.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/solid_fill.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/treetools.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/widget.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/widget_decoration.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid/widget/wimp.py +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/SOURCES.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/dependency_links.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/not-zip-safe +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/requires.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/scm_file_list.json +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/urwid.egg-info/top_level.txt +0 -0
- {urwid-4.2.0 → urwid-4.2.2}/yamllint-requirements.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pylint==4.1.1
|
|
@@ -197,7 +197,6 @@ allow-dunder-method-names = [
|
|
|
197
197
|
[tool.pylint]
|
|
198
198
|
disable = [
|
|
199
199
|
"arguments-differ",
|
|
200
|
-
"attribute-defined-outside-init", # old bad practice, fixed step-by-step
|
|
201
200
|
"broad-exception-caught",
|
|
202
201
|
"broad-exception-raised",
|
|
203
202
|
"consider-using-assignment-expr",
|
|
@@ -224,6 +224,8 @@ class CanvasError(Exception):
|
|
|
224
224
|
class Canvas:
|
|
225
225
|
"""base class for canvases."""
|
|
226
226
|
|
|
227
|
+
__slots__ = ("__dict__", "__weakref__", "_widget_info", "coords", "shortcuts")
|
|
228
|
+
|
|
227
229
|
cacheable = True
|
|
228
230
|
|
|
229
231
|
_finalized_error = CanvasError(
|
|
@@ -535,7 +537,10 @@ class TextCanvas(Canvas):
|
|
|
535
537
|
if not rows:
|
|
536
538
|
rows = maxrow - trim_top
|
|
537
539
|
|
|
538
|
-
|
|
540
|
+
# a canvas without columns (empty text) has one region only: all of it, zero columns wide
|
|
541
|
+
if not (
|
|
542
|
+
(0 <= trim_left < maxcol and cols > 0 and trim_left + cols <= maxcol) or maxcol == trim_left == cols == 0
|
|
543
|
+
):
|
|
539
544
|
raise ValueError(trim_left)
|
|
540
545
|
if not ((0 <= trim_top < maxrow) and (rows > 0 and trim_top + rows <= maxrow)):
|
|
541
546
|
raise ValueError(trim_top)
|
|
@@ -598,6 +603,8 @@ class BlankCanvas(Canvas):
|
|
|
598
603
|
Only works as part of a composite canvas since it doesn't know its own size.
|
|
599
604
|
"""
|
|
600
605
|
|
|
606
|
+
__slots__ = ()
|
|
607
|
+
|
|
601
608
|
def content(
|
|
602
609
|
self,
|
|
603
610
|
trim_left: int = 0,
|
|
@@ -730,6 +737,7 @@ class CompositeCanvas(Canvas):
|
|
|
730
737
|
|
|
731
738
|
# tuples that define the unfinished cviews that are part of shards following the first shard.
|
|
732
739
|
super().__init__()
|
|
740
|
+
self.depends_on: Sequence[AbstractWidget] | None = None
|
|
733
741
|
|
|
734
742
|
if canv is None:
|
|
735
743
|
self.shards: list[tuple[int, list[_CView]]] = []
|
|
@@ -122,6 +122,7 @@ class Screen(_raw_display_base.Screen):
|
|
|
122
122
|
super().__init__(input, output, bracketed_paste_mode=bracketed_paste_mode, focus_reporting=focus_reporting)
|
|
123
123
|
self.gpm_mev: Popen[str] | None = None
|
|
124
124
|
self.gpm_event_pending: bool = False
|
|
125
|
+
self._old_termios_settings: list[typing.Any] | None = None
|
|
125
126
|
|
|
126
127
|
# These store the previous signal handlers after setting ours
|
|
127
128
|
self._prev_sigcont_handler: SignalHandler = None
|
|
@@ -308,7 +309,7 @@ class Screen(_raw_display_base.Screen):
|
|
|
308
309
|
self._stop_restore_palette()
|
|
309
310
|
|
|
310
311
|
fd = self._input_fileno()
|
|
311
|
-
if fd is not None and os.isatty(fd):
|
|
312
|
+
if fd is not None and os.isatty(fd) and self._old_termios_settings is not None:
|
|
312
313
|
termios.tcsetattr(fd, termios.TCSAFLUSH, self._old_termios_settings)
|
|
313
314
|
|
|
314
315
|
if self._old_signal_keys:
|
|
@@ -41,7 +41,7 @@ from . import escape
|
|
|
41
41
|
from .common import UNPRINTABLE_TRANS_TABLE, UPDATE_PALETTE_ENTRY, AttrSpec, BaseScreen, RealTerminal
|
|
42
42
|
|
|
43
43
|
if typing.TYPE_CHECKING:
|
|
44
|
-
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
44
|
+
from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence
|
|
45
45
|
from types import FrameType
|
|
46
46
|
|
|
47
47
|
from typing_extensions import Literal
|
|
@@ -353,8 +353,8 @@ class Screen(BaseScreen, RealTerminal):
|
|
|
353
353
|
|
|
354
354
|
self._partial_codes: list[int] = []
|
|
355
355
|
self.modes = TermModes(bracketed_paste=bracketed_paste_mode, focus_reporting=focus_reporting)
|
|
356
|
-
self._pal_escape: dict[
|
|
357
|
-
self._pal_attrspec: dict[
|
|
356
|
+
self._pal_escape: dict[Hashable, str] = {}
|
|
357
|
+
self._pal_attrspec: dict[Hashable, AttrSpec] = {}
|
|
358
358
|
self._modified_palette_entries: set[int] = set()
|
|
359
359
|
signals.connect_signal(self, UPDATE_PALETTE_ENTRY, self._on_update_palette_entry)
|
|
360
360
|
self.term = os.environ.get("TERM", "")
|
|
@@ -423,7 +423,7 @@ class Screen(BaseScreen, RealTerminal):
|
|
|
423
423
|
|
|
424
424
|
return None
|
|
425
425
|
|
|
426
|
-
def _on_update_palette_entry(self, name:
|
|
426
|
+
def _on_update_palette_entry(self, name: Hashable, *attrspecs: AttrSpec) -> None:
|
|
427
427
|
# copy the attribute to a dictionary containing the escape seqences
|
|
428
428
|
a: AttrSpec = attrspecs[_ATTRSPEC_INDEX_BY_COLORS[self.colors]]
|
|
429
429
|
self._pal_attrspec[name] = a
|
|
@@ -1046,7 +1046,7 @@ class Screen(BaseScreen, RealTerminal):
|
|
|
1046
1046
|
run[-1:] == b" "
|
|
1047
1047
|
and self.back_color_erase
|
|
1048
1048
|
and not (
|
|
1049
|
-
isinstance(pal_a := self._pal_attrspec.get(a, a), AttrSpec)
|
|
1049
|
+
isinstance(pal_a := self._pal_attrspec.get(a, a), AttrSpec)
|
|
1050
1050
|
and (pal_a.standout or pal_a.underline)
|
|
1051
1051
|
)
|
|
1052
1052
|
):
|
|
@@ -1152,7 +1152,7 @@ class Screen(BaseScreen, RealTerminal):
|
|
|
1152
1152
|
|
|
1153
1153
|
def _attr_to_escape(self, a: AttrSpec | str | None) -> str:
|
|
1154
1154
|
"""Convert attribute instance a to an escape sequence for the terminal."""
|
|
1155
|
-
if found := self._pal_escape.get(a):
|
|
1155
|
+
if found := self._pal_escape.get(a):
|
|
1156
1156
|
return found
|
|
1157
1157
|
if isinstance(a, AttrSpec):
|
|
1158
1158
|
return self._attrspec_to_escape(a)
|
|
@@ -34,12 +34,31 @@ from urwid import signals
|
|
|
34
34
|
from urwid.util import StoppingContext, int_scale
|
|
35
35
|
|
|
36
36
|
if typing.TYPE_CHECKING:
|
|
37
|
-
from collections.abc import Callable, Iterable, Sequence
|
|
37
|
+
from collections.abc import Callable, Hashable, Iterable, Sequence
|
|
38
38
|
|
|
39
39
|
from typing_extensions import Literal, Self
|
|
40
40
|
|
|
41
41
|
from urwid import Canvas
|
|
42
42
|
|
|
43
|
+
# One register_palette() item:
|
|
44
|
+
# (name, like_other_name)
|
|
45
|
+
# or the 3, 4 or 6 positional arguments of register_palette_entry().
|
|
46
|
+
# A name is any hashable value, None being the default attribute.
|
|
47
|
+
# typing.Union rather than "|": the alias is evaluated by the checkers as Python 3.9 code.
|
|
48
|
+
PaletteEntry = typing.Union[
|
|
49
|
+
tuple[Hashable, Hashable],
|
|
50
|
+
tuple[Hashable, str, str],
|
|
51
|
+
tuple[Hashable, str, str, typing.Union[str, tuple[str, ...], None]],
|
|
52
|
+
tuple[
|
|
53
|
+
Hashable,
|
|
54
|
+
str,
|
|
55
|
+
str,
|
|
56
|
+
typing.Union[str, tuple[str, ...], None],
|
|
57
|
+
typing.Optional[str],
|
|
58
|
+
typing.Optional[str],
|
|
59
|
+
],
|
|
60
|
+
]
|
|
61
|
+
|
|
43
62
|
IS_WINDOWS = sys.platform == "win32"
|
|
44
63
|
|
|
45
64
|
# for replacing unprintable bytes with '?'
|
|
@@ -1089,7 +1108,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
|
|
|
1089
1108
|
|
|
1090
1109
|
self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
|
|
1091
1110
|
|
|
1092
|
-
self._palette: dict[
|
|
1111
|
+
self._palette: dict[Hashable, tuple[AttrSpec, AttrSpec, AttrSpec, AttrSpec, AttrSpec]] = {}
|
|
1093
1112
|
self._started: bool = False
|
|
1094
1113
|
|
|
1095
1114
|
@property
|
|
@@ -1170,12 +1189,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
|
|
|
1170
1189
|
"""
|
|
1171
1190
|
return 80, 24
|
|
1172
1191
|
|
|
1173
|
-
def register_palette(
|
|
1174
|
-
self,
|
|
1175
|
-
palette: Iterable[
|
|
1176
|
-
tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str]
|
|
1177
|
-
],
|
|
1178
|
-
) -> None:
|
|
1192
|
+
def register_palette(self, palette: Iterable[PaletteEntry]) -> None:
|
|
1179
1193
|
"""Register a set of palette entries.
|
|
1180
1194
|
|
|
1181
1195
|
:param palette: a list of (name, like_other_name) or
|
|
@@ -1204,7 +1218,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
|
|
|
1204
1218
|
|
|
1205
1219
|
def register_palette_entry(
|
|
1206
1220
|
self,
|
|
1207
|
-
name:
|
|
1221
|
+
name: Hashable,
|
|
1208
1222
|
foreground: str,
|
|
1209
1223
|
background: str,
|
|
1210
1224
|
mono: str | tuple[str, ...] | None = None,
|
|
@@ -1213,7 +1227,7 @@ class BaseScreen(abc.ABC, metaclass=signals.MetaSignals):
|
|
|
1213
1227
|
) -> None:
|
|
1214
1228
|
"""Register a single palette entry.
|
|
1215
1229
|
|
|
1216
|
-
:param name: new entry/attribute name
|
|
1230
|
+
:param name: new entry/attribute name, any hashable value; ``None`` names the default attribute
|
|
1217
1231
|
|
|
1218
1232
|
:param foreground: a string containing a comma-separated foreground
|
|
1219
1233
|
color and settings
|
|
@@ -117,6 +117,7 @@ class Screen(BaseScreen, RealTerminal):
|
|
|
117
117
|
super().__init__()
|
|
118
118
|
self.curses_pairs = [(None, None)] # Can't be sure what pair 0 will default to
|
|
119
119
|
self.has_color = False
|
|
120
|
+
self.has_default_colors = False
|
|
120
121
|
self.s: curses.window | None = None
|
|
121
122
|
self.cursor_state: typing.Literal["fixed"] | int | None = None
|
|
122
123
|
self.prev_input_resize = 0
|
|
@@ -124,6 +125,8 @@ class Screen(BaseScreen, RealTerminal):
|
|
|
124
125
|
self.last_bstate = 0
|
|
125
126
|
self._mouse_tracking_enabled = False
|
|
126
127
|
self._curses_attr_cache: dict[AttrSpec, int] = {}
|
|
128
|
+
# Holds the last drawn canvas so the canvas cache keeps its entries alive.
|
|
129
|
+
self.keep_cache_alive_link: Canvas | None = None
|
|
127
130
|
|
|
128
131
|
self.register_palette_entry(None, "default", "default")
|
|
129
132
|
|
|
@@ -41,7 +41,7 @@ if typing.TYPE_CHECKING:
|
|
|
41
41
|
|
|
42
42
|
# NOTE: because of circular imports (urwid.util -> urwid.escape -> urwid.util)
|
|
43
43
|
# from urwid.util import is_mouse_event -- will not work here
|
|
44
|
-
import urwid.util # isort: skip
|
|
44
|
+
import urwid.util # isort: skip
|
|
45
45
|
|
|
46
46
|
IS_WINDOWS = sys.platform == "win32"
|
|
47
47
|
|
|
@@ -130,6 +130,15 @@ class Screen(BaseScreen):
|
|
|
130
130
|
self.bright_is_bold = False # ignored: the browser renders bold as requested
|
|
131
131
|
self.has_underline = True # ignored: the browser renders underline as requested
|
|
132
132
|
self._colour_index = _COLOUR_INDEX[self.colors]
|
|
133
|
+
self.update_method = ""
|
|
134
|
+
self.last_screen: dict[tuple[tuple[AttrSpec | str | None, str] | int | None, ...], list[int]] = {}
|
|
135
|
+
self.last_screen_width = 0
|
|
136
|
+
self.pipe_name = ""
|
|
137
|
+
self.input_fd: int | None = None
|
|
138
|
+
self.input_tail = ""
|
|
139
|
+
self.content_head = ""
|
|
140
|
+
self.screen_size: tuple[int, int] | None = None
|
|
141
|
+
self.server_socket: socket.socket | None = None
|
|
133
142
|
self.register_palette_entry(None, _default_foreground, _default_background)
|
|
134
143
|
|
|
135
144
|
@property
|
|
@@ -216,7 +225,7 @@ class Screen(BaseScreen):
|
|
|
216
225
|
sys.stdout.write("Status: 400 Bad Request\r\n\r\n")
|
|
217
226
|
sys.exit(0)
|
|
218
227
|
|
|
219
|
-
self.last_screen
|
|
228
|
+
self.last_screen = {}
|
|
220
229
|
self.last_screen_width = 0
|
|
221
230
|
|
|
222
231
|
clients = glob.glob(os.path.join(_prefs.pipe_dir, "urwid*.in"))
|
|
@@ -252,8 +261,9 @@ class Screen(BaseScreen):
|
|
|
252
261
|
with suppress(Exception):
|
|
253
262
|
self._close_connection()
|
|
254
263
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
if self.input_fd is not None:
|
|
265
|
+
with suppress(Exception):
|
|
266
|
+
os.close(self.input_fd)
|
|
257
267
|
self._cleanup_pipe()
|
|
258
268
|
self._started = False
|
|
259
269
|
|
|
@@ -261,7 +271,7 @@ class Screen(BaseScreen):
|
|
|
261
271
|
"""Not supported for web display."""
|
|
262
272
|
|
|
263
273
|
def _close_connection(self) -> None:
|
|
264
|
-
if self.update_method == "polling child":
|
|
274
|
+
if self.update_method == "polling child" and self.server_socket is not None:
|
|
265
275
|
self.server_socket.settimeout(0)
|
|
266
276
|
sock, _addr = self.server_socket.accept()
|
|
267
277
|
sock.sendall(b"Z")
|
|
@@ -303,7 +313,7 @@ class Screen(BaseScreen):
|
|
|
303
313
|
elif self.update_method == "polling child":
|
|
304
314
|
signal.alarm(0)
|
|
305
315
|
try:
|
|
306
|
-
s, _addr = self.server_socket.accept()
|
|
316
|
+
s, _addr = typing.cast("socket.socket", self.server_socket).accept()
|
|
307
317
|
except socket.timeout:
|
|
308
318
|
sys.exit(0)
|
|
309
319
|
send = s.sendall # type: ignore[assignment] # use default flags
|
|
@@ -409,7 +419,7 @@ class Screen(BaseScreen):
|
|
|
409
419
|
if self.update_method == "polling child":
|
|
410
420
|
# send empty update
|
|
411
421
|
try:
|
|
412
|
-
s, _addr = self.server_socket.accept()
|
|
422
|
+
s, _addr = typing.cast("socket.socket", self.server_socket).accept()
|
|
413
423
|
s.close()
|
|
414
424
|
except socket.timeout:
|
|
415
425
|
sys.exit(0)
|
|
@@ -421,7 +431,7 @@ class Screen(BaseScreen):
|
|
|
421
431
|
|
|
422
432
|
def get_cols_rows(self) -> tuple[int, int]:
|
|
423
433
|
"""Return the screen size."""
|
|
424
|
-
return self.screen_size
|
|
434
|
+
return typing.cast("tuple[int, int]", self.screen_size)
|
|
425
435
|
|
|
426
436
|
@typing.overload
|
|
427
437
|
def get_input(self, raw_keys: Literal[False]) -> list[str]: ...
|
|
@@ -433,8 +443,9 @@ class Screen(BaseScreen):
|
|
|
433
443
|
"""Return pending input as a list."""
|
|
434
444
|
pending_input = []
|
|
435
445
|
resized = False
|
|
446
|
+
input_fd = typing.cast("int", self.input_fd)
|
|
436
447
|
with selectors.DefaultSelector() as selector:
|
|
437
|
-
selector.register(
|
|
448
|
+
selector.register(input_fd, selectors.EVENT_READ)
|
|
438
449
|
|
|
439
450
|
iready = [event.fd for event, _ in selector.select(0.5)]
|
|
440
451
|
|
|
@@ -443,8 +454,8 @@ class Screen(BaseScreen):
|
|
|
443
454
|
return [], []
|
|
444
455
|
return []
|
|
445
456
|
|
|
446
|
-
keydata = os.read(
|
|
447
|
-
os.close(
|
|
457
|
+
keydata = os.read(input_fd, MAX_READ).decode(get_encoding())
|
|
458
|
+
os.close(input_fd)
|
|
448
459
|
self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
|
|
449
460
|
# sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
|
|
450
461
|
keys = keydata.split("\n")
|
|
@@ -48,6 +48,7 @@ if typing.TYPE_CHECKING:
|
|
|
48
48
|
from typing_extensions import Literal, Self
|
|
49
49
|
|
|
50
50
|
from urwid.display import BaseScreen
|
|
51
|
+
from urwid.display.common import PaletteEntry
|
|
51
52
|
from urwid.widget import AbstractWidget
|
|
52
53
|
|
|
53
54
|
from .abstract_loop import EventLoop
|
|
@@ -143,12 +144,27 @@ class MainLoop:
|
|
|
143
144
|
:attr:`event_loop`; whether it actually does anything still depends on :attr:`event_loop`.
|
|
144
145
|
"""
|
|
145
146
|
|
|
147
|
+
__slots__ = (
|
|
148
|
+
"__dict__",
|
|
149
|
+
"__weakref__",
|
|
150
|
+
"_input_filter",
|
|
151
|
+
"_pop_ups",
|
|
152
|
+
"_topmost_widget",
|
|
153
|
+
"_unhandled_input",
|
|
154
|
+
"_watch_pipes",
|
|
155
|
+
"_widget",
|
|
156
|
+
"event_loop",
|
|
157
|
+
"handle_mouse",
|
|
158
|
+
"idle_handle",
|
|
159
|
+
"logger",
|
|
160
|
+
"screen",
|
|
161
|
+
"screen_size",
|
|
162
|
+
)
|
|
163
|
+
|
|
146
164
|
def __init__(
|
|
147
165
|
self,
|
|
148
166
|
widget: AbstractWidget,
|
|
149
|
-
palette: Iterable[
|
|
150
|
-
tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str]
|
|
151
|
-
] = (),
|
|
167
|
+
palette: Iterable[PaletteEntry] = (),
|
|
152
168
|
screen: BaseScreen | None = None,
|
|
153
169
|
handle_mouse: bool = True,
|
|
154
170
|
input_filter: (
|
|
@@ -187,6 +203,7 @@ class MainLoop:
|
|
|
187
203
|
if event_loop is None:
|
|
188
204
|
event_loop = SelectEventLoop()
|
|
189
205
|
self.event_loop: EventLoop = event_loop
|
|
206
|
+
self.idle_handle: typing.Any = None
|
|
190
207
|
|
|
191
208
|
if hasattr(self.screen, "signal_handler_setter"):
|
|
192
209
|
# Tell the screen what function it must use to set
|
|
@@ -477,7 +494,7 @@ class MainLoop:
|
|
|
477
494
|
Only call this if you're managing the event loop yourself, after the loop stops.
|
|
478
495
|
"""
|
|
479
496
|
self.event_loop.remove_enter_idle(self.idle_handle)
|
|
480
|
-
|
|
497
|
+
self.idle_handle = None
|
|
481
498
|
signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors)
|
|
482
499
|
typing.cast("_ExternalLoopScreen", self.screen).unhook_event_loop(self.event_loop)
|
|
483
500
|
|
|
@@ -39,7 +39,9 @@ if sys.version_info < (3, 11):
|
|
|
39
39
|
from exceptiongroup import BaseExceptionGroup # pylint: disable=redefined-builtin # backport
|
|
40
40
|
|
|
41
41
|
if typing.TYPE_CHECKING:
|
|
42
|
+
import asyncio
|
|
42
43
|
from collections.abc import Awaitable, Callable
|
|
44
|
+
from concurrent.futures import Executor, Future
|
|
43
45
|
|
|
44
46
|
from typing_extensions import ParamSpec
|
|
45
47
|
|
|
@@ -96,6 +98,22 @@ class TrioEventLoop(EventLoop):
|
|
|
96
98
|
self._sleep = trio.sleep
|
|
97
99
|
self._wait_readable = trio.lowlevel.wait_readable
|
|
98
100
|
|
|
101
|
+
def run_in_executor(
|
|
102
|
+
self,
|
|
103
|
+
executor: Executor,
|
|
104
|
+
func: Callable[_Spec, _T],
|
|
105
|
+
*args: _Spec.args,
|
|
106
|
+
**kwargs: _Spec.kwargs,
|
|
107
|
+
) -> Future[_T] | asyncio.Future[_T]:
|
|
108
|
+
"""Raise :exc:`NotImplementedError`: use Trio's own thread API.
|
|
109
|
+
|
|
110
|
+
:raises NotImplementedError: Trio runs blocking calls in threads itself; use ``trio.to_thread.run_sync``.
|
|
111
|
+
"""
|
|
112
|
+
raise NotImplementedError(
|
|
113
|
+
"Trio implements its own worker threads. Please use native API for call:\n"
|
|
114
|
+
"'await trio.to_thread.run_sync(Callable[..., T], *args)'"
|
|
115
|
+
)
|
|
116
|
+
|
|
99
117
|
def alarm(
|
|
100
118
|
self,
|
|
101
119
|
seconds: float,
|
|
@@ -121,26 +121,33 @@ class StandardTextLayout(TextLayout):
|
|
|
121
121
|
A tab character advances to the next tab stop, measured in rendered screen columns
|
|
122
122
|
from the start of the displayed line: explicit *tab_stops* are used first,
|
|
123
123
|
then stops repeat every *tab_stop_every* columns counted from column 0.
|
|
124
|
+
With *tab_stop_every* ``0`` a tab past the last explicit stop takes no columns.
|
|
124
125
|
|
|
125
126
|
:param tab_stops: screen columns of explicit tab stops.
|
|
126
|
-
:param tab_stop_every: interval of the default tab stops.
|
|
127
|
-
:raises ValueError: *tab_stop_every* or one of *tab_stops* is not a positive column.
|
|
127
|
+
:param tab_stop_every: interval of the default tab stops, ``0`` for none.
|
|
128
|
+
:raises ValueError: *tab_stop_every* is negative, or one of *tab_stops* is not a positive column.
|
|
128
129
|
"""
|
|
129
130
|
self.tab_stops: tuple[int, ...] = tuple(sorted(set(tab_stops)))
|
|
130
|
-
if tab_stop_every <
|
|
131
|
+
if tab_stop_every < 0 or (self.tab_stops and self.tab_stops[0] < 1):
|
|
131
132
|
raise ValueError(f"Tab stops must be positive: {tab_stops=!r}, {tab_stop_every=!r}")
|
|
132
133
|
self.tab_stop_every = tab_stop_every
|
|
133
134
|
|
|
134
135
|
def next_tab_stop(self, column: int) -> int:
|
|
135
136
|
"""Return the screen column of the first tab stop after *column*.
|
|
136
137
|
|
|
138
|
+
When there is no stop after *column*, return *column* itself: the tab takes no columns.
|
|
139
|
+
|
|
137
140
|
>>> StandardTextLayout(tab_stops=(4, 10)).next_tab_stop(5)
|
|
138
141
|
10
|
|
139
142
|
>>> StandardTextLayout(tab_stops=(4, 10)).next_tab_stop(10)
|
|
140
143
|
16
|
|
144
|
+
>>> StandardTextLayout(tab_stops=(4, 10), tab_stop_every=0).next_tab_stop(10)
|
|
145
|
+
10
|
|
141
146
|
"""
|
|
142
147
|
if (idx := bisect.bisect_right(self.tab_stops, column)) < len(self.tab_stops):
|
|
143
148
|
return self.tab_stops[idx]
|
|
149
|
+
if not self.tab_stop_every:
|
|
150
|
+
return column
|
|
144
151
|
return (column // self.tab_stop_every + 1) * self.tab_stop_every
|
|
145
152
|
|
|
146
153
|
def supports_align_mode(self, align: Literal["left", "center", "right"] | Align) -> bool:
|
|
@@ -305,7 +312,10 @@ class StandardTextLayout(TextLayout):
|
|
|
305
312
|
pos = start
|
|
306
313
|
while pos < end:
|
|
307
314
|
if text[pos : pos + 1] == tab:
|
|
308
|
-
tab_width
|
|
315
|
+
if not (tab_width := self.next_tab_stop(column) - column):
|
|
316
|
+
line.append((0, pos))
|
|
317
|
+
pos += 1
|
|
318
|
+
continue
|
|
309
319
|
if limit is not None and column + tab_width > limit:
|
|
310
320
|
if limit > column:
|
|
311
321
|
line.append((limit - column, pos, b" " * (limit - column)))
|
|
@@ -355,7 +365,10 @@ class StandardTextLayout(TextLayout):
|
|
|
355
365
|
pos = start
|
|
356
366
|
while pos < end:
|
|
357
367
|
if text[pos : pos + 1] == tab:
|
|
358
|
-
if column
|
|
368
|
+
if self.next_tab_stop(column) == column:
|
|
369
|
+
line.append((0, pos))
|
|
370
|
+
pos += 1
|
|
371
|
+
elif column < width:
|
|
359
372
|
tab_width = min(self.next_tab_stop(column), width) - column
|
|
360
373
|
line.append((tab_width, pos, b" " * tab_width))
|
|
361
374
|
column += tab_width
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '4.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (4, 2,
|
|
21
|
+
__version__ = version = '4.2.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (4, 2, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gd131ddace'
|
|
@@ -1377,6 +1377,7 @@ class Terminal(Widget):
|
|
|
1377
1377
|
self.height: int | None = None
|
|
1378
1378
|
self.term: TermCanvas | None = None
|
|
1379
1379
|
self.has_focus = False
|
|
1380
|
+
self.old_tios: tuple[int, int, int, int, int] | None = None
|
|
1380
1381
|
self.terminated = False
|
|
1381
1382
|
|
|
1382
1383
|
def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
|
|
@@ -1519,8 +1520,8 @@ class Terminal(Widget):
|
|
|
1519
1520
|
if has_focus:
|
|
1520
1521
|
self.old_tios = RealTerminal().tty_signal_keys()
|
|
1521
1522
|
RealTerminal().tty_signal_keys(*(["undefined"] * 5)) # type: ignore[arg-type]
|
|
1522
|
-
elif
|
|
1523
|
-
RealTerminal().tty_signal_keys(*old_tios)
|
|
1523
|
+
elif self.old_tios:
|
|
1524
|
+
RealTerminal().tty_signal_keys(*self.old_tios)
|
|
1524
1525
|
|
|
1525
1526
|
def render(self, size: tuple[int, int], focus: bool = False) -> TermCanvas: # type: ignore[override]
|
|
1526
1527
|
"""Resize the terminal to *size*, read any pending child output, and return the terminal canvas."""
|
|
@@ -22,7 +22,7 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
import typing
|
|
24
24
|
|
|
25
|
-
from urwid.ansi_parser import AnsiParser
|
|
25
|
+
from urwid.ansi_parser import AnsiParser, ParsedLine
|
|
26
26
|
|
|
27
27
|
from .constants import Align, WrapMode
|
|
28
28
|
from .text import Text
|
|
@@ -33,7 +33,6 @@ if typing.TYPE_CHECKING:
|
|
|
33
33
|
|
|
34
34
|
from typing_extensions import Literal
|
|
35
35
|
|
|
36
|
-
from urwid.ansi_parser import ParsedLine
|
|
37
36
|
from urwid.display import AttrSpec
|
|
38
37
|
|
|
39
38
|
__all__ = ("ANSIText",)
|
|
@@ -93,6 +92,7 @@ class ANSIText(WidgetWrap[Text]):
|
|
|
93
92
|
"""
|
|
94
93
|
self._one_line = one_line
|
|
95
94
|
super().__init__(Text("", align=align, wrap=wrap))
|
|
95
|
+
self._parsed = ParsedLine("", [], previous_attr)
|
|
96
96
|
self.set_ansi_text(ansi_text, previous_attr)
|
|
97
97
|
|
|
98
98
|
def _repr_words(self) -> list[str]:
|
|
@@ -67,18 +67,10 @@ class AttrMap(
|
|
|
67
67
|
[('greeting', None, ...'hi'), ('bg', None, ...' ')]
|
|
68
68
|
"""
|
|
69
69
|
super().__init__(w)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
self.attr_map = {None: attr_map}
|
|
75
|
-
|
|
76
|
-
if isinstance(focus_map, Mapping):
|
|
77
|
-
self.focus_map = dict(focus_map)
|
|
78
|
-
elif focus_map is None:
|
|
79
|
-
self.focus_map = focus_map
|
|
80
|
-
else:
|
|
81
|
-
self.focus_map = {None: focus_map}
|
|
70
|
+
self._attr_map: dict[Hashable, Hashable] = {}
|
|
71
|
+
self._focus_map: dict[Hashable, Hashable] | None = None
|
|
72
|
+
self.set_attr_map(attr_map)
|
|
73
|
+
self.set_focus_map(focus_map)
|
|
82
74
|
|
|
83
75
|
def _repr_attrs(self) -> dict[str, typing.Any]:
|
|
84
76
|
# only include the focus_attr when it takes effect (not None)
|
|
@@ -93,28 +85,31 @@ class AttrMap(
|
|
|
93
85
|
# FIXME: a dictionary that detects modifications would be better
|
|
94
86
|
return dict(self._attr_map)
|
|
95
87
|
|
|
96
|
-
def set_attr_map(self, attr_map:
|
|
88
|
+
def set_attr_map(self, attr_map: Hashable | Mapping[Hashable, Hashable]) -> None:
|
|
97
89
|
"""
|
|
98
90
|
Set the attribute mapping dictionary {from_attr: to_attr, ...}.
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
constructor does. You must specify {None: attribute} instead.
|
|
92
|
+
A single attribute is stored as ``{None: attribute}``, the way the constructor does.
|
|
102
93
|
|
|
103
94
|
>>> from urwid import Text
|
|
104
95
|
>>> w = AttrMap(Text("hi"), None)
|
|
105
96
|
>>> w.set_attr_map({"a": "b"})
|
|
106
97
|
>>> w
|
|
107
98
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={'a': 'b'}>
|
|
99
|
+
>>> w.set_attr_map("c")
|
|
100
|
+
>>> w
|
|
101
|
+
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={None: 'c'}>
|
|
108
102
|
|
|
109
103
|
:raises AttrMapError: a key or value of the mapping is not hashable.
|
|
110
104
|
"""
|
|
111
|
-
|
|
105
|
+
mapping: dict[Hashable, Hashable] = dict(attr_map) if isinstance(attr_map, Mapping) else {None: attr_map}
|
|
106
|
+
for from_attr, to_attr in mapping.items():
|
|
112
107
|
if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
|
|
113
108
|
raise AttrMapError(
|
|
114
109
|
f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
|
|
115
110
|
)
|
|
116
111
|
|
|
117
|
-
self._attr_map =
|
|
112
|
+
self._attr_map = mapping
|
|
118
113
|
self._invalidate()
|
|
119
114
|
|
|
120
115
|
attr_map = property(get_attr_map, set_attr_map)
|
|
@@ -127,33 +122,39 @@ class AttrMap(
|
|
|
127
122
|
return dict(self._focus_map)
|
|
128
123
|
return None
|
|
129
124
|
|
|
130
|
-
def set_focus_map(self, focus_map:
|
|
125
|
+
def set_focus_map(self, focus_map: Hashable | Mapping[Hashable, Hashable] | None) -> None:
|
|
131
126
|
"""Set the focus attribute mapping dictionary {from_attr: to_attr, ...}.
|
|
132
127
|
|
|
133
128
|
If None this widget will use the attr mapping instead (no change
|
|
134
129
|
when in focus).
|
|
135
130
|
|
|
136
|
-
|
|
137
|
-
constructor does. You must specify {None: attribute} instead.
|
|
131
|
+
A single attribute is stored as ``{None: attribute}``, the way the constructor does.
|
|
138
132
|
|
|
139
133
|
>>> from urwid import Text
|
|
140
134
|
>>> w = AttrMap(Text("hi"), {})
|
|
141
135
|
>>> w.set_focus_map({"a": "b"})
|
|
142
136
|
>>> w
|
|
143
137
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={} focus_map={'a': 'b'}>
|
|
138
|
+
>>> w.set_focus_map("c")
|
|
139
|
+
>>> w
|
|
140
|
+
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={} focus_map={None: 'c'}>
|
|
144
141
|
>>> w.set_focus_map(None)
|
|
145
142
|
>>> w
|
|
146
143
|
<AttrMap fixed/flow widget <Text fixed/flow widget 'hi'> attr_map={}>
|
|
147
144
|
|
|
148
145
|
:raises AttrMapError: a key or value of the mapping is not hashable.
|
|
149
146
|
"""
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
mapping: dict[Hashable, Hashable] | None
|
|
148
|
+
if focus_map is None:
|
|
149
|
+
mapping = None
|
|
150
|
+
else:
|
|
151
|
+
mapping = dict(focus_map) if isinstance(focus_map, Mapping) else {None: focus_map}
|
|
152
|
+
for from_attr, to_attr in mapping.items():
|
|
152
153
|
if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
|
|
153
154
|
raise AttrMapError(
|
|
154
155
|
f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
|
|
155
156
|
)
|
|
156
|
-
self._focus_map =
|
|
157
|
+
self._focus_map = mapping
|
|
157
158
|
self._invalidate()
|
|
158
159
|
|
|
159
160
|
focus_map = property(get_focus_map, set_focus_map)
|
|
@@ -114,7 +114,7 @@ class AttrWrap(AttrMap[WrappedWidget]):
|
|
|
114
114
|
>> w
|
|
115
115
|
<AttrWrap flow widget <Divider flow widget '-'> attr='new_attr'>
|
|
116
116
|
"""
|
|
117
|
-
self.set_attr_map(
|
|
117
|
+
self.set_attr_map(attr)
|
|
118
118
|
|
|
119
119
|
attr = property(get_attr, set_attr)
|
|
120
120
|
|
|
@@ -138,7 +138,7 @@ class AttrWrap(AttrMap[WrappedWidget]):
|
|
|
138
138
|
>> w
|
|
139
139
|
<AttrWrap flow widget <Divider flow widget '-'> attr='old'>
|
|
140
140
|
"""
|
|
141
|
-
self.set_focus_map(
|
|
141
|
+
self.set_focus_map(focus_attr)
|
|
142
142
|
|
|
143
143
|
focus_attr = property(get_focus_attr, set_focus_attr)
|
|
144
144
|
|
|
@@ -487,7 +487,8 @@ class Edit(WidgetWrap[Text]):
|
|
|
487
487
|
if idx:
|
|
488
488
|
column = layout.next_tab_stop(column)
|
|
489
489
|
column += calc_width(chunk, 0, len(chunk))
|
|
490
|
-
|
|
490
|
+
if spaces := layout.next_tab_stop(column) - column:
|
|
491
|
+
self.insert_text(" " * spaces)
|
|
491
492
|
return None
|
|
492
493
|
|
|
493
494
|
if key == "enter" and self.multiline:
|
|
@@ -675,6 +676,11 @@ class Edit(WidgetWrap[Text]):
|
|
|
675
676
|
if not self._shift_view_to_cursor:
|
|
676
677
|
return trans
|
|
677
678
|
|
|
679
|
+
if self._w.wrap in (WrapMode.SPACE, WrapMode.ANY):
|
|
680
|
+
# Wrapping modes grow the number of rows instead of clipping a row's width,
|
|
681
|
+
# so there is never a partially hidden row to scroll horizontally into view.
|
|
682
|
+
return trans
|
|
683
|
+
|
|
678
684
|
text, _ignore = self.get_text()
|
|
679
685
|
x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))
|
|
680
686
|
if x < 0:
|
|
@@ -703,7 +709,13 @@ class Edit(WidgetWrap[Text]):
|
|
|
703
709
|
(maxcol,) = size
|
|
704
710
|
|
|
705
711
|
self._shift_view_to_cursor = True
|
|
706
|
-
|
|
712
|
+
x, y = self.position_coords(maxcol, self.edit_pos)
|
|
713
|
+
if self._w.wrap in (WrapMode.SPACE, WrapMode.ANY) and x >= maxcol:
|
|
714
|
+
# The cursor sits right after a row that is already full and entirely visible.
|
|
715
|
+
# Clamp onto its last column instead of scrolling that row out of view for a wrap
|
|
716
|
+
# that has not happened yet.
|
|
717
|
+
x = maxcol - 1
|
|
718
|
+
return x, y
|
|
707
719
|
|
|
708
720
|
def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:
|
|
709
721
|
"""Return (*x*, *y*) coordinates for an offset into self.edit_text."""
|
|
@@ -192,7 +192,7 @@ class LineBox(
|
|
|
192
192
|
middle: Columns = typing.cast("Columns", self._wrapped_widget[v_index])
|
|
193
193
|
_old_widget, options = middle.contents[h_index]
|
|
194
194
|
middle.contents[h_index] = (original_widget, options) # type: ignore[assignment]
|
|
195
|
-
WidgetDecoration.original_widget.fset(self, original_widget) # type: ignore[attr-defined]
|
|
195
|
+
WidgetDecoration.original_widget.fset(self, original_widget) # type: ignore[attr-defined]
|
|
196
196
|
|
|
197
197
|
@property
|
|
198
198
|
def _w(self) -> Pile:
|
|
@@ -183,6 +183,12 @@ class Overlay(
|
|
|
183
183
|
|
|
184
184
|
self.top_w = top_w
|
|
185
185
|
self.bottom_w = bottom_w
|
|
186
|
+
self.min_width: int | None = None
|
|
187
|
+
self.min_height: int | None = None
|
|
188
|
+
self.left = 0
|
|
189
|
+
self.right = 0
|
|
190
|
+
self.top = 0
|
|
191
|
+
self.bottom = 0
|
|
186
192
|
|
|
187
193
|
self.set_overlay_parameters(align, width, valign, height, min_width, min_height, left, right, top, bottom)
|
|
188
194
|
|
|
@@ -129,6 +129,7 @@ class PopUpTarget(WidgetDecoration[WrappedWidget]):
|
|
|
129
129
|
# One (widget, Overlay) entry per currently open pop-up, outermost first.
|
|
130
130
|
self._pop_up_levels: list[tuple[AbstractWidget, Overlay[AbstractWidget, AbstractWidget]]] = []
|
|
131
131
|
self._current_widget: AbstractWidget = self._original_widget
|
|
132
|
+
self._cache_original_canvas: Canvas | None = None
|
|
132
133
|
|
|
133
134
|
def _update_overlay(self, size: tuple[int, int], focus: bool) -> None:
|
|
134
135
|
"""Rebuild :attr:`_current_widget` as a chain of Overlay widgets, one per open pop-up.
|
|
@@ -75,6 +75,8 @@ class Text(Widget):
|
|
|
75
75
|
self._layout: text_layout.TextLayout = layout or text_layout.default_layout
|
|
76
76
|
self._text: str | bytes
|
|
77
77
|
self._attrib: list[tuple[Hashable, int]]
|
|
78
|
+
self._align_mode: Literal["left", "center", "right"] | Align = Align.LEFT
|
|
79
|
+
self._wrap_mode: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE
|
|
78
80
|
self.set_text(markup)
|
|
79
81
|
self.set_layout(align, wrap, layout)
|
|
80
82
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pylint==4.0.9
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|