From 4890c79944949a330f76833b8542de80a7d7b4cb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 11:28:56 +0000 Subject: [PATCH 01/15] Clipboard keys --- docs/blog/posts/inline-mode.md | 2 +- docs/guide/app.md | 2 +- docs/guide/input.md | 6 ++---- docs/tutorial.md | 4 ++-- src/textual/app.py | 13 ++++++++++++- src/textual/widgets/_input.py | 22 ++++++++++++++++++++-- src/textual/widgets/_text_area.py | 31 ++++++++++++++++++++++++++++++- 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/docs/blog/posts/inline-mode.md b/docs/blog/posts/inline-mode.md index 0127932144..759f994e11 100644 --- a/docs/blog/posts/inline-mode.md +++ b/docs/blog/posts/inline-mode.md @@ -16,7 +16,7 @@ You can see this in action if you run the [calculator example](https://github.co The application appears directly under the prompt, rather than occupying the full height of the screen—which is more typical of TUI applications. You can interact with this calculator using keys *or* the mouse. -When you press ++ctrl+c++ the calculator disappears and returns you to the prompt. +When you press ++ctrl+q++ the calculator disappears and returns you to the prompt. Here's another app that creates an inline code editor: diff --git a/docs/guide/app.md b/docs/guide/app.md index d538b6e939..ec3d6ccd0f 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -32,7 +32,7 @@ If we run this app with `python simple02.py` you will see a blank terminal, some When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*). -If you hit ++ctrl+c++ Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored. +If you hit ++ctrl+q++ Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored. !!! tip diff --git a/docs/guide/input.md b/docs/guide/input.md index 30713bfb1b..a227798878 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -172,13 +172,11 @@ The tuple of three strings may be enough for simple bindings, but you can also r Individual bindings may be marked as a *priority*, which means they will be checked prior to the bindings of the focused widget. This feature is often used to create hot-keys on the app or screen. Such bindings can not be disabled by binding the same key on a widget. -You can create priority key bindings by setting `priority=True` on the Binding object. Textual uses this feature to add a default binding for ++ctrl+c++ so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority: +You can create priority key bindings by setting `priority=True` on the Binding object. Textual uses this feature to add a default binding for ++ctrl+q++ so there is always a way to exit the app. Here's the `BINDINGS` from the App base class. Note the quit binding is set as a priority: ```python BINDINGS = [ - Binding("ctrl+c", "quit", "Quit", show=False, priority=True), - Binding("tab", "focus_next", "Focus Next", show=False), - Binding("shift+tab", "focus_previous", "Focus Previous", show=False), + Binding("ctrl+q", "quit", "Quit", show=False, priority=True) ] ``` diff --git a/docs/tutorial.md b/docs/tutorial.md index 5ca7f6d0e9..17279279ae 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -122,7 +122,7 @@ Hit the ++d++ key to toggle between light and dark themes. ```{.textual path="docs/examples/tutorial/stopwatch01.py" press="d" title="stopwatch01.py"} ``` -Hit ++ctrl+c++ to exit the app and return to the command prompt. +Hit ++ctrl+q++ to exit the app and return to the command prompt. ### A closer look at the App class @@ -157,7 +157,7 @@ Here's what the above app defines: --8<-- "docs/examples/tutorial/stopwatch01.py" ``` -The final three lines create an instance of the app and calls the [run()][textual.app.App.run] method which puts your terminal in to *application mode* and runs the app until you exit with ++ctrl+c++. This happens within a `__name__ == "__main__"` block so we could run the app with `python stopwatch01.py` or import it as part of a larger project. +The final three lines create an instance of the app and calls the [run()][textual.app.App.run] method which puts your terminal in to *application mode* and runs the app until you exit with ++ctrl+q++. This happens within a `__name__ == "__main__"` block so we could run the app with `python stopwatch01.py` or import it as part of a larger project. ## Designing a UI with widgets diff --git a/src/textual/app.py b/src/textual/app.py index d54f5f49f7..414d31be07 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -438,7 +438,14 @@ class MyApp(App[None]): """The default value of [Screen.ALLOW_IN_MAXIMIZED_VIEW][textual.screen.Screen.ALLOW_IN_MAXIMIZED_VIEW].""" BINDINGS: ClassVar[list[BindingType]] = [ - Binding("ctrl+c", "quit", "Quit", show=False, priority=True) + Binding( + "ctrl+q", + "quit", + "Quit", + tooltip="Quit the app and return to the command prompt.", + show=True, + priority=True, + ) ] """The default key bindings.""" @@ -767,6 +774,9 @@ def __init__( self._css_update_count: int = 0 """Incremented when CSS is invalidated.""" + self._clipboard: str = "" + """Contents of local clipboard.""" + if self.ENABLE_COMMAND_PALETTE: for _key, binding in self._bindings: if binding.action in {"command_palette", "app.command_palette"}: @@ -1497,6 +1507,7 @@ def copy_to_clipboard(self, text: str) -> None: Args: text: Text you wish to copy to the clipboard. """ + self._clipboard = text if self._driver is None: return diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 7a14fc393f..22d5096d76 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -115,7 +115,9 @@ class Input(ScrollView): "ctrl+f", "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+c", "copy_selection", "Copy selected text", show=False), + Binding("ctrl+x", "cut", "Cut selected text", show=False), + Binding("ctrl+c", "copy", "Copy selected text", show=False), + Binding("ctrl+v", "paste", "Paste text from the clipboard", show=False), ] """ | Key(s) | Description | @@ -995,6 +997,22 @@ async def action_submit(self) -> None: ) self.post_message(self.Submitted(self, self.value, validation_result)) - def action_copy_selection(self) -> None: + def action_cut(self) -> None: + """Cut the current selection (copy to clipboard and remove from input).""" + start, end = sorted(self.selection) + text = self.value[start:end] + self.app.copy_to_clipboard(text) + new_value = self.value[:start] + self.value[end:] + self.value = new_value + + def action_copy(self) -> None: """Copy the current selection to the clipboard.""" self.app.copy_to_clipboard(self.selected_text) + + def action_paste(self) -> None: + """Paste from the local clipboard.""" + clipboard = self.app._clipboard + start, end = sorted(self.selection) + new_value = self.value[:start] + clipboard + self.value[end:] + self.value = new_value + self.cursor_position = start + len(clipboard) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 2b68efbc60..cc227daaad 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -226,7 +226,10 @@ class TextArea(ScrollView): Binding( "ctrl+f", "delete_word_right", "Delete right to start of word", show=False ), - Binding("ctrl+x", "delete_line", "Delete line", show=False), + Binding("ctrl+shift+x", "delete_line", "Delete line", show=False), + Binding("ctrl+x", "cut", "Cut", show=False), + Binding("ctrl+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 ), @@ -2199,6 +2202,32 @@ def action_delete_line(self) -> None: if deletion is not None: self.move_cursor_relative(columns=end_column, record_width=False) + def action_cut(self) -> None: + """Cut text (remove and copy to clipboard).""" + if self.read_only: + return + start, end = self.selection + if start == end: + return + copy_text = self.get_text_range(start, end) + self.notify(f"cut {copy_text!r}") + self.app.copy_to_clipboard(copy_text) + self._delete_via_keyboard(start, end) + + def action_copy(self) -> None: + """Copy selection to clipboard.""" + start, end = self.selection + if start == end: + return + copy_text = self.get_text_range(start, end) + self.app.copy_to_clipboard(copy_text) + + def action_paste(self) -> None: + """Paste from local clipboard.""" + clipboard = self.app._clipboard + start, end = self.selection + self._replace_via_keyboard(clipboard, start, end) + 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 From 65c213ca8e1457a2beaca54246ca7a18833710fe Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 12:27:16 +0000 Subject: [PATCH 02/15] help to quit --- src/textual/app.py | 19 +++++++++++++++++-- src/textual/widgets/_input.py | 3 +-- src/textual/widgets/_text_area.py | 1 - 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 414d31be07..df872f77b8 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -443,9 +443,10 @@ class MyApp(App[None]): "quit", "Quit", tooltip="Quit the app and return to the command prompt.", - show=True, + show=False, priority=True, - ) + ), + Binding("ctrl+c", "help_quit"), ] """The default key bindings.""" @@ -3616,6 +3617,20 @@ async def _check_bindings(self, key: str, priority: bool = False) -> bool: return True return False + def action_help_quit(self) -> None: + """Bound to ctrl+C to alert the user that it no longer quits.""" + # Doing this because users will reflexively hit ctrl+C to exit + # Ctrl+C is now bound to copy if an input / textarea is focused. + # This makes is possible, even likely, that a user may do it accidentally -- which would be maddening. + # Rather than do nothing, we can make an educated guess the user was trying + # to quit, and inform them how you really quit. + for key, active_binding in self.active_bindings.items(): + if active_binding.binding.action in ("quit", "app.quit"): + self.notify( + f"Press [b]{key}[/b] to quit the app", title="Do you want to quit?" + ) + return + def set_keymap(self, keymap: Keymap) -> None: """Set the keymap, a mapping of binding IDs to key strings. diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 22d5096d76..530a774435 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -1002,8 +1002,7 @@ def action_cut(self) -> None: start, end = sorted(self.selection) text = self.value[start:end] self.app.copy_to_clipboard(text) - new_value = self.value[:start] + self.value[end:] - self.value = new_value + self.delete_selection() def action_copy(self) -> None: """Copy the current selection to the clipboard.""" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index cc227daaad..a304eb609c 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2210,7 +2210,6 @@ def action_cut(self) -> None: if start == end: return copy_text = self.get_text_range(start, end) - self.notify(f"cut {copy_text!r}") self.app.copy_to_clipboard(copy_text) self._delete_via_keyboard(start, end) From 4ea1044aafb281936dd0d99f1fed06bc31edbb37 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 13:01:36 +0000 Subject: [PATCH 03/15] test for cut n paste --- src/textual/app.py | 9 ++++++ tests/input/test_cut_copy_paste.py | 52 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/input/test_cut_copy_paste.py diff --git a/src/textual/app.py b/src/textual/app.py index df872f77b8..8bd1ac5d55 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -877,6 +877,15 @@ def children(self) -> Sequence["Widget"]: except StopIteration: return () + @property + def clipboard(self) -> str: + """The value of the local clipboard. + + Note, that this only contains text copied in the app, and not + text copied from elsewhere in the OS. + """ + return self._clipboard + @contextmanager def batch_update(self) -> Generator[None, None, None]: """A context manager to suspend all repaints until the end of the batch.""" diff --git a/tests/input/test_cut_copy_paste.py b/tests/input/test_cut_copy_paste.py new file mode 100644 index 0000000000..b6dd434e14 --- /dev/null +++ b/tests/input/test_cut_copy_paste.py @@ -0,0 +1,52 @@ +from textual.app import App, ComposeResult +from textual.widgets import Input + + +class InputApp(App): + def compose(self) -> ComposeResult: + yield Input() + + +async def test_cut(): + """Check that cut removes text and places it in the clipboard.""" + app = InputApp() + async with app.run_test() as pilot: + input = app.query_one(Input) + await pilot.click(input) + await pilot.press(*"Hello, World") + await pilot.press("left", "shift+left", "shift+left") + await pilot.press("ctrl+x") + assert input.value == "Hello, Wod" + assert app.clipboard == "rl" + + +async def test_copy(): + """Check that copy places text in the clipboard.""" + app = InputApp() + async with app.run_test() as pilot: + input = app.query_one(Input) + await pilot.click(input) + await pilot.press(*"Hello, World") + await pilot.press("left", "shift+left", "shift+left") + await pilot.press("ctrl+c") + assert input.value == "Hello, World" + assert app.clipboard == "rl" + + +async def test_paste(): + """Check that paste copies text from the local clipboard.""" + app = InputApp() + async with app.run_test() as pilot: + input = app.query_one(Input) + await pilot.click(input) + await pilot.press(*"Hello, World") + await pilot.press( + "shift+left", "shift+left", "shift+left", "shift+left", "shift+left" + ) + await pilot.press("ctrl+c") + assert input.value == "Hello, World" + assert app.clipboard == "World" + await pilot.press("ctrl+v") + assert input.value == "Hello, World" + await pilot.press("ctrl+v") + assert input.value == "Hello, WorldWorld" From 49d97dcdd7769c21ef62daf1fa49552bca0e0c77 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 13:06:07 +0000 Subject: [PATCH 04/15] changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e9973345..2af8953425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unlrelease + +### Added + +- Added `App.clipboard` +- Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea +- + +### Changed + +- Change default quit key to `ctrl+q` +- Changed delete line binding on TextArea to use `ctrl+shift+x` + ## [0.89.1] - 2024-11-05 ### Fixed From 130e8c74508cdababaf311769b476ea2dd87a462 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 13:16:50 +0000 Subject: [PATCH 05/15] test for textarea cut/copy/paste --- .../text_area/test_textarea_cut_copy_paste.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/text_area/test_textarea_cut_copy_paste.py diff --git a/tests/text_area/test_textarea_cut_copy_paste.py b/tests/text_area/test_textarea_cut_copy_paste.py new file mode 100644 index 0000000000..a762fdacf4 --- /dev/null +++ b/tests/text_area/test_textarea_cut_copy_paste.py @@ -0,0 +1,52 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea() + + +async def test_cut(): + """Check that cut removes text and places it in the clipboard.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(text_area) + await pilot.press(*"Hello, World") + await pilot.press("left", "shift+left", "shift+left") + await pilot.press("ctrl+x") + assert text_area.document.text == "Hello, Wod" + assert app.clipboard == "rl" + + +async def test_copy(): + """Check that copy places text in the clipboard.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(text_area) + await pilot.press(*"Hello, World") + await pilot.press("left", "shift+left", "shift+left") + await pilot.press("ctrl+c") + assert text_area.document.text == "Hello, World" + assert app.clipboard == "rl" + + +async def test_paste(): + """Check that paste copies text from the local clipboard.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(text_area) + await pilot.press(*"Hello, World") + await pilot.press( + "shift+left", "shift+left", "shift+left", "shift+left", "shift+left" + ) + await pilot.press("ctrl+c") + assert text_area.document.text == "Hello, World" + assert app.clipboard == "World" + await pilot.press("ctrl+v") + assert text_area.document.text == "Hello, World" + await pilot.press("ctrl+v") + assert text_area.document.text == "Hello, WorldWorld" From b7c0215b6ce292d78363bb2962427eef8d3f299f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 13:18:23 +0000 Subject: [PATCH 06/15] changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af8953425..392302697b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unlrelease +## Unlreleased ### Added - Added `App.clipboard` - Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea -- ### Changed From e58141ce705f546ea8e86d8647f0da36c0ebc814 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 13:26:16 +0000 Subject: [PATCH 07/15] read only exception --- src/textual/widgets/_text_area.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index a304eb609c..527ff31c67 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2223,6 +2223,8 @@ def action_copy(self) -> None: def action_paste(self) -> None: """Paste from local clipboard.""" + if self.read_only: + return clipboard = self.app._clipboard start, end = self.selection self._replace_via_keyboard(clipboard, start, end) From 5f5a38313a4927bb945191c0ec8a5bd8680dd996 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:01:54 +0000 Subject: [PATCH 08/15] snapshots --- CHANGELOG.md | 1 + src/textual/app.py | 2 +- src/textual/binding.py | 3 + src/textual/widgets/_key_panel.py | 5 +- src/textual/widgets/_text_area.py | 2 +- .../test_footer_classic_styling.svg | 116 +++++++------- .../test_snapshots/test_footer_compact.svg | 116 +++++++------- .../test_footer_compact_with_hover.svg | 116 +++++++------- ..._footer_standard_after_reactive_change.svg | 116 +++++++------- .../test_footer_standard_with_hover.svg | 116 +++++++------- .../test_snapshots/test_help_panel.svg | 145 +++++++++--------- ..._help_panel_key_display_not_duplicated.svg | 124 +++++++-------- .../test_snapshots/test_key_display.svg | 116 +++++++------- ...bindings_display_footer_and_help_panel.svg | 124 +++++++-------- .../test_keymap_bindings_key_display.svg | 124 +++++++-------- .../snapshot_apps/footer_toggle_compact.py | 1 - tests/test_binding_inheritance.py | 11 +- tests/text_area/test_edit_via_bindings.py | 4 +- 18 files changed, 625 insertions(+), 617 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 392302697b..cf85c7312a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `App.clipboard` - Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea +- Added `system` boolean to Binding, which hides the binding from the help panel ### Changed diff --git a/src/textual/app.py b/src/textual/app.py index 8bd1ac5d55..3a8c4ea207 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -446,7 +446,7 @@ class MyApp(App[None]): show=False, priority=True, ), - Binding("ctrl+c", "help_quit"), + Binding("ctrl+c", "help_quit", show=False, system=True), ] """The default key bindings.""" diff --git a/src/textual/binding.py b/src/textual/binding.py index 09e0b3b892..fbf5afd4c2 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -81,6 +81,8 @@ class Binding: If specified in the App's keymap then Textual will use this ID to lookup the binding, and substitute the `key` property of the Binding with the key specified in the keymap. """ + system: bool = False + """Make this binding a system binding, and hide it from apps.""" def parse_key(self) -> tuple[list[str], str]: """Parse a key in to a list of modifiers, and the actual key. @@ -148,6 +150,7 @@ def make_bindings(cls, bindings: Iterable[BindingType]) -> Iterable[Binding]: priority=binding.priority, tooltip=binding.tooltip, id=binding.id, + system=binding.system, ) diff --git a/src/textual/widgets/_key_panel.py b/src/textual/widgets/_key_panel.py index 6e0502f21b..e187a67982 100644 --- a/src/textual/widgets/_key_panel.py +++ b/src/textual/widgets/_key_panel.py @@ -71,7 +71,10 @@ def render_bindings_table(self) -> Table: action_to_bindings: defaultdict[str, list[tuple[Binding, bool, str]]] action_to_bindings = defaultdict(list) for _, binding, enabled, tooltip in table_bindings: - action_to_bindings[binding.action].append((binding, enabled, tooltip)) + if not binding.system: + action_to_bindings[binding.action].append( + (binding, enabled, tooltip) + ) description_style = self.get_component_rich_style( "bindings-table--description" diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 527ff31c67..7f1af6f6fa 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -268,7 +268,7 @@ class TextArea(ScrollView): | ctrl+w | 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. | - | ctrl+x | Delete the current line. | + | ctrl+shift+ | 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. | diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_classic_styling.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_classic_styling.svg index 64ecb85e83..79b68f5188 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_classic_styling.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_classic_styling.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-25414991-matrix { + .terminal-1862585679-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-25414991-title { + .terminal-1862585679-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-25414991-r1 { fill: #e0e0e0 } -.terminal-25414991-r2 { fill: #c5c8c6 } -.terminal-25414991-r3 { fill: #dde2e8;font-weight: bold } -.terminal-25414991-r4 { fill: #2c648c } + .terminal-1862585679-r1 { fill: #e0e0e0 } +.terminal-1862585679-r2 { fill: #c5c8c6 } +.terminal-1862585679-r3 { fill: #dde2e8;font-weight: bold } +.terminal-1862585679-r4 { fill: #2c648c } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ClassicFooterStylingApp + ClassicFooterStylingApp - - - - - - - - - - - - - - - - - - - - - - - - - - - ^t  Toggle Dark mode  ^q  Quit                                    ^p palette  + + + + + + + + + + + + + + + + + + + + + + + + + + + ^q  Quit  ^t  Toggle Dark mode                                    ^p palette  diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact.svg index 7b9ffa723e..2074324b24 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-4200977229-matrix { + .terminal-3694805181-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4200977229-title { + .terminal-3694805181-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4200977229-r1 { fill: #e0e0e0 } -.terminal-4200977229-r2 { fill: #c5c8c6 } -.terminal-4200977229-r3 { fill: #ffa62b;font-weight: bold } -.terminal-4200977229-r4 { fill: #495259 } + .terminal-3694805181-r1 { fill: #e0e0e0 } +.terminal-3694805181-r2 { fill: #c5c8c6 } +.terminal-3694805181-r3 { fill: #ffa62b;font-weight: bold } +.terminal-3694805181-r4 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ToggleCompactFooterApp + ToggleCompactFooterApp - - - - - - - - - - - - - - -                                 Compact Footer                                  - - - - - - - - - - - -^t Toggle Compact Footer ^q Quit                                    ^p palette + + + + + + + + + + + + + + +                                 Compact Footer                                  + + + + + + + + + + + +^t Toggle Compact Footer                                            ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact_with_hover.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact_with_hover.svg index 448da0bad6..0cad2e0382 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact_with_hover.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_compact_with_hover.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-2618956252-matrix { + .terminal-2309719869-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2618956252-title { + .terminal-2309719869-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2618956252-r1 { fill: #e0e0e0 } -.terminal-2618956252-r2 { fill: #c5c8c6 } -.terminal-2618956252-r3 { fill: #ffa62b;font-weight: bold } -.terminal-2618956252-r4 { fill: #495259 } + .terminal-2309719869-r1 { fill: #e0e0e0 } +.terminal-2309719869-r2 { fill: #c5c8c6 } +.terminal-2309719869-r3 { fill: #ffa62b;font-weight: bold } +.terminal-2309719869-r4 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ToggleCompactFooterApp + ToggleCompactFooterApp - - - - - - - - - - - - - - -                                 Compact Footer                                  - - - - - - - - - - - -^t Toggle Compact Footer^q Quit                                    ^p palette + + + + + + + + + + + + + + +                                 Compact Footer                                  + + + + + + + + + + + +^t Toggle Compact Footer^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_after_reactive_change.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_after_reactive_change.svg index 0d975273b1..cbd73724c7 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_after_reactive_change.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_after_reactive_change.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-368038807-matrix { + .terminal-753549575-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-368038807-title { + .terminal-753549575-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-368038807-r1 { fill: #e0e0e0 } -.terminal-368038807-r2 { fill: #c5c8c6 } -.terminal-368038807-r3 { fill: #ffa62b;font-weight: bold } -.terminal-368038807-r4 { fill: #495259 } + .terminal-753549575-r1 { fill: #e0e0e0 } +.terminal-753549575-r2 { fill: #c5c8c6 } +.terminal-753549575-r3 { fill: #ffa62b;font-weight: bold } +.terminal-753549575-r4 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ToggleCompactFooterApp + ToggleCompactFooterApp - - - - - - - - - - - - - - -                                Standard Footer                                  - - - - - - - - - - - - ^t Toggle Compact Footer  ^q Quit                                  ^p palette + + + + + + + + + + + + + + +                                Standard Footer                                  + + + + + + + + + + + + ^t Toggle Compact Footer                                           ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_with_hover.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_with_hover.svg index 436e4ab2f3..f95f831c31 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_with_hover.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_footer_standard_with_hover.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-177918869-matrix { + .terminal-3286534023-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-177918869-title { + .terminal-3286534023-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-177918869-r1 { fill: #e0e0e0 } -.terminal-177918869-r2 { fill: #c5c8c6 } -.terminal-177918869-r3 { fill: #ffa62b;font-weight: bold } -.terminal-177918869-r4 { fill: #495259 } + .terminal-3286534023-r1 { fill: #e0e0e0 } +.terminal-3286534023-r2 { fill: #c5c8c6 } +.terminal-3286534023-r3 { fill: #ffa62b;font-weight: bold } +.terminal-3286534023-r4 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ToggleCompactFooterApp + ToggleCompactFooterApp - - - - - - - - - - - - - - -                                Standard Footer                                  - - - - - - - - - - - - ^t Toggle Compact Footer  ^q Quit                                  ^p palette + + + + + + + + + + + + + + +                                Standard Footer                                  + + + + + + + + + + + + ^t Toggle Compact Footer ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg index ea40e194ef..d886a21616 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel.svg @@ -19,160 +19,161 @@ font-weight: 700; } - .terminal-3974648907-matrix { + .terminal-3186447019-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3974648907-title { + .terminal-3186447019-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3974648907-r1 { fill: #121212 } -.terminal-3974648907-r2 { fill: #0178d4 } -.terminal-3974648907-r3 { fill: #4f4f4f } -.terminal-3974648907-r4 { fill: #c5c8c6 } -.terminal-3974648907-r5 { fill: #fea62b;font-weight: bold } -.terminal-3974648907-r6 { fill: #e0e0e0 } + .terminal-3186447019-r1 { fill: #121212 } +.terminal-3186447019-r2 { fill: #0178d4 } +.terminal-3186447019-r3 { fill: #4f4f4f } +.terminal-3186447019-r4 { fill: #c5c8c6 } +.terminal-3186447019-r5 { fill: #fea62b;font-weight: bold } +.terminal-3186447019-r6 { fill: #e0e0e0 } +.terminal-3186447019-r7 { fill: #000000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HelpPanelApp + HelpPanelApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -         ↑Scroll Up       -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁         ↓Scroll Down     -         ←Move cursor     -left            -         →Move cursor     -right           -   home ^aGo to start     -    end ^eGo to end       -      pgupPage Up         -      pgdnPage Down       -     ^pgupPage Left       -     ^pgdnPage Right      -   shift+←Move cursor     -left and select -        ^←Move cursor     -left a word     -  shift+^←Move cursor     -left a word and -select          -   shift+→Move cursor     -right and       -select          -        ^→Move cursor     -right a word    -  shift+^→Move cursor     -right a word    -and select      -         ⌫Delete          -character left  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +         ↑Scroll Up       +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁         ↓Scroll Down     +         ←Move cursor     +left            +         →Move cursor     +right           +   home ^aGo to start     +    end ^eGo to end       +      pgupPage Up         +      pgdnPage Down       +     ^pgupPage Left       +     ^pgdnPage Right      +   shift+←Move cursor     +left and select▁▁ +        ^←Move cursor     +left a word     +  shift+^←Move cursor     +left a word and +select          +   shift+→Move cursor     +right and       +select          +        ^→Move cursor     +right a word    +  shift+^→Move cursor     +right a word    +and select      +         ⌫Delete          +character left  diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel_key_display_not_duplicated.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel_key_display_not_duplicated.svg index 4b59f103a2..3cd85cbfa4 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel_key_display_not_duplicated.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_help_panel_key_display_not_duplicated.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-1571702058-matrix { + .terminal-4067586418-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1571702058-title { + .terminal-4067586418-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1571702058-r1 { fill: #e0e0e0 } -.terminal-1571702058-r2 { fill: #4f4f4f } -.terminal-1571702058-r3 { fill: #c5c8c6 } -.terminal-1571702058-r4 { fill: #121212 } -.terminal-1571702058-r5 { fill: #fea62b;font-weight: bold } -.terminal-1571702058-r6 { fill: #8d8d8d } -.terminal-1571702058-r7 { fill: #ffa62b;font-weight: bold } -.terminal-1571702058-r8 { fill: #495259 } + .terminal-4067586418-r1 { fill: #e0e0e0 } +.terminal-4067586418-r2 { fill: #4f4f4f } +.terminal-4067586418-r3 { fill: #c5c8c6 } +.terminal-4067586418-r4 { fill: #121212 } +.terminal-4067586418-r5 { fill: #fea62b;font-weight: bold } +.terminal-4067586418-r6 { fill: #8d8d8d } +.terminal-4067586418-r7 { fill: #ffa62b;font-weight: bold } +.terminal-4067586418-r8 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HelpPanelApp + HelpPanelApp - - - - -      tabFocus Next      -shift+tabFocus Previous  - -       ^cQuit            -      fooRing the bell   -       ^ppalette Open  -command palette - - - - - - - - - - - - - - - - foo Ring the bell                    ^p palette + + + + +      tabFocus Next      +shift+tabFocus Previous  + +       ^qQuit Quit the  +app and return  +to the command  +prompt. +      fooRing the bell   +       ^ppalette Open  +command palette + + + + + + + + + + + + + foo Ring the bell                    ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_key_display.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_key_display.svg index 59bb0e3fad..abf2855a37 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_key_display.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_key_display.svg @@ -19,134 +19,134 @@ font-weight: 700; } - .terminal-457306101-matrix { + .terminal-4070961141-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-457306101-title { + .terminal-4070961141-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-457306101-r1 { fill: #e0e0e0 } -.terminal-457306101-r2 { fill: #c5c8c6 } -.terminal-457306101-r3 { fill: #ffa62b;font-weight: bold } -.terminal-457306101-r4 { fill: #495259 } + .terminal-4070961141-r1 { fill: #e0e0e0 } +.terminal-4070961141-r2 { fill: #c5c8c6 } +.terminal-4070961141-r3 { fill: #ffa62b;font-weight: bold } +.terminal-4070961141-r4 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - KeyDisplayApp + KeyDisplayApp - - - - - - - - - - - - - - - - - - - - - - - - - - - ? Question  ^q Quit app  esc Escape  a Letter A                    ^p palette + + + + + + + + + + + + + + + + + + + + + + + + + + + ^q Quit app  ? Question  esc Escape  a Letter A                    ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_display_footer_and_help_panel.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_display_footer_and_help_panel.svg index 6a76775c8c..92e02a2f01 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_display_footer_and_help_panel.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_display_footer_and_help_panel.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-1973286530-matrix { + .terminal-3686474457-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1973286530-title { + .terminal-3686474457-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1973286530-r1 { fill: #e0e0e0 } -.terminal-1973286530-r2 { fill: #4f4f4f } -.terminal-1973286530-r3 { fill: #c5c8c6 } -.terminal-1973286530-r4 { fill: #121212 } -.terminal-1973286530-r5 { fill: #fea62b;font-weight: bold } -.terminal-1973286530-r6 { fill: #8d8d8d } -.terminal-1973286530-r7 { fill: #ffa62b;font-weight: bold } -.terminal-1973286530-r8 { fill: #495259 } + .terminal-3686474457-r1 { fill: #e0e0e0 } +.terminal-3686474457-r2 { fill: #4f4f4f } +.terminal-3686474457-r3 { fill: #c5c8c6 } +.terminal-3686474457-r4 { fill: #121212 } +.terminal-3686474457-r5 { fill: #fea62b;font-weight: bold } +.terminal-3686474457-r6 { fill: #8d8d8d } +.terminal-3686474457-r7 { fill: #ffa62b;font-weight: bold } +.terminal-3686474457-r8 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Counter + Counter - - - - Counter                                            -      tabFocus Next      -shift+tabFocus Previous  - -       ^cQuit            -       ^ppalette Open  -command palette -      k +Increment       -    ↓ - jDecrement       - - - - - - - - - - - - - - - k Increment  ↓ Decrement             ^p palette + + + + Counter                                            +      tabFocus Next      +shift+tabFocus Previous  + +       ^qQuit Quit the  +app and return  +to the command  +prompt. +       ^ppalette Open  +command palette +      k +Increment       +    ↓ - jDecrement       + + + + + + + + + + + + k Increment  ↓ Decrement             ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_key_display.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_key_display.svg index facfbf311a..1801196c43 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_key_display.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_keymap_bindings_key_display.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-911952663-matrix { + .terminal-2134341471-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-911952663-title { + .terminal-2134341471-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-911952663-r1 { fill: #e0e0e0 } -.terminal-911952663-r2 { fill: #4f4f4f } -.terminal-911952663-r3 { fill: #c5c8c6 } -.terminal-911952663-r4 { fill: #121212 } -.terminal-911952663-r5 { fill: #fea62b;font-weight: bold } -.terminal-911952663-r6 { fill: #8d8d8d } -.terminal-911952663-r7 { fill: #ffa62b;font-weight: bold } -.terminal-911952663-r8 { fill: #495259 } + .terminal-2134341471-r1 { fill: #e0e0e0 } +.terminal-2134341471-r2 { fill: #4f4f4f } +.terminal-2134341471-r3 { fill: #c5c8c6 } +.terminal-2134341471-r4 { fill: #121212 } +.terminal-2134341471-r5 { fill: #fea62b;font-weight: bold } +.terminal-2134341471-r6 { fill: #8d8d8d } +.terminal-2134341471-r7 { fill: #ffa62b;font-weight: bold } +.terminal-2134341471-r8 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - Check the footer and help panel                    -      tabFocus Next      -shift+tabFocus Previous  - -       ^cQuit            -       ^ppalette Open  -command palette -  correctIncrement       - - - - - - - - - - - - - - - - correct Increment                    ^p palette + + + + Check the footer and help panel                    +      tabFocus Next      +shift+tabFocus Previous  + +       ^qQuit Quit the  +app and return  +to the command  +prompt. +       ^ppalette Open  +command palette +  correctIncrement       + + + + + + + + + + + + + correct Increment                    ^p palette diff --git a/tests/snapshot_tests/snapshot_apps/footer_toggle_compact.py b/tests/snapshot_tests/snapshot_apps/footer_toggle_compact.py index de8a01d9f3..a81034597f 100644 --- a/tests/snapshot_tests/snapshot_apps/footer_toggle_compact.py +++ b/tests/snapshot_tests/snapshot_apps/footer_toggle_compact.py @@ -11,7 +11,6 @@ class ToggleCompactFooterApp(App): BINDINGS = [ ("ctrl+t", "toggle_compact_footer", "Toggle Compact Footer"), - ("ctrl+q", "quit", "Quit"), ] def compose(self) -> ComposeResult: diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 574ca8a295..69400f2230 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -40,10 +40,11 @@ async def test_just_app_no_bindings() -> None: """An app with no bindings should have no bindings, other than the app's hard-coded ones.""" async with NoBindings().run_test() as pilot: assert list(pilot.app._bindings.key_to_bindings.keys()) == [ + "ctrl+q", "ctrl+c", "ctrl+p", ] - assert pilot.app._bindings.get_bindings_for_key("ctrl+c")[0].priority is True + assert pilot.app._bindings.get_bindings_for_key("ctrl+q")[0].priority is True ############################################################################## @@ -65,9 +66,9 @@ async def test_just_app_alpha_binding() -> None: """An app with a single binding should have just the one binding.""" async with AlphaBinding().run_test() as pilot: assert sorted(pilot.app._bindings.key_to_bindings.keys()) == sorted( - ["ctrl+c", "ctrl+p", "a"] + ["ctrl+c", "ctrl+p", "ctrl+q", "a"] ) - assert pilot.app._bindings.get_bindings_for_key("ctrl+c")[0].priority is True + assert pilot.app._bindings.get_bindings_for_key("ctrl+q")[0].priority is True assert pilot.app._bindings.get_bindings_for_key("a")[0].priority is True @@ -89,9 +90,9 @@ async def test_just_app_low_priority_alpha_binding() -> None: """An app with a single low-priority binding should have just the one binding.""" async with LowAlphaBinding().run_test() as pilot: assert sorted(pilot.app._bindings.key_to_bindings.keys()) == sorted( - ["ctrl+c", "ctrl+p", "a"] + ["ctrl+c", "ctrl+p", "ctrl+q", "a"] ) - assert pilot.app._bindings.get_bindings_for_key("ctrl+c")[0].priority is True + assert pilot.app._bindings.get_bindings_for_key("ctrl+q")[0].priority is True assert pilot.app._bindings.get_bindings_for_key("a")[0].priority is False diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py index 17d7f52f02..4724b74ea8 100644 --- a/tests/text_area/test_edit_via_bindings.py +++ b/tests/text_area/test_edit_via_bindings.py @@ -184,7 +184,7 @@ async def test_delete_line(selection, expected_result): text_area.load_text("0123456789") text_area.selection = selection - await pilot.press("ctrl+x") + await pilot.press("ctrl+shift+x") assert text_area.selection == Selection.cursor((0, 0)) assert text_area.text == expected_result @@ -219,7 +219,7 @@ async def test_delete_line_multiline_document(selection, expected_result): text_area.load_text("012\n345\n678\n9\n") text_area.selection = selection - await pilot.press("ctrl+x") + await pilot.press("ctrl+shift+x") cursor_row, cursor_column = text_area.cursor_location assert text_area.selection == Selection.cursor((cursor_row, cursor_column)) From a0c166cc6de453ac1b0f0de2e291a7b145addc8a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:05:03 +0000 Subject: [PATCH 09/15] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf85c7312a..83eacc5cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## Unlreleased +## Unreleased ### Added From 004a62e4bf24d3de42ea027479c779609ce43f63 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:06:45 +0000 Subject: [PATCH 10/15] fix key table --- src/textual/widgets/_text_area.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 7f1af6f6fa..2ff79f1072 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -268,7 +268,7 @@ class TextArea(ScrollView): | ctrl+w | 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. | - | ctrl+shift+ | Delete the current line. | + | ctrl+shift+x | 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. | From d040b644f0c02166ce6c3665854e7057f394264a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:12:20 +0000 Subject: [PATCH 11/15] update docstring --- src/textual/binding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/binding.py b/src/textual/binding.py index fbf5afd4c2..a04bb5cf60 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -82,7 +82,7 @@ class Binding: and substitute the `key` property of the Binding with the key specified in the keymap. """ system: bool = False - """Make this binding a system binding, and hide it from apps.""" + """Make this binding a system binding, which removes it from the key panel.""" def parse_key(self) -> tuple[list[str], str]: """Parse a key in to a list of modifiers, and the actual key. From e723ca2746e6eb5d72549d34b9934b7277ee6d6a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:14:11 +0000 Subject: [PATCH 12/15] changelog --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83eacc5cee..25fb5ae6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,14 +9,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Added `App.clipboard` -- Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea -- Added `system` boolean to Binding, which hides the binding from the help panel +- Added `App.clipboard` https://github.com/Textualize/textual/pull/5352 +- Added standard cut/copy/paste (ctrl+x, ctrl+c, ctrl+v) bindings to Input / TextArea https://github.com/Textualize/textual/pull/5352 +- Added `system` boolean to Binding, which hides the binding from the help panel https://github.com/Textualize/textual/pull/5352 ### Changed -- Change default quit key to `ctrl+q` -- Changed delete line binding on TextArea to use `ctrl+shift+x` +- Change default quit key to `ctrl+q` https://github.com/Textualize/textual/pull/5352 +- Changed delete line binding on TextArea to use `ctrl+shift+x` https://github.com/Textualize/textual/pull/5352 ## [0.89.1] - 2024-11-05 From b2bc4c98a50589e777e1cee7cdafee164883f7de Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:56:01 +0000 Subject: [PATCH 13/15] simplify --- src/textual/widgets/_input.py | 8 ++------ tests/text_area/test_textarea_cut_copy_paste.py | 10 +++++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 530a774435..a6e25d70e1 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -999,9 +999,7 @@ async def action_submit(self) -> None: def action_cut(self) -> None: """Cut the current selection (copy to clipboard and remove from input).""" - start, end = sorted(self.selection) - text = self.value[start:end] - self.app.copy_to_clipboard(text) + self.app.copy_to_clipboard(self.selected_text) self.delete_selection() def action_copy(self) -> None: @@ -1012,6 +1010,4 @@ def action_paste(self) -> None: """Paste from the local clipboard.""" clipboard = self.app._clipboard start, end = sorted(self.selection) - new_value = self.value[:start] + clipboard + self.value[end:] - self.value = new_value - self.cursor_position = start + len(clipboard) + self.replace(clipboard, start, end) diff --git a/tests/text_area/test_textarea_cut_copy_paste.py b/tests/text_area/test_textarea_cut_copy_paste.py index a762fdacf4..3922eb41e2 100644 --- a/tests/text_area/test_textarea_cut_copy_paste.py +++ b/tests/text_area/test_textarea_cut_copy_paste.py @@ -16,7 +16,7 @@ async def test_cut(): await pilot.press(*"Hello, World") await pilot.press("left", "shift+left", "shift+left") await pilot.press("ctrl+x") - assert text_area.document.text == "Hello, Wod" + assert text_area.text == "Hello, Wod" assert app.clipboard == "rl" @@ -29,7 +29,7 @@ async def test_copy(): await pilot.press(*"Hello, World") await pilot.press("left", "shift+left", "shift+left") await pilot.press("ctrl+c") - assert text_area.document.text == "Hello, World" + assert text_area.text == "Hello, World" assert app.clipboard == "rl" @@ -44,9 +44,9 @@ async def test_paste(): "shift+left", "shift+left", "shift+left", "shift+left", "shift+left" ) await pilot.press("ctrl+c") - assert text_area.document.text == "Hello, World" + assert text_area.text == "Hello, World" assert app.clipboard == "World" await pilot.press("ctrl+v") - assert text_area.document.text == "Hello, World" + assert text_area.text == "Hello, World" await pilot.press("ctrl+v") - assert text_area.document.text == "Hello, WorldWorld" + assert text_area.text == "Hello, WorldWorld" From 1e6f3313aa9644c2379f9d356a79cc85aff71a3a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 16:58:24 +0000 Subject: [PATCH 14/15] simplify paste --- src/textual/widgets/_text_area.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 2ff79f1072..475f6efb7b 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -2225,9 +2225,9 @@ def action_paste(self) -> None: """Paste from local clipboard.""" if self.read_only: return - clipboard = self.app._clipboard - start, end = self.selection - self._replace_via_keyboard(clipboard, start, end) + clipboard = self.app.clipboard + if result := self._replace_via_keyboard(clipboard, *self.selection): + self.move_cursor(result.end_location) def action_delete_to_start_of_line(self) -> None: """Deletes from the cursor location to the start of the line.""" From 8a09562b12d85d3bb7358354608a1be8e13498d8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 6 Dec 2024 17:05:11 +0000 Subject: [PATCH 15/15] no need to sort --- src/textual/widgets/_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index a6e25d70e1..dd8d0aac13 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -1009,5 +1009,5 @@ def action_copy(self) -> None: def action_paste(self) -> None: """Paste from the local clipboard.""" clipboard = self.app._clipboard - start, end = sorted(self.selection) + start, end = self.selection self.replace(clipboard, start, end)