summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-06-03 19:22:05 +0300
committerrobot-piglet <[email protected]>2026-06-03 19:57:13 +0300
commitc685a77776bbd8560518f2e5fed40be53292be89 (patch)
treee8193e1b58789cd2a30c32eb42f6c55dc3112fcc /contrib/python
parent39788ecfadedc4f54420727a18ab0ee272a4b5cd (diff)
Intermediate changes
commit_hash:714a2397bc9bba58d04cc4be156fac88e0774e56
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/textual/.dist-info/METADATA2
-rw-r--r--contrib/python/textual/textual/_keyboard_protocol.py22
-rw-r--r--contrib/python/textual/textual/_styles_cache.py14
-rw-r--r--contrib/python/textual/textual/_xterm_parser.py94
-rw-r--r--contrib/python/textual/textual/constants.py3
-rw-r--r--contrib/python/textual/textual/drivers/linux_driver.py21
-rw-r--r--contrib/python/textual/textual/events.py13
-rw-r--r--contrib/python/textual/textual/renderables/text_opacity.py76
-rw-r--r--contrib/python/textual/textual/widgets/_input.py5
-rw-r--r--contrib/python/textual/textual/widgets/_text_area.py215
-rw-r--r--contrib/python/textual/ya.make2
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)