diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e9973345..25fb5ae6e1 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/). +## Unreleased + +### Added + +- 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` 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 ### Fixed 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..3a8c4ea207 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -438,7 +438,15 @@ 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=False, + priority=True, + ), + Binding("ctrl+c", "help_quit", show=False, system=True), ] """The default key bindings.""" @@ -767,6 +775,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"}: @@ -866,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.""" @@ -1497,6 +1517,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 @@ -3605,6 +3626,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/binding.py b/src/textual/binding.py index 09e0b3b892..a04bb5cf60 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, 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. @@ -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/_input.py b/src/textual/widgets/_input.py index 7a14fc393f..dd8d0aac13 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,17 @@ 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).""" + self.app.copy_to_clipboard(self.selected_text) + self.delete_selection() + + 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 = self.selection + self.replace(clipboard, start, end) 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 2b68efbc60..475f6efb7b 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 ), @@ -265,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+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. | @@ -2199,6 +2202,33 @@ 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.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.""" + if self.read_only: + return + 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_location = self.selection.end 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" 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)) 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..3922eb41e2 --- /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.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.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.text == "Hello, World" + assert app.clipboard == "World" + await pilot.press("ctrl+v") + assert text_area.text == "Hello, World" + await pilot.press("ctrl+v") + assert text_area.text == "Hello, WorldWorld"