diff options
| author | robot-piglet <[email protected]> | 2026-06-03 19:22:05 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-06-03 19:57:13 +0300 |
| commit | c685a77776bbd8560518f2e5fed40be53292be89 (patch) | |
| tree | e8193e1b58789cd2a30c32eb42f6c55dc3112fcc /contrib/python | |
| parent | 39788ecfadedc4f54420727a18ab0ee272a4b5cd (diff) | |
Intermediate changes
commit_hash:714a2397bc9bba58d04cc4be156fac88e0774e56
Diffstat (limited to 'contrib/python')
| -rw-r--r-- | contrib/python/textual/.dist-info/METADATA | 2 | ||||
| -rw-r--r-- | contrib/python/textual/textual/_keyboard_protocol.py | 22 | ||||
| -rw-r--r-- | contrib/python/textual/textual/_styles_cache.py | 14 | ||||
| -rw-r--r-- | contrib/python/textual/textual/_xterm_parser.py | 94 | ||||
| -rw-r--r-- | contrib/python/textual/textual/constants.py | 3 | ||||
| -rw-r--r-- | contrib/python/textual/textual/drivers/linux_driver.py | 21 | ||||
| -rw-r--r-- | contrib/python/textual/textual/events.py | 13 | ||||
| -rw-r--r-- | contrib/python/textual/textual/renderables/text_opacity.py | 76 | ||||
| -rw-r--r-- | contrib/python/textual/textual/widgets/_input.py | 5 | ||||
| -rw-r--r-- | contrib/python/textual/textual/widgets/_text_area.py | 215 | ||||
| -rw-r--r-- | contrib/python/textual/ya.make | 2 |
11 files changed, 375 insertions, 92 deletions
diff --git a/contrib/python/textual/.dist-info/METADATA b/contrib/python/textual/.dist-info/METADATA index 381a4fceb5c..f3bb97ae79a 100644 --- a/contrib/python/textual/.dist-info/METADATA +++ b/contrib/python/textual/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: textual -Version: 8.2.6 +Version: 8.2.7 Summary: Modern Text User Interface framework License: MIT License-File: LICENSE diff --git a/contrib/python/textual/textual/_keyboard_protocol.py b/contrib/python/textual/textual/_keyboard_protocol.py index 9b765411026..e35b419e129 100644 --- a/contrib/python/textual/textual/_keyboard_protocol.py +++ b/contrib/python/textual/textual/_keyboard_protocol.py @@ -1,5 +1,7 @@ +from typing import Final + # https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions -FUNCTIONAL_KEYS = { +FUNCTIONAL_KEYS: Final = { "27u": "escape", "13u": "enter", "9u": "tab", @@ -121,3 +123,21 @@ FUNCTIONAL_KEYS = { "57453u": "iso_level3_shift", "57454u": "iso_level5_shift", } + +# A sub-set of modifier keys +MODIFIER_FUNCTIONAL_KEYS: Final = { + "left_shift", + "left_control", + "left_alt", + "left_super", + "left_hyper", + "left_meta", + "right_shift", + "right_control", + "right_alt", + "right_super", + "right_hyper", + "right_meta", + "iso_level3_shift", + "iso_level5_shift", +} diff --git a/contrib/python/textual/textual/_styles_cache.py b/contrib/python/textual/textual/_styles_cache.py index 38d145cde1d..7e9d32a0b4a 100644 --- a/contrib/python/textual/textual/_styles_cache.py +++ b/contrib/python/textual/textual/_styles_cache.py @@ -112,6 +112,7 @@ class StylesCache: base_background, background = widget.background_colors styles = widget.styles + app = widget.app strips = self.render( styles, widget.region.size, @@ -139,7 +140,8 @@ class StylesCache: padding=styles.padding, crop=crop, opacity=widget.opacity, - ansi_theme=widget.app.ansi_theme, + ansi_theme=app.ansi_theme, + native_ansi=app.native_ansi_color, ) if widget.auto_links: @@ -173,6 +175,7 @@ class StylesCache: crop: Region | None = None, opacity: float = 1.0, ansi_theme: TerminalTheme = DEFAULT_TERMINAL_THEME, + native_ansi: bool = False, ) -> list[Strip]: """Render a widget content plus CSS styles. @@ -191,6 +194,7 @@ class StylesCache: filters: Additional post-processing for the segments. opacity: Widget opacity. ansi_theme: Theme for ANSI colors. + native_ansi: Use native ANSI colors? Returns: Rendered lines. @@ -227,6 +231,7 @@ class StylesCache: border_subtitle, opacity, ansi_theme, + native_ansi, ) self._cache[y] = strip else: @@ -273,6 +278,7 @@ class StylesCache: border_subtitle: tuple[Content, Color, Color, Style] | None, opacity: float, ansi_theme: TerminalTheme, + native_ansi: bool, ) -> Strip: """Render a styled line. @@ -289,6 +295,8 @@ class StylesCache: border_title: Optional tuple of (title, color, background, style). border_subtitle: Optional tuple of (subtitle, color, background, style). opacity: Opacity of line. + ansi_theme: ANSI theme. + native_ansi: Use native ANSI colors? Returns: A line of segments. @@ -450,7 +458,9 @@ class StylesCache: line = Strip.blank(content_width, inner.rich_style) if (text_opacity := styles.text_opacity) != 1.0: - line = TextOpacity.process_segments(line, text_opacity, ansi_theme) + line = TextOpacity.process_segments( + line, text_opacity, ansi_theme, native_ansi + ) if pad_left or pad_right: line = line_post(line_pad(line, pad_left, pad_right, inner.rich_style)) else: diff --git a/contrib/python/textual/textual/_xterm_parser.py b/contrib/python/textual/textual/_xterm_parser.py index 1b7f3e57d9a..eafa7414ecb 100644 --- a/contrib/python/textual/textual/_xterm_parser.py +++ b/contrib/python/textual/textual/_xterm_parser.py @@ -2,13 +2,14 @@ from __future__ import annotations import os import re +from functools import lru_cache from typing import Any, Generator, Iterable from typing_extensions import Final from textual import constants, events, messages from textual._ansi_sequences import ANSI_SEQUENCES_KEYS, IGNORE_SEQUENCE -from textual._keyboard_protocol import FUNCTIONAL_KEYS +from textual._keyboard_protocol import FUNCTIONAL_KEYS, MODIFIER_FUNCTIONAL_KEYS from textual._parser import ParseEOF, Parser, ParseTimeout, Peek1, Read1, TokenCallback from textual.keys import KEY_NAME_REPLACEMENTS, Keys, _character_to_key from textual.message import Message @@ -37,8 +38,10 @@ FOCUSOUT: Final[str] = "\x1b[O" SPECIAL_SEQUENCES = {BRACKETED_PASTE_START, BRACKETED_PASTE_END, FOCUSIN, FOCUSOUT} """Set of special sequences.""" -_re_extended_key: Final = re.compile(r"\x1b\[(?:(\d+)(?:;(\d+))?)?([u~ABCDEFHPQRS])") -_re_in_band_window_resize: Final = re.compile( +_re_extended_key: Final[re.Pattern[str]] = re.compile( + r"\x1b\[((?:\d*;?){2,3})([u~ABCDEFHPQRS])" +) +_re_in_band_window_resize: Final[re.Pattern[str]] = re.compile( r"\x1b\[48;(\d+(?:\:.*?)?);(\d+(?:\:.*?)?);(\d+(?:\:.*?)?);(\d+(?:\:.*?)?)t" ) @@ -48,6 +51,13 @@ IS_ITERM = ( or os.environ.get("TERM_PROGRAM", "") == "iTerm.app" ) +SPECIAL_KEY_TO_CHARACTER: Final = { + "backspace": "\x7f", + "enter": "\r", + "tab": "\t", +} +"""Explcit characters for keys, used in Kitty protocol parsing""" + class XTermParser(Parser[Message]): _re_sgr_mouse = re.compile(r"\x1b\[<(\d+);(-?\d+);(-?\d+)([Mm])") @@ -324,6 +334,53 @@ class XTermParser(Parser[Message]): self._debug_log_file.close() self._debug_log_file = None + @lru_cache(maxsize=1024) + def _parse_extended_key(self, sequence: str) -> events.Key | None: + """Parse a Kitty sequence. + + Args: + sequence: Input sequence + + Returns: + Key event, or `None` of none could be parsed. + """ + + if (match := _re_extended_key.fullmatch(sequence)) is None: + return None + + codes, end = match.groups(default="") + codepoint_str, modifiers_str, text_str, *_ = codes.split(";") + ["", "", ""] + + codepoint = int(codepoint_str or "1") + modifiers = int(modifiers_str or "0") + text = chr(int(text_str)) if text_str else None + + if not (key := FUNCTIONAL_KEYS.get(f"{codepoint}{end}", "")): + key = _character_to_key(text if text else chr(codepoint)) + + key_tokens: list[str] = [] + # The modifier is redundant on a modifier key + if modifiers and key not in MODIFIER_FUNCTIONAL_KEYS and text_str is not None: + modifier_bits = int(modifiers) - 1 + # Not convinced of the utility in reporting caps_lock and num_lock + MODIFIERS = ("alt", "ctrl", "super", "hyper", "meta") + # Ignore caps_lock and num_lock modifiers + if modifier_bits & 1 and (text is None or text.isspace()): + key_tokens.append("shift") + for bit, modifier in enumerate(MODIFIERS, 1): + if modifier == "alt" and text is not None: + continue + if modifier_bits & (1 << bit): + key_tokens.append(modifier) + + key_tokens.sort() + if key is not None: + key_tokens.append(key) + return events.Key( + "+".join(key_tokens), + text or (None if modifiers else SPECIAL_KEY_TO_CHARACTER.get(key, None)), + ) + def _sequence_to_key_events( self, sequence: str, alt: bool = False ) -> Iterable[events.Key]: @@ -333,32 +390,14 @@ class XTermParser(Parser[Message]): sequence: Sequence of code points. Returns: - Keys + Iterable of key events. """ - if (match := _re_extended_key.fullmatch(sequence)) is not None: - number, modifiers, end = match.groups() - number = number or 1 - if not (key := FUNCTIONAL_KEYS.get(f"{number}{end}", "")): - try: - key = _character_to_key(chr(int(number))) - except Exception: - key = chr(int(number)) - key_tokens: list[str] = [] - if modifiers: - modifier_bits = int(modifiers) - 1 - # Not convinced of the utility in reporting caps_lock and num_lock - MODIFIERS = ("shift", "alt", "ctrl", "super", "hyper", "meta") - # Ignore caps_lock and num_lock modifiers - for bit, modifier in enumerate(MODIFIERS): - if modifier_bits & (1 << bit): - key_tokens.append(modifier) - - key_tokens.sort() - key_tokens.append(key.lower()) - yield events.Key( - "+".join(key_tokens), sequence if len(sequence) == 1 else None - ) + if ( + not constants.DISABLE_KITTY_KEY + and (key := self._parse_extended_key(sequence)) is not None + ): + yield key.copy() return keys = ANSI_SEQUENCES_KEYS.get(sequence) @@ -383,6 +422,7 @@ class XTermParser(Parser[Message]): sequence = keys # If the sequence is a single character, attempt to process it as a # key. + if len(sequence) == 1: try: if not sequence.isalnum(): diff --git a/contrib/python/textual/textual/constants.py b/contrib/python/textual/textual/constants.py index feedbea6e96..a2702db11a6 100644 --- a/contrib/python/textual/textual/constants.py +++ b/contrib/python/textual/textual/constants.py @@ -113,6 +113,9 @@ DEBUG: Final[bool] = _get_environ_bool("TEXTUAL_DEBUG") DRIVER: Final[str | None] = get_environ("TEXTUAL_DRIVER", None) """Import for replacement driver.""" +DISABLE_KITTY_KEY: Final[bool] = _get_environ_bool("TEXTUAL_DISABLE_KITTY_KEY") +"""Disable kitty key protocol.""" + FILTERS: Final[str] = get_environ("TEXTUAL_FILTERS", "") """A list of filters to apply to renderables.""" diff --git a/contrib/python/textual/textual/drivers/linux_driver.py b/contrib/python/textual/textual/drivers/linux_driver.py index 98bf632ff70..74ed4631879 100644 --- a/contrib/python/textual/textual/drivers/linux_driver.py +++ b/contrib/python/textual/textual/drivers/linux_driver.py @@ -9,11 +9,11 @@ import termios import tty from codecs import getincrementaldecoder from threading import Event, Thread -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final import rich.repr -from textual import events +from textual import constants, events from textual._loop import loop_last from textual._parser import ParseError from textual._xterm_parser import XTermParser @@ -26,6 +26,13 @@ from textual.messages import InBandWindowResize if TYPE_CHECKING: from textual.app import App +# https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +KITTY_DISAMBIGUATE_ESCAPE_CODES: Final = 0b00000001 +KITTY_REPORT_EVENT_TYPES: Final = 0b00000010 +KITTY_REPORT_ALTERNATE_KEYS: Final = 0b00000100 +KITTY_REPORT_ALL_KEYS: Final = 0b00001000 +KITTY_REPORT_ASSOCIATED_TEXT: Final = 0b00010000 + @rich.repr.auto(angular=True) class LinuxDriver(Driver): @@ -274,7 +281,15 @@ class LinuxDriver(Driver): self.write("\x1b[?25l") # Hide cursor self.write("\x1b[?1004h") # Enable FocusIn/FocusOut. - self.write("\x1b[>1u") # https://sw.kovidgoyal.net/kitty/keyboard-protocol/ + + if not constants.DISABLE_KITTY_KEY: + # https://sw.kovidgoyal.net/kitty/keyboard-protocol/ + KITTY_PROTOCOL_FLAG = ( + KITTY_DISAMBIGUATE_ESCAPE_CODES + | KITTY_REPORT_ALL_KEYS + | KITTY_REPORT_ASSOCIATED_TEXT + ) + self.write(f"\x1b[>{KITTY_PROTOCOL_FLAG}u") self.flush() self._key_thread = Thread(target=self._run_input_thread, name="textual-input") diff --git a/contrib/python/textual/textual/events.py b/contrib/python/textual/textual/events.py index 92e05a91a1a..d3847f4ad49 100644 --- a/contrib/python/textual/textual/events.py +++ b/contrib/python/textual/textual/events.py @@ -269,7 +269,7 @@ class Key(InputEvent): character: A printable character or `None` if it is not printable. """ - __slots__ = ["key", "character", "aliases"] + __slots__ = ["key", "character"] def __init__(self, key: str, character: str | None) -> None: super().__init__() @@ -279,8 +279,6 @@ class Key(InputEvent): (key if len(key) == 1 else None) if character is None else character ) """A printable character or ``None`` if it is not printable.""" - self.aliases: list[str] = _get_key_aliases(key) - """The aliases for the key, including the key itself.""" def __rich_repr__(self) -> rich.repr.Result: yield "key", self.key @@ -289,6 +287,10 @@ class Key(InputEvent): yield "is_printable", self.is_printable yield "aliases", self.aliases, [self.key] + def copy(self) -> Key: + """Get a copy of this key event.""" + return Key(self.key, self.character) + @property def name(self) -> str: """Name of a key suitable for use as a Python identifier.""" @@ -308,6 +310,11 @@ class Key(InputEvent): """ return False if self.character is None else self.character.isprintable() + @property + def aliases(self) -> list[str]: + """The aliases for the key, including the key itself.""" + return _get_key_aliases(self.key) + def _key_to_identifier(key: str) -> str: """Convert the key string to a name suitable for use as a Python identifier.""" diff --git a/contrib/python/textual/textual/renderables/text_opacity.py b/contrib/python/textual/textual/renderables/text_opacity.py index 7ba41e36d13..b1ed31a49fa 100644 --- a/contrib/python/textual/textual/renderables/text_opacity.py +++ b/contrib/python/textual/textual/renderables/text_opacity.py @@ -56,6 +56,7 @@ class TextOpacity: segments: Iterable[Segment], opacity: float, ansi_theme: TerminalTheme, + native_ansi: bool = False, ) -> Iterable[Segment]: """Apply opacity to segments. @@ -64,6 +65,7 @@ class TextOpacity: opacity: Opacity to apply. ansi_theme: Terminal theme. background: Color of background. + native_ansi: Allow ANSI colors. Returns: Segments with applied opacity. @@ -82,21 +84,67 @@ class TextOpacity: elif opacity == 1: yield from segments else: - filter = ANSIToTruecolor(ansi_theme) - for segment in filter.apply(list(segments), TRANSPARENT): - # use Tuple rather than tuple so Python 3.7 doesn't complain - text, style, control = cast(Tuple[str, Style, object], segment) - if not style: - yield segment - continue + if native_ansi: + # Special case for native ANSI + # Without RGB we can't accurately calculate the foreground color + DIM = Style(dim=True) + filter = ANSIToTruecolor(ansi_theme) + segments_ = list(segments) + for original_segment, segment in zip( + segments_, filter.apply(segments_, TRANSPARENT) + ): + if original_segment.style is not None: + text, style, control = original_segment + if ( + style is not None + and style.bgcolor is not None + and style.bgcolor.is_default + ): + # If the opacity is less than or equal to 50%, then set the dim attribute + if style.color is not None and opacity <= 0.5: + yield Segment(text, style + DIM, control) + else: + yield original_segment + continue + # use Tuple rather than tuple so Python 3.7 doesn't complain + text, style, control = segment + if not style: + yield segment + continue + + color = style.color + bgcolor = style.bgcolor + if ( + color is not None + and color.triplet + and bgcolor + and bgcolor.triplet + ): + color_style = _get_blended_style_cached(bgcolor, color, opacity) + yield _Segment(text, style + color_style) + else: + yield segment + else: + filter = ANSIToTruecolor(ansi_theme) + for segment in filter.apply(list(segments), TRANSPARENT): + # use Tuple rather than tuple so Python 3.7 doesn't complain + text, style, control = segment + if not style: + yield segment + continue - color = style.color - bgcolor = style.bgcolor - if color and color.triplet and bgcolor and bgcolor.triplet: - color_style = _get_blended_style_cached(bgcolor, color, opacity) - yield _Segment(text, style + color_style) - else: - yield segment + color = style.color + bgcolor = style.bgcolor + if ( + color is not None + and color.triplet + and bgcolor + and bgcolor.triplet + ): + color_style = _get_blended_style_cached(bgcolor, color, opacity) + yield _Segment(text, style + color_style) + else: + yield segment def __rich_console__( self, console: Console, options: ConsoleOptions diff --git a/contrib/python/textual/textual/widgets/_input.py b/contrib/python/textual/textual/widgets/_input.py index 77b77906cbc..3aa2a945bdb 100644 --- a/contrib/python/textual/textual/widgets/_input.py +++ b/contrib/python/textual/textual/widgets/_input.py @@ -124,7 +124,10 @@ class Input(ScrollView): ), Binding("ctrl+u", "delete_left_all", "Delete all to the left", show=False), Binding( - "ctrl+f", "delete_right_word", "Delete right to start of word", show=False + "ctrl+backspace", + "delete_right_word", + "Delete right to start of word", + show=False, ), Binding("ctrl+k", "delete_right_all", "Delete all to the right", show=False), Binding("ctrl+x", "cut", "Cut selected text", show=False), diff --git a/contrib/python/textual/textual/widgets/_text_area.py b/contrib/python/textual/textual/widgets/_text_area.py index 87a0bb10ee4..d970a31c5a7 100644 --- a/contrib/python/textual/textual/widgets/_text_area.py +++ b/contrib/python/textual/textual/widgets/_text_area.py @@ -102,7 +102,7 @@ class TextAreaLanguage: name: str """The name of the language""" - language: "Language" | None + language: "Language | None" """The tree-sitter language object if that has been overridden, or None if it is a built-in language.""" highlight_query: str @@ -223,16 +223,66 @@ TextArea { BINDINGS = [ # Cursor movement - Binding("up", "cursor_up", "Cursor up", show=False), - Binding("down", "cursor_down", "Cursor down", show=False), - Binding("left", "cursor_left", "Cursor left", show=False), - Binding("right", "cursor_right", "Cursor right", show=False), - Binding("ctrl+left", "cursor_word_left", "Cursor word left", show=False), - Binding("ctrl+right", "cursor_word_right", "Cursor word right", show=False), - Binding("home,ctrl+a", "cursor_line_start", "Cursor line start", show=False), - Binding("end,ctrl+e", "cursor_line_end", "Cursor line end", show=False), - Binding("pageup", "cursor_page_up", "Cursor page up", show=False), - Binding("pagedown", "cursor_page_down", "Cursor page down", show=False), + Binding( + "up", + "cursor_up", + "Cursor up", + show=False, + ), + Binding( + "down", + "cursor_down", + "Cursor down", + show=False, + ), + Binding( + "left", + "cursor_left", + "Cursor left", + show=False, + ), + Binding( + "right", + "cursor_right", + "Cursor right", + show=False, + ), + Binding( + "ctrl+left", + "cursor_word_left", + "Cursor word left", + show=False, + ), + Binding( + "ctrl+right", + "cursor_word_right", + "Cursor word right", + show=False, + ), + Binding( + "home,ctrl+a", + "cursor_line_start", + "Cursor line start", + show=False, + ), + Binding( + "end,ctrl+e", + "cursor_line_end", + "Cursor line end", + show=False, + ), + Binding( + "pageup", + "cursor_page_up", + "Cursor page up", + show=False, + ), + Binding( + "pagedown", + "cursor_page_down", + "Cursor page down", + show=False, + ), # Making selections (generally holding the shift key and moving cursor) Binding( "ctrl+shift+left", @@ -253,30 +303,97 @@ TextArea { show=False, ), Binding( - "shift+end", "cursor_line_end(True)", "Cursor line end select", show=False + "shift+end", + "cursor_line_end(True)", + "Cursor line end select", + show=False, + ), + Binding( + "shift+up", + "cursor_up(True)", + "Cursor up select", + show=False, + ), + Binding( + "shift+down", + "cursor_down(True)", + "Cursor down select", + show=False, + ), + Binding( + "shift+left", + "cursor_left(True)", + "Cursor left select", + show=False, + ), + Binding( + "shift+right", + "cursor_right(True)", + "Cursor right select", + show=False, ), - Binding("shift+up", "cursor_up(True)", "Cursor up select", show=False), - Binding("shift+down", "cursor_down(True)", "Cursor down select", show=False), - Binding("shift+left", "cursor_left(True)", "Cursor left select", show=False), - Binding("shift+right", "cursor_right(True)", "Cursor right select", show=False), # Shortcut ways of making selections # Binding("f5", "select_word", "select word", show=False), - Binding("f6", "select_line", "Select line", show=False), - Binding("f7", "select_all", "Select all", show=False), + Binding( + "f6", + "select_line", + "Select line", + show=False, + ), + Binding( + "f7", + "select_all", + "Select all", + show=False, + ), # Deletion - Binding("backspace", "delete_left", "Delete character left", show=False), Binding( - "ctrl+w", "delete_word_left", "Delete left to start of word", show=False + "backspace", + "delete_left", + "Delete character left", + show=False, + ), + Binding( + "ctrl+w,ctrl+backspace", + "delete_word_left", + "Delete left to start of word", + show=False, + ), + Binding( + "delete,ctrl+d", + "delete_right", + "Delete character right", + show=False, + ), + Binding( + "alt+delete", + "delete_word_right", + "Delete right to start of word", + show=False, + ), + Binding( + "ctrl+x,super+x", + "cut", + "Cut", + show=False, + ), + Binding( + "ctrl+c,super+c", + "copy", + "Copy", + show=False, ), - Binding("delete,ctrl+d", "delete_right", "Delete character right", show=False), Binding( - "ctrl+f", "delete_word_right", "Delete right to start of word", show=False + "ctrl+v", + "paste", + "Paste", + show=False, ), - Binding("ctrl+x", "cut", "Cut", show=False), - Binding("ctrl+c,super+c", "copy", "Copy", show=False), - Binding("ctrl+v", "paste", "Paste", show=False), Binding( - "ctrl+u", "delete_to_start_of_line", "Delete to line start", show=False + "ctrl+u", + "delete_to_start_of_line", + "Delete to line start", + show=False, ), Binding( "ctrl+k", @@ -290,8 +407,18 @@ TextArea { "Delete line", show=False, ), - Binding("ctrl+z", "undo", "Undo", show=False), - Binding("ctrl+y", "redo", "Redo", show=False), + Binding( + "ctrl+z,super+z", + "undo", + "Undo", + show=False, + ), + Binding( + "ctrl+y,super+y", + "redo", + "Redo", + show=False, + ), ] """ | Key(s) | Description | @@ -315,19 +442,19 @@ TextArea { | shift+left | Select while moving the cursor left. | | shift+right | Select while moving the cursor right. | | backspace | Delete character to the left of cursor. | - | ctrl+w | Delete from cursor to start of the word. | + | ctrl+w,ctrl+backspace | Delete from cursor to start of the word. | | delete,ctrl+d | Delete character to the right of cursor. | - | ctrl+f | Delete from cursor to end of the word. | + | alt+delete | Delete from cursor to end of the word. | | ctrl+shift+k | Delete the current line. | | ctrl+u | Delete from cursor to the start of the line. | | ctrl+k | Delete from cursor to the end of the line. | | f6 | Select the current line. | | f7 | Select all text in the document. | - | ctrl+z | Undo. | - | ctrl+y | Redo. | - | ctrl+x | Cut selection or line if no selection. | - | ctrl+c | Copy selection to clipboard. | - | ctrl+v | Paste from clipboard. | + | ctrl+z,super+z | Undo. | + | ctrl+y,super+y | Redo. | + | ctrl+x,super+x | Cut selection or line if no selection. | + | ctrl+c,super+c | Copy selection to clipboard. | + | ctrl+v,super+v | Paste from clipboard. | """ language: Reactive[str | None] = reactive(None, always_update=True, init=False) @@ -2541,9 +2668,19 @@ TextArea { def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" - from_location = self.selection.end - to_location = self.get_cursor_line_start_location() - self._delete_via_keyboard(from_location, to_location) + if self.read_only: + return + + if self.cursor_at_start_of_line: + selection = self.selection + start, end = selection + if selection.is_empty: + end = self.get_cursor_left_location() + self._delete_via_keyboard(start, end) + else: + from_location = self.selection.end + to_location = self.get_cursor_line_start_location() + self._delete_via_keyboard(from_location, to_location) def action_delete_to_end_of_line(self) -> None: """Deletes from the cursor location to the end of the line.""" @@ -2605,7 +2742,7 @@ TextArea { # Check the current line for a word boundary line = self.document[cursor_row][cursor_column:] - matches = list(re.finditer(self._word_pattern, line)) + matches = list(re.finditer(r"\s*\w+", line)) current_row_length = len(self.document[cursor_row]) if matches: diff --git a/contrib/python/textual/ya.make b/contrib/python/textual/ya.make index a92ca7dbbc3..84a9205d17e 100644 --- a/contrib/python/textual/ya.make +++ b/contrib/python/textual/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(8.2.6) +VERSION(8.2.7) LICENSE(MIT) |
