summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Stepanov <penguinolog@users.noreply.github.com>2023-04-06 13:28:21 +0200
committerGitHub <noreply@github.com>2023-04-06 13:28:21 +0200
commit0c0ea377ab9b418cbb5233fa6e178dd05f1f4e5a (patch)
tree605aec7db0b4849525051aa094cb4289cacc9b09
parent02d0fd689731d957f360858d3e3244a4c36166fb (diff)
downloadurwid-0c0ea377ab9b418cbb5233fa6e178dd05f1f4e5a.tar.gz
Add extra type annotations (#532)
* add basic mypy config for better tracking (now crazy amount of warnings without `strict`) * Useless check in `raw_display` (`if not Popen` will be always `False`) * use explicit `return None` Partial: #406 Related: #512 Related: #408 Co-authored-by: Aleksei Stepanov <alekseis@nvidia.com>
-rw-r--r--pyproject.toml16
-rw-r--r--urwid/canvas.py3
-rwxr-xr-xurwid/container.py86
-rwxr-xr-xurwid/curses_display.py6
-rwxr-xr-xurwid/decoration.py3
-rwxr-xr-xurwid/font.py1
-rw-r--r--urwid/lcd_display.py1
-rwxr-xr-xurwid/monitored_list.py2
-rw-r--r--urwid/raw_display.py5
-rw-r--r--urwid/vterm.py278
-rw-r--r--urwid/widget.py6
11 files changed, 240 insertions, 167 deletions
diff --git a/pyproject.toml b/pyproject.toml
index 7b8ae12..3992e6b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,4 +83,18 @@ exclude_lines = [
# OS Specific
"if platform.system()",
-] \ No newline at end of file
+]
+
+[tool.mypy]
+show_error_context = true
+show_column_numbers = true
+show_error_codes = true
+pretty = true
+
+[[tool.mypy.overrides]]
+module = [
+ "gi.*",
+ "trio.*",
+ "serial.*"
+]
+ignore_missing_imports = true \ No newline at end of file
diff --git a/urwid/canvas.py b/urwid/canvas.py
index b1f7c9c..7838ec2 100644
--- a/urwid/canvas.py
+++ b/urwid/canvas.py
@@ -252,8 +252,7 @@ class Canvas:
Return the text content of the canvas as a list of strings,
one for each row.
"""
- return [b''.join([text for (attr, cs, text) in row])
- for row in self.content()]
+ return [b''.join([text for (attr, cs, text) in row]) for row in self.content()]
text = property(_text_content, _raise_old_repr_error)
attr = property(_raise_old_repr_error, _raise_old_repr_error)
diff --git a/urwid/container.py b/urwid/container.py
index 376e8e5..9914e9b 100755
--- a/urwid/container.py
+++ b/urwid/container.py
@@ -171,12 +171,12 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi
return frozenset([FLOW])
def __init__(
- self,
- cells: Sequence[Widget],
- cell_width: int,
- h_sep: int,
- v_sep: int,
- align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
+ self,
+ cells: Sequence[Widget],
+ cell_width: int,
+ h_sep: int,
+ v_sep: int,
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
):
"""
:param cells: list of flow widgets to display
@@ -505,19 +505,19 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
TOP, None, RELATIVE, 100, None, 0, 0)
def __init__(
- self,
- top_w: Widget,
- bottom_w: Widget,
- align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
- width: Literal['pack'] | int | tuple[Literal['relative'], int],
- valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative'], int],
- height: Literal['pack'] | int | tuple[Literal['relative'], int],
- min_width: int | None = None,
- min_height: int | None = None,
- left: int = 0,
- right: int = 0,
- top: int = 0,
- bottom: int = 0,
+ self,
+ top_w: Widget,
+ bottom_w: Widget,
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative', 'fixed left', 'fixed right'], int],
+ width: Literal['pack'] | int | tuple[Literal['relative'], int],
+ valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative', 'fixed top', 'fixed bottom'], int],
+ height: Literal['pack'] | int | tuple[Literal['relative'], int],
+ min_width: int | None = None,
+ min_height: int | None = None,
+ left: int = 0,
+ right: int = 0,
+ top: int = 0,
+ bottom: int = 0,
) -> None:
"""
:param top_w: a flow, box or fixed widget to overlay "on top"
@@ -570,19 +570,20 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
self.top_w = top_w
self.bottom_w = bottom_w
- self.set_overlay_parameters(align, width, valign, height,
- min_width, min_height, left, right, top, bottom)
+ self.set_overlay_parameters(
+ align, width, valign, height, min_width, min_height, left, right, top, bottom
+ )
@staticmethod
def options(
align_type: Literal['left', 'center', 'right', 'relative'],
align_amount: int | None,
- width_type,
- width_amount,
+ width_type: Literal['clip', 'pack', 'relative', 'given'],
+ width_amount: int | None,
valign_type: Literal['top', 'middle', 'bottom', 'relative'],
valign_amount: int | None,
- height_type,
- height_amount,
+ height_type: Literal['flow', 'pack', 'relative', 'given'],
+ height_amount: int | None,
min_width: int | None = None,
min_height: int | None = None,
left: int = 0,
@@ -599,15 +600,28 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
See also :meth:`.set_overlay_parameters`
"""
- return (align_type, align_amount, width_type, width_amount,
- min_width, left, right, valign_type, valign_amount,
- height_type, height_amount, min_height, top, bottom)
+ return (
+ align_type,
+ align_amount,
+ width_type,
+ width_amount,
+ min_width,
+ left,
+ right,
+ valign_type,
+ valign_amount,
+ height_type,
+ height_amount,
+ min_height,
+ top,
+ bottom,
+ )
def set_overlay_parameters(
self,
- align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int],
+ align: Literal['left', 'center', 'right'] | tuple[Literal['relative', 'fixed left', 'fixed right'], int],
width: int | None,
- valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative'], int],
+ valign: Literal['top', 'middle', 'bottom'] | tuple[Literal['relative', 'fixed top', 'fixed bottom'], int],
height: int | None,
min_width: int | None = None,
min_height: int | None = None,
@@ -666,10 +680,14 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin):
min_height = None
# use container API to set the parameters
- self.contents[1] = (self.top_w, self.options(
- align_type, align_amount, width_type, width_amount,
- valign_type, valign_amount, height_type, height_amount,
- min_width, min_height, left, right, top, bottom))
+ self.contents[1] = (
+ self.top_w,
+ self.options(
+ align_type, align_amount, width_type, width_amount,
+ valign_type, valign_amount, height_type, height_amount,
+ min_width, min_height, left, right, top, bottom
+ )
+ )
def selectable(self) -> bool:
"""Return selectable from top_w."""
diff --git a/urwid/curses_display.py b/urwid/curses_display.py
index 31e0b22..e9519f7 100755
--- a/urwid/curses_display.py
+++ b/urwid/curses_display.py
@@ -586,10 +586,10 @@ class _test:
for c in self.l:
t = ""
a = []
- for p in f"{c} on black",f"{c} on dark blue",f"{c} on light gray":
+ for p in f"{c} on black", f"{c} on dark blue", f"{c} on light gray":
- a.append((p,27))
- t=t+ (p+27*" ")[:27]
+ a.append((p, 27))
+ t += (p + 27 * " ")[:27]
text.append( t )
attr.append( a )
diff --git a/urwid/decoration.py b/urwid/decoration.py
index bcdecde..f1ab16c 100755
--- a/urwid/decoration.py
+++ b/urwid/decoration.py
@@ -220,6 +220,7 @@ class AttrMap(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
# FIXME: a dictionary that detects modifications would be better
if self._focus_map:
return dict(self._focus_map)
+ return None
def set_focus_map(self, focus_map: dict[Hashable | None, Hashable]) -> None:
"""
@@ -460,7 +461,7 @@ class Padding(WidgetDecoration):
def __init__(
self,
w: Widget,
- align: Literal["left", "center", "right"] = LEFT,
+ align: Literal["left", "center", "right"] | tuple[Literal['relative'], int] = LEFT,
width: int | Literal['pack', 'clip'] | tuple[Literal['relative'], int] = RELATIVE_100,
min_width: int | None = None,
left: int = 0,
diff --git a/urwid/font.py b/urwid/font.py
index 295bcf1..6445678 100755
--- a/urwid/font.py
+++ b/urwid/font.py
@@ -112,6 +112,7 @@ class Font:
return obj
elif isinstance(obj, bytes):
return obj.decode(encoding, errors)
+ raise TypeError(f"{obj!r} is not str|bytes")
def add_glyphs(self, gdata):
d, utf8_required = separate_glyphs(gdata, self.height)
diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py
index 6e69577..edceead 100644
--- a/urwid/lcd_display.py
+++ b/urwid/lcd_display.py
@@ -246,6 +246,7 @@ class KeyRepeatSimulator:
return None
for key in self.pressed:
return max(0., self.pressed[key] + self.repeat_delay - time.time()), key
+ return None
def sent_event(self) -> None:
"""
diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py
index 13700c8..3d6d6fb 100755
--- a/urwid/monitored_list.py
+++ b/urwid/monitored_list.py
@@ -21,6 +21,7 @@
from __future__ import annotations
+import functools
import typing
from collections.abc import Callable
@@ -32,6 +33,7 @@ if typing.TYPE_CHECKING:
def _call_modified(fn: Callable[ArgSpec, Ret]) -> Callable[ArgSpec, Ret]:
+ @functools.wraps(fn)
def call_modified_wrapper(self: MonitoredList, *args, **kwargs):
rval = fn(self, *args, **kwargs)
self._modified()
diff --git a/urwid/raw_display.py b/urwid/raw_display.py
index 1b06862..23c00d7 100644
--- a/urwid/raw_display.py
+++ b/urwid/raw_display.py
@@ -209,10 +209,9 @@ class Screen(BaseScreen, RealTerminal):
def _start_gpm_tracking(self):
if not os.path.isfile("/usr/bin/mev"):
return
- if not os.environ.get('TERM',"").lower().startswith("linux"):
- return
- if not Popen:
+ if not os.environ.get('TERM', "").lower().startswith("linux"):
return
+
m = Popen(["/usr/bin/mev", "-e", "158"], stdin=PIPE, stdout=PIPE, close_fds=True, encoding="ascii")
fcntl.fcntl(m.stdout.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
self.gpm_mev = m
diff --git a/urwid/vterm.py b/urwid/vterm.py
index e234a76..486fc01 100644
--- a/urwid/vterm.py
+++ b/urwid/vterm.py
@@ -33,6 +33,8 @@ import struct
import sys
import time
import traceback
+import typing
+from collections.abc import Iterable, Mapping, Sequence
try:
import fcntl
@@ -47,6 +49,9 @@ from urwid.display_common import _BASIC_COLORS, AttrSpec, RealTerminal
from urwid.escape import ALT_DEC_SPECIAL_CHARS, DEC_SPECIAL_CHARS
from urwid.widget import BOX, Widget
+if typing.TYPE_CHECKING:
+ from typing_extensions import Literal
+
EOF = b''
ESC = chr(27)
@@ -138,11 +143,12 @@ CSI_COMMANDS = {
CHARSET_DEFAULT = 1
CHARSET_UTF8 = 2
+
class TermModes:
- def __init__(self):
+ def __init__(self) -> None:
self.reset()
- def reset(self):
+ def reset(self) -> None:
# ECMA-48
self.display_ctrl = False
self.insert = False
@@ -158,6 +164,7 @@ class TermModes:
# charset stuff
self.main_charset = CHARSET_DEFAULT
+
class TermCharset:
MAPPING = {
'default': None,
@@ -166,7 +173,7 @@ class TermCharset:
'user': None,
}
- def __init__(self):
+ def __init__(self) -> None:
self._g = [
'default',
'vt100',
@@ -174,16 +181,20 @@ class TermCharset:
self._sgr_mapping = False
+ # prepare defaults
+ self.active = 0
+ self.current: str | None = None
+
self.activate(0)
- def define(self, g, charset):
+ def define(self, g: int, charset: str) -> None:
"""
Redefine G'g' with new mapping.
"""
self._g[g] = charset
self.activate(g=self.active)
- def activate(self, g):
+ def activate(self, g: int) -> None:
"""
Activate the given charset slot.
"""
@@ -215,6 +226,7 @@ class TermCharset:
else:
return char
+
class TermScroller(list):
"""
List subclass that handles the terminal scrollback buffer,
@@ -238,10 +250,11 @@ class TermScroller(list):
self.trunc()
super().extend(seq)
+
class TermCanvas(Canvas):
cacheable = False
- def __init__(self, width, height, widget):
+ def __init__(self, width: int, height: int, widget: Terminal) -> None:
super().__init__()
self.width, self.height = width, height
@@ -254,12 +267,16 @@ class TermCanvas(Canvas):
self.utf8_eat_bytes = None
self.utf8_buffer = b''
+ self.escbuf = b''
self.coords["cursor"] = (0, 0, None)
+ self.term_cursor = [0, 0] # do not allow to shoot in the leg at `set_term_cursor`
+ self.cursor: tuple[int, int] | None = None
+
self.reset()
- def set_term_cursor(self, x=None, y=None):
+ def set_term_cursor(self, x: int | None = None, y: int | None = None) -> None:
"""
Set terminal cursor to x/y and update canvas cursor. If one or both axes
are omitted, use the values of the current position.
@@ -271,21 +288,19 @@ class TermCanvas(Canvas):
self.term_cursor = self.constrain_coords(x, y)
- if self.has_focus \
- and self.modes.visible_cursor \
- and self.scrolling_up < self.height - y:
+ if self.has_focus and self.modes.visible_cursor and self.scrolling_up < self.height - y:
self.cursor = (x, y + self.scrolling_up)
else:
self.cursor = None
- def reset_scroll(self):
+ def reset_scroll(self) -> None:
"""
Reset scrolling region to full terminal size.
"""
self.scrollregion_start = 0
self.scrollregion_end = self.height - 1
- def scroll_buffer(self, up=True, reset=False, lines=None):
+ def scroll_buffer(self, up: bool = True, reset: bool = False, lines: int | None = None) -> None:
"""
Scroll the scrolling buffer up (up=True) or down (up=False) the given
amount of lines or half the screen height.
@@ -314,7 +329,7 @@ class TermCanvas(Canvas):
self.set_term_cursor()
- def reset(self):
+ def reset(self) -> None:
"""
Reset the terminal.
"""
@@ -340,7 +355,7 @@ class TermCanvas(Canvas):
# initialize self.term
self.clear()
- def init_tabstops(self, extend=False):
+ def init_tabstops(self, extend: bool = False) -> None:
tablen, mod = divmod(self.width, 8)
if mod > 0:
tablen += 1
@@ -351,7 +366,7 @@ class TermCanvas(Canvas):
else:
self.tabstops = [1 << 0] * tablen
- def set_tabstop(self, x=None, remove=False, clear=False):
+ def set_tabstop(self, x: int | None = None, remove: bool = False, clear: bool = False) -> None:
if clear:
for tab in range(len(self.tabstops)):
self.tabstops[tab] = 0
@@ -366,17 +381,17 @@ class TermCanvas(Canvas):
else:
self.tabstops[div] |= (1 << mod)
- def is_tabstop(self, x=None):
+ def is_tabstop(self, x: int | None = None) -> bool:
if x is None:
x = self.term_cursor[0]
div, mod = divmod(x, 8)
return (self.tabstops[div] & (1 << mod)) > 0
- def empty_line(self, char=b' '):
+ def empty_line(self, char: bytes = b' '):
return [self.empty_char(char)] * self.width
- def empty_char(self, char=b' '):
+ def empty_char(self, char: bytes = b' '):
return (self.attrspec, self.charset.current, char)
def addstr(self, data):
@@ -387,7 +402,7 @@ class TermCanvas(Canvas):
for byte in data:
self.addbyte(byte)
- def resize(self, width, height):
+ def resize(self, width: int, height: int) -> None:
"""
Resize the terminal to the given width and height.
"""
@@ -440,7 +455,7 @@ class TermCanvas(Canvas):
# extend tabs
self.init_tabstops(extend=True)
- def set_g01(self, char, mod):
+ def set_g01(self, char: bytes, mod: str) -> None:
"""
Set G0 or G1 according to 'char' and modifier 'mod'.
"""
@@ -463,7 +478,7 @@ class TermCanvas(Canvas):
self.charset.define(g, cset)
- def parse_csi(self, char):
+ def parse_csi(self, char: bytes) -> None:
"""
Parse ECMA-48 CSI (Control Sequence Introducer) sequences.
"""
@@ -498,50 +513,50 @@ class TermCanvas(Canvas):
# unpacked tuples in CSI_COMMANDS.
pass
- def parse_noncsi(self, char, mod=None):
+ def parse_noncsi(self, char: bytes, mod: str | None = None) -> None:
"""
Parse escape sequences which are not CSI.
"""
if mod == b'#' and char == b'8':
self.decaln()
- elif mod == b'%': # select main character set
+ elif mod == b'%': # select main character set
if char == b'@':
self.modes.main_charset = CHARSET_DEFAULT
elif char in b'G8':
# 8 is obsolete and only for backwards compatibility
self.modes.main_charset = CHARSET_UTF8
- elif mod == b'(' or mod == b')': # define G0/G1
+ elif mod == b'(' or mod == b')': # define G0/G1
self.set_g01(char, mod)
- elif char == b'M': # reverse line feed
+ elif char == b'M': # reverse line feed
self.linefeed(reverse=True)
- elif char == b'D': # line feed
+ elif char == b'D': # line feed
self.linefeed()
- elif char == b'c': # reset terminal
+ elif char == b'c': # reset terminal
self.reset()
- elif char == b'E': # newline
+ elif char == b'E': # newline
self.newline()
- elif char == b'H': # set tabstop
+ elif char == b'H': # set tabstop
self.set_tabstop()
- elif char == b'Z': # DECID
+ elif char == b'Z': # DECID
self.widget.respond(f"{ESC}[?6c")
- elif char == b'7': # save current state
+ elif char == b'7': # save current state
self.save_cursor(with_attrs=True)
- elif char == b'8': # restore current state
+ elif char == b'8': # restore current state
self.restore_cursor(with_attrs=True)
- def parse_osc(self, buf):
+ def parse_osc(self, buf: bytes) -> None:
"""
Parse operating system command.
"""
- if buf.startswith(b';'): # set window title and icon
+ if buf.startswith(b';'): # set window title and icon
self.widget.set_title(buf[1:])
- elif buf.startswith(b'3;'): # set window title
+ elif buf.startswith(b'3;'): # set window title
self.widget.set_title(buf[2:])
- def parse_escape(self, char):
+ def parse_escape(self, char: bytes) -> None:
if self.parsestate == 1:
# within CSI
- if char in CSI_COMMANDS.keys():
+ if char in CSI_COMMANDS:
self.parse_csi(char)
self.parsestate = 0
elif char in b'0123456789;' or (not self.escbuf and char == b'?'):
@@ -558,8 +573,7 @@ class TermCanvas(Canvas):
elif self.parsestate == 2 and self.escbuf[-1:] + char == f"{ESC}\\".encode('iso8859-1'):
# end of OSC
self.parse_osc(self.escbuf[:-1].lstrip(b'0'))
- elif self.parsestate == 2 and self.escbuf.startswith(b'P') and \
- len(self.escbuf) == 8:
+ elif self.parsestate == 2 and self.escbuf.startswith(b'P') and len(self.escbuf) == 8:
# set palette (ESC]Pnrrggbb)
pass
elif self.parsestate == 2 and not self.escbuf and char == b'R':
@@ -585,12 +599,12 @@ class TermCanvas(Canvas):
self.leave_escape()
- def leave_escape(self):
+ def leave_escape(self) -> None:
self.within_escape = False
self.parsestate = 0
self.escbuf = b''
- def get_utf8_len(self, bytenum):
+ def get_utf8_len(self, bytenum: int) -> int:
"""
Process startbyte and return the number of bytes following it to get a
valid UTF-8 multibyte sequence.
@@ -605,7 +619,7 @@ class TermCanvas(Canvas):
return length
- def addbyte(self, byte):
+ def addbyte(self, byte: int) -> None:
"""
Parse main charset and add the processed byte(s) to the terminal state
machine.
@@ -628,7 +642,7 @@ class TermCanvas(Canvas):
else:
# end multibyte sequence
self.utf8_eat_bytes = None
- sequence = (self.utf8_buffer+bytes([byte])).decode('utf-8', 'ignore')
+ sequence = (self.utf8_buffer + bytes([byte])).decode('utf-8', 'ignore')
if len(sequence) == 0:
# invalid multibyte sequence, stop processing
return
@@ -641,7 +655,7 @@ class TermCanvas(Canvas):
self.process_char(char)
- def process_char(self, char):
+ def process_char(self, char: int | bytes):
"""
Process a single character (single- and multi-byte).
@@ -654,41 +668,41 @@ class TermCanvas(Canvas):
dc = self.modes.display_ctrl
- if char == b"\x1b" and self.parsestate != 2: # escape
+ if char == b"\x1b" and self.parsestate != 2: # escape
self.within_escape = True
- elif not dc and char == b"\x0d": # carriage return
+ elif not dc and char == b"\x0d": # carriage return
self.carriage_return()
- elif not dc and char == b"\x0f": # activate G0
+ elif not dc and char == b"\x0f": # activate G0
self.charset.activate(0)
- elif not dc and char == b"\x0e": # activate G1
+ elif not dc and char == b"\x0e": # activate G1
self.charset.activate(1)
- elif not dc and char in b"\x0a\x0b\x0c": # line feed
+ elif not dc and char in b"\x0a\x0b\x0c": # line feed
self.linefeed()
if self.modes.lfnl:
self.carriage_return()
- elif not dc and char == b"\x09": # char tab
+ elif not dc and char == b"\x09": # char tab
self.tab()
- elif not dc and char == b"\x08": # backspace
+ elif not dc and char == b"\x08": # backspace
if x > 0:
self.set_term_cursor(x - 1, y)
- elif not dc and char == b"\x07" and self.parsestate != 2: # beep
+ elif not dc and char == b"\x07" and self.parsestate != 2: # beep
# we need to check if we're in parsestate 2, as an OSC can be
# terminated by the BEL character!
self.widget.beep()
- elif not dc and char in b"\x18\x1a": # CAN/SUB
+ elif not dc and char in b"\x18\x1a": # CAN/SUB
self.leave_escape()
- elif not dc and char in b"\x00\x7f": # NUL/DEL
- pass # this is ignored
+ elif not dc and char in b"\x00\x7f": # NUL/DEL
+ pass # this is ignored
elif self.within_escape:
self.parse_escape(char)
- elif not dc and char == b"\x9b": # CSI (equivalent to "ESC [")
+ elif not dc and char == b"\x9b": # CSI (equivalent to "ESC [")
self.within_escape = True
self.escbuf = b''
self.parsestate = 1
else:
self.push_cursor(char)
- def set_char(self, char, x=None, y=None):
+ def set_char(self, char, x: int | None = None, y: int | None = None) -> None:
"""
Set character of either the current cursor position
or a position given by 'x' and/or 'y' to 'char'.
@@ -701,7 +715,7 @@ class TermCanvas(Canvas):
x, y = self.constrain_coords(x, y)
self.term[y][x] = (self.attrspec, self.charset.current, char)
- def constrain_coords(self, x, y, ignore_scrolling=False):
+ def constrain_coords(self, x: int, y: int, ignore_scrolling: bool = False) -> tuple[int, int]:
"""
Checks if x/y are within the terminal and returns the corrected version.
If 'ignore_scrolling' is set, constrain within the full size of the
@@ -725,7 +739,7 @@ class TermCanvas(Canvas):
return x, y
- def linefeed(self, reverse=False):
+ def linefeed(self, reverse: bool = False) -> None:
"""
Move the cursor down (or up if reverse is True) one line but don't reset
horizontal position.
@@ -749,18 +763,24 @@ class TermCanvas(Canvas):
self.set_term_cursor(x, y)
- def carriage_return(self):
+ def carriage_return(self) -> None:
self.set_term_cursor(0, self.term_cursor[1])
- def newline(self):
+ def newline(self) -> None:
"""
Do a carriage return followed by a line feed.
"""
self.carriage_return()
self.linefeed()
- def move_cursor(self, x, y, relative_x=False, relative_y=False,
- relative=False):
+ def move_cursor(
+ self,
+ x: int,
+ y: int,
+ relative_x: bool = False,
+ relative_y: bool = False,
+ relative: bool = False,
+ ) -> None:
"""
Move cursor to position x/y while constraining terminal sizes.
If 'relative' is True, x/y is relative to the current cursor
@@ -780,7 +800,7 @@ class TermCanvas(Canvas):
self.set_term_cursor(x, y)
- def push_char(self, char, x, y):
+ def push_char(self, char, x: int, y: int) -> None:
"""
Push one character to current position and advance cursor to x/y.
"""
@@ -830,13 +850,12 @@ class TermCanvas(Canvas):
self.is_rotten_cursor = False
self.push_char(char, x, y)
- def save_cursor(self, with_attrs=False):
+ def save_cursor(self, with_attrs: bool = False) -> None:
self.saved_cursor = tuple(self.term_cursor)
if with_attrs:
- self.saved_attrs = (copy.copy(self.attrspec),
- copy.copy(self.charset))
+ self.saved_attrs = (copy.copy(self.attrspec), copy.copy(self.charset))
- def restore_cursor(self, with_attrs=False):
+ def restore_cursor(self, with_attrs: bool = False) -> None:
if self.saved_cursor is None:
return
@@ -844,10 +863,9 @@ class TermCanvas(Canvas):
self.set_term_cursor(x, y)
if with_attrs and self.saved_attrs is not None:
- self.attrspec, self.charset = (copy.copy(self.saved_attrs[0]),
- copy.copy(self.saved_attrs[1]))
+ self.attrspec, self.charset = (copy.copy(self.saved_attrs[0]), copy.copy(self.saved_attrs[1]))
- def tab(self, tabstop=8):
+ def tab(self, tabstop: int = 8) -> None:
"""
Moves cursor to the next 'tabstop' filling everything in between
with spaces.
@@ -864,7 +882,7 @@ class TermCanvas(Canvas):
self.is_rotten_cursor = False
self.set_term_cursor(x, y)
- def scroll(self, reverse=False):
+ def scroll(self, reverse: bool = False) -> None:
"""
Append a new line at the bottom and put the topmost line into the
scrollback buffer.
@@ -880,20 +898,25 @@ class TermCanvas(Canvas):
self.scrollback_buffer.append(killed)
self.term.insert(self.scrollregion_end, self.empty_line())
- def decaln(self):
+ def decaln(self) -> None:
"""
DEC screen alignment test: Fill screen with E's.
"""
for row in range(self.height):
self.term[row] = self.empty_line('E')
- def blank_line(self, row):
+ def blank_line(self, row) -> None:
"""
Blank a single line at the specified row, without modifying other lines.
"""
self.term[row] = self.empty_line()
- def insert_chars(self, position=None, chars=1, char=None):
+ def insert_chars(
+ self,
+ position: tuple[int, int] | None = None,
+ chars: int = 1,
+ char: bytes | None = None,
+ ) -> None:
"""
Insert 'chars' number of either empty characters - or those specified by
'char' - before 'position' (or the current position if not specified)
@@ -917,7 +940,7 @@ class TermCanvas(Canvas):
self.term[y].pop()
chars -= 1
- def remove_chars(self, position=None, chars=1):
+ def remove_chars(self, position: tuple[int, int] | None = None, chars: int = 1) -> None:
"""
Remove 'chars' number of empty characters from 'position' (or the current
position if not specified) pulling subsequent characters of the line to
@@ -936,7 +959,7 @@ class TermCanvas(Canvas):
self.term[y].append(self.empty_char())
chars -= 1
- def insert_lines(self, row=None, lines=1):
+ def insert_lines(self, row: int | None = None, lines: int = 1) -> None:
"""
Insert 'lines' of empty lines after the specified row, pushing all
subsequent lines to the bottom. If no 'row' is specified, the current
@@ -955,7 +978,7 @@ class TermCanvas(Canvas):
self.term.pop(self.scrollregion_end)
lines -= 1
- def remove_lines(self, row=None, lines=1):
+ def remove_lines(self, row: int | None = None, lines: int = 1) -> None:
"""
Remove 'lines' number of lines at the specified row, pulling all
subsequent lines to the top. If no 'row' is specified, the current row
@@ -974,7 +997,11 @@ class TermCanvas(Canvas):
self.term.insert(self.scrollregion_end, self.empty_line())
lines -= 1
- def erase(self, start, end):
+ def erase(
+ self,
+ start: tuple[int, int] | tuple[int, int, bool],
+ end: tuple[int, int] | tuple[int, int, bool],
+ ) -> None:
"""
Erase a region of the terminal. The 'start' tuple (x, y) defines the
starting position of the erase, while end (x, y) the last position.
@@ -1009,7 +1036,7 @@ class TermCanvas(Canvas):
y += 1
- def sgi_to_attrspec(self, attrs, fg, bg, attributes):
+ def sgi_to_attrspec(self, attrs: Iterable[int], fg: int, bg: int, attributes: set[str]) -> AttrSpec | None:
"""
Parse SGI sequence and return an AttrSpec representing the sequence
including all earlier sequences specified as 'fg', 'bg' and
@@ -1063,7 +1090,7 @@ class TermCanvas(Canvas):
if 'bold' in attributes and fg is not None:
fg += 8
- def _defaulter(color):
+ def _defaulter(color: int | None) -> str:
if color is None:
return 'default'
else:
@@ -1117,7 +1144,7 @@ class TermCanvas(Canvas):
else:
self.attrspec = attrspec
- def reverse_attrspec(self, attrspec, undo=False):
+ def reverse_attrspec(self, attrspec: AttrSpec | None, undo: bool = False) -> AttrSpec:
"""
Put standout mode to the 'attrspec' given and remove it if 'undo' is
True.
@@ -1133,7 +1160,7 @@ class TermCanvas(Canvas):
attrspec.foreground = ','.join(attrs)
return attrspec
- def reverse_video(self, undo=False):
+ def reverse_video(self, undo: bool = False) -> None:
"""
Reverse video/scanmode (DECSCNM) by swapping fg and bg colors.
"""
@@ -1143,7 +1170,13 @@ class TermCanvas(Canvas):
attrs = self.reverse_attrspec(char[0], undo=undo)
self.term[y][x] = (attrs,) + char[1:]
- def set_mode(self, mode, flag, qmark, reset):
+ def set_mode(
+ self,
+ mode: Literal[1, 3, 4, 5, 6, 7, 20, 25] | int,
+ flag: bool,
+ qmark: bool,
+ reset: bool,
+ ) -> None:
"""
Helper method for csi_set_modes: set single mode.
"""
@@ -1176,7 +1209,7 @@ class TermCanvas(Canvas):
elif mode == 20:
self.modes.lfnl = flag
- def csi_set_modes(self, modes, qmark, reset=False):
+ def csi_set_modes(self, modes: Iterable[int], qmark: bool, reset: bool = False) -> None:
"""
Set (DECSET/ECMA-48) or reset modes (DECRST/ECMA-48) if reset is True.
"""
@@ -1185,28 +1218,24 @@ class TermCanvas(Canvas):
for mode in modes:
self.set_mode(mode, flag, qmark, reset)
- def csi_set_scroll(self, top=0, bottom=0):
+ def csi_set_scroll(self, top: int = 0, bottom: int = 0) -> None:
"""
Set scrolling region, 'top' is the line number of first line in the
scrolling region. 'bottom' is the line number of bottom line. If both
are set to 0, the whole screen will be used (default).
"""
- if top == 0:
+ if not top:
top = 1
- if bottom == 0:
+ if not bottom:
bottom = self.height
if top < bottom <= self.height:
- self.scrollregion_start = self.constrain_coords(
- 0, top - 1, ignore_scrolling=True
- )[1]
- self.scrollregion_end = self.constrain_coords(
- 0, bottom - 1, ignore_scrolling=True
- )[1]
+ self.scrollregion_start = self.constrain_coords(0, top - 1, ignore_scrolling=True)[1]
+ self.scrollregion_end = self.constrain_coords(0, bottom - 1, ignore_scrolling=True)[1]
self.set_term_cursor(0, 0)
- def csi_clear_tabstop(self, mode=0):
+ def csi_clear_tabstop(self, mode: Literal[0, 3] | int = 0):
"""
Clear tabstop at current position or if 'mode' is 3, delete all
tabstops.
@@ -1216,7 +1245,7 @@ class TermCanvas(Canvas):
elif mode == 3:
self.set_tabstop(clear=True)
- def csi_get_device_attributes(self, qmark):
+ def csi_get_device_attributes(self, qmark: bool) -> None:
"""
Report device attributes (what are you?). In our case, we'll report
ourself as a VT102 terminal.
@@ -1224,7 +1253,7 @@ class TermCanvas(Canvas):
if not qmark:
self.widget.respond(f"{ESC}[?6c")
- def csi_status_report(self, mode):
+ def csi_status_report(self, mode: Literal[5, 6] | int) -> None:
"""
Report various information about the terminal status.
Information is queried by 'mode', where possible values are:
@@ -1238,7 +1267,7 @@ class TermCanvas(Canvas):
x, y = self.term_cursor
self.widget.respond(ESC + '[%d;%dR' % (y + 1, x + 1))
- def csi_erase_line(self, mode):
+ def csi_erase_line(self, mode: Literal[0, 1, 2] | int) -> None:
"""
Erase current line, modes are:
0 -> erase from cursor to end of line.
@@ -1254,7 +1283,7 @@ class TermCanvas(Canvas):
elif mode == 2:
self.blank_line(y)
- def csi_erase_display(self, mode):
+ def csi_erase_display(self, mode: Literal[0, 1, 2] | int) -> None:
"""
Erase display, modes are:
0 -> erase from cursor to end of display.
@@ -1268,7 +1297,7 @@ class TermCanvas(Canvas):
elif mode == 2:
self.clear(cursor=self.term_cursor)
- def csi_set_keyboard_leds(self, mode=0):
+ def csi_set_keyboard_leds(self, mode: Literal[0, 1, 2, 3] | int = 0) -> None:
"""
Set keyboard LEDs, modes are:
0 -> clear all LEDs
@@ -1289,26 +1318,32 @@ class TermCanvas(Canvas):
if mode in states:
self.widget.leds(states[mode])
- def clear(self, cursor=None):
+ def clear(self, cursor: tuple[int, int] | None = None) -> None:
"""
Clears the whole terminal screen and resets the cursor position
to (0, 0) or to the coordinates given by 'cursor'.
"""
- self.term = [self.empty_line() for x in range(self.height)]
+ self.term = [self.empty_line() for _ in range(self.height)]
if cursor is None:
self.set_term_cursor(0, 0)
else:
self.set_term_cursor(*cursor)
- def cols(self):
+ def cols(self) -> int:
return self.width
- def rows(self):
+ def rows(self) -> int:
return self.height
- def content(self, trim_left=0, trim_right=0, cols=None, rows=None,
- attr_map=None):
+ def content(
+ self,
+ trim_left: int = 0,
+ trim_right: int = 0,
+ cols: int | None = None,
+ rows: int | None = None,
+ attr=None,
+ ):
if self.scrolling_up == 0:
yield from self.term
else:
@@ -1329,11 +1364,11 @@ class Terminal(Widget):
def __init__(
self,
- command,
- env=None,
+ command: Sequence[str] | None,
+ env: Mapping[str, str] | Iterable[Sequence[str]] | None = None,
main_loop=None,
- escape_sequence=None,
- encoding='utf-8',
+ escape_sequence: str | None = None,
+ encoding: str = 'utf-8',
):
"""
A terminal emulator within a widget.
@@ -1379,7 +1414,7 @@ class Terminal(Widget):
self.keygrab = False
self.last_key = None
- self.response_buffer = []
+ self.response_buffer: list[str] = []
self.term_modes = TermModes()
@@ -1390,7 +1425,7 @@ class Terminal(Widget):
self.width = None
self.height = None
- self.term = None
+ self.term: TermCanvas | None = None
self.has_focus = False
self.terminated = False
@@ -1450,8 +1485,7 @@ class Terminal(Widget):
if self.pid > 0:
self.set_termsize(0, 0)
- for sig in (signal.SIGHUP, signal.SIGCONT, signal.SIGINT,
- signal.SIGTERM, signal.SIGKILL):
+ for sig in (signal.SIGHUP, signal.SIGCONT, signal.SIGINT, signal.SIGTERM, signal.SIGKILL):
try:
os.kill(self.pid, sig)
pid, status = os.waitpid(self.pid, os.WNOHANG)
@@ -1474,7 +1508,7 @@ class Terminal(Widget):
def leds(self, which) -> None:
self._emit('leds', which)
- def respond(self, string) -> None:
+ def respond(self, string: str) -> None:
"""
Respond to the underlying application with 'string'.
"""
@@ -1573,7 +1607,7 @@ class Terminal(Widget):
try:
data = os.read(self.master, 4096)
except OSError as e:
- if e.errno == 5: # EIO, child terminated
+ if e.errno == 5: # EIO, child terminated
data = EOF
elif e.errno == errno.EWOULDBLOCK: # empty buffer
return
@@ -1596,7 +1630,7 @@ class Terminal(Widget):
if key == "window resize":
width, height = size
self.touch_term(width, height)
- return
+ return None
if (self.last_key == self.escape_sequence
and key == self.escape_sequence):
@@ -1609,18 +1643,18 @@ class Terminal(Widget):
# stop grabbing the terminal
self.keygrab = False
self.last_key = key
- return
+ return None
else:
if key == 'page up':
self.term.scroll_buffer()
self.last_key = key
self._invalidate()
- return
+ return None
elif key == 'page down':
self.term.scroll_buffer(up=False)
self.last_key = key
self._invalidate()
- return
+ return None
elif (self.last_key == self.escape_sequence
and key != self.escape_sequence):
# hand down keypress directly after ungrab.
@@ -1630,7 +1664,7 @@ class Terminal(Widget):
# start grabbing the terminal
self.keygrab = True
self.last_key = key
- return
+ return None
elif self._command_map[key] is None or key == 'enter':
# printable character or escape sequence means:
# lock in terminal...
@@ -1663,3 +1697,5 @@ class Terminal(Widget):
key = key.encode(self.encoding, 'ignore')
os.write(self.master, key)
+
+ return None
diff --git a/urwid/widget.py b/urwid/widget.py
index 52b8597..d5abf40 100644
--- a/urwid/widget.py
+++ b/urwid/widget.py
@@ -1845,7 +1845,7 @@ def delegate_to_widget_mixin(attribute_name: str):
return CompositeCanvas(canv)
@property
- def selectable(self):
+ def selectable(self) -> Callable[[], bool]:
return get_delegate(self).selectable
@property
@@ -1869,7 +1869,9 @@ def delegate_to_widget_mixin(attribute_name: str):
return get_delegate(self).rows
@property
- def mouse_event(self):
+ def mouse_event(
+ self
+ ) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], str, int, int, int, bool], bool | None]:
return get_delegate(self).mouse_event
@property