diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db1a6405a..9fb99ff770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fix priority bindings not appearing in footer when key clashes with focused widget https://github.com/Textualize/textual/pull/4342 +- Reverted auto-width change https://github.com/Textualize/textual/pull/4369 + +### Changed + - Exceptions inside `Widget.compose` or workers weren't bubbling up in tests https://github.com/Textualize/textual/issues/4282 - Fixed `DataTable` scrolling issues by changing `max-height` back to 100% https://github.com/Textualize/textual/issues/4286 +- Fixed `Button` not rendering correctly with console markup https://github.com/Textualize/textual/issues/4328 ### Added - Added `Document.start` and `end` location properties for convenience https://github.com/Textualize/textual/pull/4267 +- Added support for JavaScript, Golang, Rust, Bash, Java and Kotlin to `TextArea` https://github.com/Textualize/textual/pull/4350 +- Added `inline` parameter to `run` and `run_async` to run app inline (under the prompt). https://github.com/Textualize/textual/pull/4343 +- Added `mouse` parameter to disable mouse support https://github.com/Textualize/textual/pull/4343 ## [0.54.0] - 2024-03-26 diff --git a/docs/examples/how-to/inline01.py b/docs/examples/how-to/inline01.py new file mode 100644 index 0000000000..65bd22f303 --- /dev/null +++ b/docs/examples/how-to/inline01.py @@ -0,0 +1,31 @@ +from datetime import datetime + +from textual.app import App, ComposeResult +from textual.widgets import Digits + + +class ClockApp(App): + CSS = """ + Screen { + align: center middle; + } + #clock { + width: auto; + } + """ + + def compose(self) -> ComposeResult: + yield Digits("", id="clock") + + def on_ready(self) -> None: + self.update_clock() + self.set_interval(1, self.update_clock) + + def update_clock(self) -> None: + clock = datetime.now().time() + self.query_one(Digits).update(f"{clock:%T}") + + +if __name__ == "__main__": + app = ClockApp() + app.run(inline=True) # (1)! diff --git a/docs/examples/how-to/inline02.py b/docs/examples/how-to/inline02.py new file mode 100644 index 0000000000..05b76e626a --- /dev/null +++ b/docs/examples/how-to/inline02.py @@ -0,0 +1,38 @@ +from datetime import datetime + +from textual.app import App, ComposeResult +from textual.widgets import Digits + + +class ClockApp(App): + CSS = """ + Screen { + align: center middle; + &:inline { + border: none; + height: 50vh; + Digits { + color: $success; + } + } + } + #clock { + width: auto; + } + """ + + def compose(self) -> ComposeResult: + yield Digits("", id="clock") + + def on_ready(self) -> None: + self.update_clock() + self.set_interval(1, self.update_clock) + + def update_clock(self) -> None: + clock = datetime.now().time() + self.query_one(Digits).update(f"{clock:%T}") + + +if __name__ == "__main__": + app = ClockApp() + app.run(inline=True) diff --git a/docs/examples/widgets/clock.py b/docs/examples/widgets/clock.py index 6b18af3c85..10fb281d99 100644 --- a/docs/examples/widgets/clock.py +++ b/docs/examples/widgets/clock.py @@ -28,4 +28,4 @@ def update_clock(self) -> None: if __name__ == "__main__": app = ClockApp() - app.run() + app.run(inline=True) diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 5eccb2391a..daff1349a3 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -321,12 +321,13 @@ The `background: green` is only applied to the Button underneath the mouse curso Here are some other pseudo classes: +- `:blur` Matches widgets which *do not* have input focus. +- `:dark` Matches widgets in dark mode (where `App.dark == True`). - `:disabled` Matches widgets which are in a disabled state. - `:enabled` Matches widgets which are in an enabled state. -- `:focus` Matches widgets which have input focus. -- `:blur` Matches widgets which *do not* have input focus. - `:focus-within` Matches widgets with a focused child widget. -- `:dark` Matches widgets in dark mode (where `App.dark == True`). +- `:focus` Matches widgets which have input focus. +- `:inline` Matches widgets when the app is running in inline mode. - `:light` Matches widgets in dark mode (where `App.dark == False`). ## Combinators diff --git a/docs/guide/animation.md b/docs/guide/animation.md index 50bc87d35d..1bf55fb203 100644 --- a/docs/guide/animation.md +++ b/docs/guide/animation.md @@ -1,6 +1,6 @@ # Animation -Ths chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading. +This chapter discusses how to use Textual's animation system to create visual effects such as movement, blending, and fading. ## Animating styles diff --git a/docs/guide/app.md b/docs/guide/app.md index 5a59322802..7bc9e69e26 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -38,6 +38,14 @@ If you hit ++ctrl+c++ Textual will exit application mode and return you to the c A side effect of application mode is that you may no longer be able to select and copy text in the usual way. Terminals typically offer a way to bypass this limit with a key modifier. On iTerm you can select text if you hold the ++option++ key. See the documentation for your terminal software for how to select text in application mode. +#### Run inline + +!!! tip "Added in version 0.45.0" + +You can also run apps in _inline_ mode, which will cause the app to appear beneath the prompt (and won't go in to application mode). +Inline apps are useful for tools that integrate closely with the typical workflow of a terminal. + +To run an app in inline mode set the `inline` parameter to `True` when you call [App.run()][textual.app.App.run]. See [Style Inline Apps](../how-to/style-inline-apps.md) for how to apply additional styles to inline apps. ## Events diff --git a/docs/how-to/style-inline-apps.md b/docs/how-to/style-inline-apps.md new file mode 100644 index 0000000000..a726f048c3 --- /dev/null +++ b/docs/how-to/style-inline-apps.md @@ -0,0 +1,37 @@ +# Style Inline Apps + +Version 0.55.0 of Textual added support for running apps *inline* (below the prompt). +Running an inline app is as simple as adding `inline=True` to [`run()`][textual.app.App.run]. + + + +Your apps will typically run inline without modification, but you may want to make some tweaks for inline mode, which you can do with a little CSS. +This How-To will explain how. + +Let's look at an inline app. +The following app displays the the current time (and keeps it up to date). + +```python hl_lines="31" +--8<-- "docs/examples/how-to/inline01.py" +``` + +1. The `inline=True` runs the app inline. + +With Textual's default settings, this clock will be displayed in 5 lines; 3 for the digits and 2 for a top and bottom border. + +You can change the height or the border with CSS and the `:inline` pseudo-selector, which only matches rules in inline mode. +Let's update this app to remove the default border, and increase the height: + +```python hl_lines="11-17" +--8<-- "docs/examples/how-to/inline02.py" +``` + +The highlighted CSS targets online inline mode. +By setting the `height` rule on Screen we can define how many lines the app should consume when it runs. +Setting `border: none` removes the default border when running in inline mode. + +We've also added a rule to change the color of the clock when running inline. + +## Summary + +Most apps will not require modification to run inline, but if you want to tweak the height and border you can write CSS that targets inline mode with the `:inline` pseudo-selector. diff --git a/docs/widgets/selection_list.md b/docs/widgets/selection_list.md index 1ece006784..73de670754 100644 --- a/docs/widgets/selection_list.md +++ b/docs/widgets/selection_list.md @@ -30,8 +30,8 @@ my_selection_list: SelectionList[int] = SelectionList(*selections) ## Examples A selection list is designed to be built up of single-line prompts (which -can be [Rich renderables](../guide/widgets.md#rich-renderables)) and an -associated unique value. +can be [Rich `Text`](https://rich.readthedocs.io/en/stable/text.html)) and +an associated unique value. ### Selections as tuples diff --git a/examples/calculator.py b/examples/calculator.py index 9c8f2f9e76..8f6442ebee 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -168,4 +168,4 @@ def pressed_equals(self) -> None: if __name__ == "__main__": - CalculatorApp().run() + CalculatorApp().run(inline=True) diff --git a/examples/calculator.tcss b/examples/calculator.tcss index f25b387fcd..180e3f8e0b 100644 --- a/examples/calculator.tcss +++ b/examples/calculator.tcss @@ -12,6 +12,10 @@ Screen { min-height: 25; min-width: 26; height: 100%; + + &:inline { + margin: 0 2; + } } Button { diff --git a/examples/code_browser.tcss b/examples/code_browser.tcss index 05928614b3..21fc7cad00 100644 --- a/examples/code_browser.tcss +++ b/examples/code_browser.tcss @@ -1,5 +1,8 @@ Screen { background: $surface-darken-1; + &:inline { + height: 50vh; + } } #tree-view { diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 7dd0863b43..4fa9912b41 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -223,6 +223,7 @@ nav: - "how-to/design-a-layout.md" - "how-to/package-with-hatch.md" - "how-to/render-and-compose.md" + - "how-to/style-inline-apps.md" - "FAQ.md" - "roadmap.md" - "Blog": diff --git a/poetry.lock b/poetry.lock index 94c07ff975..a313be29fa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -383,63 +383,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.3" +version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, - {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, - {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, - {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, - {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, - {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, - {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, - {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, - {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, - {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, - {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.extras] @@ -472,18 +472,18 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.13.1" +version = "3.13.3" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, + {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] @@ -716,22 +716,22 @@ files = [ [[package]] name = "importlib-metadata" -version = "7.0.1" +version = "7.1.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -783,13 +783,13 @@ test = ["coverage", "pytest", "pytest-cov"] [[package]] name = "markdown" -version = "3.5.2" +version = "3.6" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, - {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] [package.dependencies] @@ -997,13 +997,13 @@ mkdocs = "*" [[package]] name = "mkdocs-material" -version = "9.5.12" +version = "9.5.15" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.12-py3-none-any.whl", hash = "sha256:d6f0c269f015e48c76291cdc79efb70f7b33bbbf42d649cfe475522ebee61b1f"}, - {file = "mkdocs_material-9.5.12.tar.gz", hash = "sha256:5f69cef6a8aaa4050b812f72b1094fda3d079b9a51cf27a247244c03ec455e97"}, + {file = "mkdocs_material-9.5.15-py3-none-any.whl", hash = "sha256:e5c96dec3d19491de49ca643fc1dbb92b278e43cdb816c775bc47db77d9b62fb"}, + {file = "mkdocs_material-9.5.15.tar.gz", hash = "sha256:39f03cca45e82bf54eb7456b5a18bd252eabfdd67f237a229471484a0a4d4635"}, ] [package.dependencies] @@ -1159,7 +1159,7 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [[package]] @@ -1263,38 +1263,38 @@ files = [ [[package]] name = "mypy" -version = "1.8.0" +version = "1.9.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] @@ -1335,13 +1335,13 @@ setuptools = "*" [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1430,13 +1430,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.7" +version = "10.7.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"}, - {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"}, + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, ] [package.dependencies] @@ -1470,13 +1470,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-asyncio" -version = "0.23.5" +version = "0.23.6" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, - {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, ] [package.dependencies] @@ -1525,13 +1525,13 @@ textual = ">=0.28.0" [[package]] name = "python-dateutil" -version = "2.9.0" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.9.0.tar.gz", hash = "sha256:78e73e19c63f5b20ffa567001531680d939dc042bf7850431877645523c66709"}, - {file = "python_dateutil-2.9.0-py2.py3-none-any.whl", hash = "sha256:cbf2f1da5e6083ac2fbfd4da39a25f34312230110440f424a14c7558bb85d82e"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -1560,6 +1560,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1567,8 +1568,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1585,6 +1593,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1592,6 +1601,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1772,18 +1782,18 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "69.1.1" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, - {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2046,13 +2056,13 @@ files = [ [[package]] name = "types-tree-sitter" -version = "0.20.1.20240106" +version = "0.20.1.20240311" description = "Typing stubs for tree-sitter" optional = false python-versions = ">=3.8" files = [ - {file = "types-tree-sitter-0.20.1.20240106.tar.gz", hash = "sha256:b0866a74942af5e223ceda9d1665befab9d55e0ccaa6704215efeaa2b02b0ca6"}, - {file = "types_tree_sitter-0.20.1.20240106-py3-none-any.whl", hash = "sha256:3b38b2500d3235f07644bc7148c5c9dbc929fdcb5d10d36709601028c9e0ebe2"}, + {file = "types-tree-sitter-0.20.1.20240311.tar.gz", hash = "sha256:a046e3da64efa68ac23dfeb7a6fbe10787294e802c5cad46d8b6daf765f5e409"}, + {file = "types_tree_sitter-0.20.1.20240311-py3-none-any.whl", hash = "sha256:fb7babaad37c907fa11904f33bae7f8a99b1a288b3c3099ed01f527fc0073069"}, ] [[package]] @@ -2288,23 +2298,23 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] -syntax = ["tree-sitter", "tree_sitter_languages"] +syntax = ["tree-sitter", "tree-sitter-languages"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "014186a223d4236fb0ace86bef24fb030d6921cf714a8b4d2b889ba066a26375" +content-hash = "4d935ffc4d465a2e51881f971fe3b9b62b9a1a8462ae511a512b5d7fae06a733" diff --git a/pyproject.toml b/pyproject.toml index 630cc27599..81f6ecfa7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ rich = ">=13.3.3" #rich = {path="../rich", develop=true} typing-extensions = "^4.4.0" tree-sitter = { version = "^0.20.1", optional = true } -tree_sitter_languages = { version = ">=1.7.0", optional = true } +tree-sitter-languages = { version = "1.10.2", optional = true } [tool.poetry.extras] syntax = ["tree-sitter", "tree_sitter_languages"] diff --git a/src/textual/_animator.py b/src/textual/_animator.py index 27d6c96ce5..21234fee8b 100644 --- a/src/textual/_animator.py +++ b/src/textual/_animator.py @@ -191,7 +191,7 @@ def __call__( attribute: Name of the attribute to animate. value: The value to animate to. final_value: The final value of the animation. Defaults to `value` if not set. - duration: The duration of the animate. + duration: The duration (in seconds) of the animation. speed: The speed of the animation. delay: A delay (in seconds) before the animation starts. easing: An easing method. diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 6b786e8b26..d9047e3fc3 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -144,6 +144,48 @@ def __rich_repr__(self) -> rich.repr.Result: yield self.region +@rich.repr.auto(angular=True) +class InlineUpdate(CompositorUpdate): + """A renderable to write an inline update.""" + + def __init__(self, strips: list[Strip]) -> None: + self.strips = strips + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + new_line = Segment.line() + for last, line in loop_last(self.strips): + yield from line + if not last: + yield new_line + + def render_segments(self, console: Console) -> str: + """Render the update to raw data, suitable for writing to terminal. + + Args: + console: Console instance. + + Returns: + Raw data with escape sequences. + """ + sequences: list[str] = [] + append = sequences.append + for last, strip in loop_last(self.strips): + append(strip.render(console)) + if not last: + append("\n") + append("\n\x1b[J") # Clear down + if len(self.strips) > 1: + append( + f"\x1b[{len(self.strips)}A\r" + ) # Move cursor back to original position + else: + append("\r") + append("\x1b[6n") # Query new cursor position + return "".join(sequences) + + @rich.repr.auto(angular=True) class ChopsUpdate(CompositorUpdate): """A renderable that applies updated spans to the screen.""" @@ -953,7 +995,7 @@ def render_update( """Render an update renderable. Args: - full: Enable full update, or `False` for a partial update. + screen_stack: Screen stack list. Defaults to None. Returns: A renderable for the update, or `None` if no update was required. @@ -966,6 +1008,21 @@ def render_update( else: return self.render_partial_update() + def render_inline( + self, size: Size, screen_stack: list[Screen] | None = None + ) -> RenderableType: + """Render an inline update. + + Args: + size: Inline size. + screen_stack: Screen stack list. Defaults to None. + + Returns: + A renderable. + """ + visible_screen_stack.set([] if screen_stack is None else screen_stack) + return InlineUpdate(self.render_strips(size)) + def render_full_update(self) -> LayoutUpdate: """Render a full update. @@ -999,14 +1056,19 @@ def render_partial_update(self) -> ChopsUpdate | None: chop_ends = [cut_set[1:] for cut_set in self.cuts] return ChopsUpdate(chops, spans, chop_ends) - def render_strips(self) -> list[Strip]: + def render_strips(self, size: Size | None = None) -> list[Strip]: """Render to a list of strips. + Args: + size: Size of render. + Returns: A list of strips with the screen content. """ - chops = self._render_chops(self.size.region, lambda y: True) - render_strips = [Strip.join(chop.values()) for chop in chops] + if size is None: + size = self.size + chops = self._render_chops(size.region, lambda y: True) + render_strips = [Strip.join(chop.values()) for chop in chops[: size.height]] return render_strips def _render_chops( diff --git a/src/textual/_layout.py b/src/textual/_layout.py index 8048a0f303..312f8c9fe5 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -160,7 +160,7 @@ def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> if not widget._nodes: width = 0 else: - arrangement = widget._arrange(Size(container.width, 0)) + arrangement = widget._arrange(Size(0, 0)) return arrangement.total_region.right return width diff --git a/src/textual/_resolve.py b/src/textual/_resolve.py index eecd99dcf1..647ee53f55 100644 --- a/src/textual/_resolve.py +++ b/src/textual/_resolve.py @@ -86,7 +86,7 @@ def resolve_fraction_unit( remaining_space: Fraction, resolve_dimension: Literal["width", "height"] = "width", ) -> Fraction: - """Calculate the fraction + """Calculate the fraction. Args: widget_styles: Styles for widgets with fraction units. diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py index 5a435b3287..ad2e607f84 100644 --- a/src/textual/_text_area_theme.py +++ b/src/textual/_text_area_theme.py @@ -222,6 +222,7 @@ def builtin_themes(cls) -> list[TextAreaTheme]: "method": Style(color="#A6E22E"), "method.call": Style(color="#A6E22E"), "boolean": Style(color="#66D9EF", italic=True), + "constant.builtin": Style(color="#66D9EF", italic=True), "json.null": Style(color="#66D9EF", italic=True), "regex.punctuation.bracket": Style(color="#F92672"), "regex.operator": Style(color="#F92672"), @@ -271,6 +272,7 @@ def builtin_themes(cls) -> list[TextAreaTheme]: "method": Style(color="#50fa7b"), "method.call": Style(color="#50fa7b"), "boolean": Style(color="#bd93f9"), + "constant.builtin": Style(color="#bd93f9"), "json.null": Style(color="#bd93f9"), "regex.punctuation.bracket": Style(color="#ff79c6"), "regex.operator": Style(color="#ff79c6"), @@ -320,6 +322,7 @@ def builtin_themes(cls) -> list[TextAreaTheme]: "method": Style(color="#4EC9B0"), "method.call": Style(color="#4EC9B0"), "boolean": Style(color="#7DAF9C"), + "constant.builtin": Style(color="#7DAF9C"), "json.null": Style(color="#7DAF9C"), "tag": Style(color="#EFCB43"), "yaml.field": Style(color="#569cd6", bold=True), @@ -365,6 +368,7 @@ def builtin_themes(cls) -> list[TextAreaTheme]: "function": Style(color="#6639BB"), "method": Style(color="#6639BB"), "boolean": Style(color="#7DAF9C"), + "constant.builtin": Style(color="#7DAF9C"), "tag": Style(color="#6639BB"), "yaml.field": Style(color="#6639BB"), "json.label": Style(color="#6639BB"), diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index 78a4fce4ac..fc84391f39 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -21,6 +21,8 @@ "^" + re.escape("\x1b[") + r"\?(?P\d+);(?P\d)\$y" ) +_re_cursor_position = re.compile(r"\x1b\[(?P\d+);(?P\d+)R") + BRACKETED_PASTE_START: Final[str] = "\x1b[200~" """Sequence received when a bracketed paste event starts.""" BRACKETED_PASTE_END: Final[str] = "\x1b[201~" @@ -235,8 +237,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: if key_events: break # Or a mouse event? - mouse_match = _re_mouse_event.match(sequence) - if mouse_match is not None: + if (mouse_match := _re_mouse_event.match(sequence)) is not None: mouse_code = mouse_match.group(0) event = self.parse_mouse_code(mouse_code) if event: @@ -245,14 +246,28 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None: # Or a mode report? # (i.e. the terminal saying it supports a mode we requested) - mode_report_match = _re_terminal_mode_response.match(sequence) - if mode_report_match is not None: + if ( + mode_report_match := _re_terminal_mode_response.match( + sequence + ) + ) is not None: if ( mode_report_match["mode_id"] == "2026" and int(mode_report_match["setting_parameter"]) > 0 ): on_token(messages.TerminalSupportsSynchronizedOutput()) break + + # Or a cursor position query? + if ( + cursor_position_match := _re_cursor_position.match(sequence) + ) is not None: + row, column = cursor_position_match.groups() + on_token( + events.CursorPosition(x=int(column) - 1, y=int(row) - 1) + ) + break + else: if not bracketed_paste: for event in sequence_to_key_events(character): diff --git a/src/textual/app.py b/src/textual/app.py index 15811232c9..d7966761db 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -45,7 +45,6 @@ Type, TypeVar, Union, - cast, overload, ) from weakref import WeakKeyDictionary, WeakSet @@ -92,7 +91,6 @@ from .design import ColorSystem from .dom import DOMNode, NoScreen from .driver import Driver -from .drivers.headless_driver import HeadlessDriver from .errors import NoWidget from .features import FeatureFlag, parse_features from .file_monitor import FileMonitor @@ -476,6 +474,9 @@ def __init__( self._mouse_down_widget: Widget | None = None """The widget that was most recently mouse downed (used to create click events).""" + self._previous_cursor_position = Offset(0, 0) + """The previous cursor position""" + self.cursor_position = Offset(0, 0) """The position of the terminal cursor in screen-space. @@ -741,7 +742,7 @@ def animate( attribute: Name of the attribute to animate. value: The value to animate to. final_value: The final value of the animation. - duration: The duration of the animate. + duration: The duration (in seconds) of the animation. speed: The speed of the animation. delay: A delay (in seconds) before the animation starts. easing: An easing method. @@ -779,12 +780,17 @@ def debug(self) -> bool: @property def is_headless(self) -> bool: - """Is the driver running in 'headless' mode? + """Is the app running in 'headless' mode? Headless mode is used when running tests with [run_test][textual.app.App.run_test]. """ return False if self._driver is None else self._driver.is_headless + @property + def is_inline(self) -> bool: + """Is the app running in 'inline' mode?""" + return False if self._driver is None else self._driver.is_inline + @property def screen_stack(self) -> Sequence[Screen[Any]]: """A snapshot of the current screen stack. @@ -855,15 +861,21 @@ def namespace_bindings(self) -> dict[str, tuple[DOMNode, Binding]]: This property may be used to inspect current bindings. Returns: - A mapping of keys onto pairs of nodes and bindings. + A map of keys to a tuple containing the DOMNode and Binding that key corresponds to. """ - namespace_binding_map: dict[str, tuple[DOMNode, Binding]] = {} + bindings_map: dict[str, tuple[DOMNode, Binding]] = {} for namespace, bindings in reversed(self._binding_chain): for key, binding in bindings.keys.items(): - namespace_binding_map[key] = (namespace, binding) + existing_key_and_binding = bindings_map.get(key) + if existing_key_and_binding: + _, existing_binding = existing_key_and_binding + if binding.priority and not existing_binding.priority: + bindings_map[key] = (namespace, binding) + else: + bindings_map[key] = (namespace, binding) - return namespace_binding_map + return bindings_map def _set_active(self) -> None: """Set this app to be the currently active app.""" @@ -979,6 +991,7 @@ def __rich_repr__(self) -> rich.repr.Result: @property def animator(self) -> Animator: + """The animator object.""" return self._animator @property @@ -1444,6 +1457,9 @@ async def run_async( self, *, headless: bool = False, + inline: bool = False, + inline_no_clear: bool = False, + mouse: bool = False, size: tuple[int, int] | None = None, auto_pilot: AutopilotCallbackType | None = None, ) -> ReturnType | None: @@ -1451,6 +1467,9 @@ async def run_async( Args: headless: Run in headless mode (no output). + inline: Run the app inline (under the prompt). + inline_no_clear: Don't clear the app output when exiting an inline app. + mouse: Enable mouse support. size: Force terminal size to `(WIDTH, HEIGHT)`, or None to auto-detect. auto_pilot: An auto pilot coroutine. @@ -1501,6 +1520,9 @@ async def run_auto_pilot( await app._process_messages( ready_callback=None if auto_pilot is None else app_ready, headless=headless, + inline=inline, + inline_no_clear=inline_no_clear, + mouse=mouse, terminal_size=size, ) finally: @@ -1516,6 +1538,9 @@ def run( self, *, headless: bool = False, + inline: bool = False, + inline_no_clear: bool = False, + mouse: bool = True, size: tuple[int, int] | None = None, auto_pilot: AutopilotCallbackType | None = None, ) -> ReturnType | None: @@ -1523,6 +1548,9 @@ def run( Args: headless: Run in headless mode (no output). + inline: Run the app inline (under the prompt). + inline_no_clear: Don't clear the app output when exiting an inline app. + mouse: Enable mouse support. size: Force terminal size to `(WIDTH, HEIGHT)`, or None to auto-detect. auto_pilot: An auto pilot coroutine. @@ -1538,6 +1566,9 @@ async def run_app() -> None: try: await self.run_async( headless=headless, + inline=inline, + inline_no_clear=inline_no_clear, + mouse=mouse, size=size, auto_pilot=auto_pilot, ) @@ -2297,6 +2328,9 @@ async def _process_messages( self, ready_callback: CallbackType | None = None, headless: bool = False, + inline: bool = False, + inline_no_clear: bool = False, + mouse: bool = True, terminal_size: tuple[int, int] | None = None, message_hook: Callable[[Message], None] | None = None, ) -> None: @@ -2314,7 +2348,6 @@ async def _process_messages( self.log.system("---") - self.log.system(driver=self.driver_class) self.log.system(loop=asyncio.get_running_loop()) self.log.system(features=self.features) if constants.LOG_FILE is not None: @@ -2406,15 +2439,26 @@ async def invoke_ready_callback() -> None: await self._dispatch_message(load_event) driver: Driver - driver_class = cast( - "type[Driver]", - HeadlessDriver if headless else self.driver_class, - ) + + driver_class: type[Driver] + if headless: + from .drivers.headless_driver import HeadlessDriver + + driver_class = HeadlessDriver + elif inline: + from .drivers.linux_inline_driver import LinuxInlineDriver + + driver_class = LinuxInlineDriver + else: + driver_class = self.driver_class + driver = self._driver = driver_class( self, debug=constants.DEBUG, + mouse=mouse, size=terminal_size, ) + self.log(driver=driver) if not self._exit: driver.start_application_mode() @@ -2424,6 +2468,15 @@ async def invoke_ready_callback() -> None: await run_process_messages() finally: + if self._driver.is_inline: + cursor_x, cursor_y = self._previous_cursor_position + self._driver.write( + Control.move(-cursor_x, -cursor_y).segment.text + ) + if inline_no_clear: + console = Console() + console.print(self.screen._compositor) + console.print() driver.stop_application_mode() except Exception as error: self._handle_exception(error) @@ -2737,11 +2790,16 @@ def _display(self, screen: Screen, renderable: RenderableType | None) -> None: try: try: if isinstance(renderable, CompositorUpdate): + cursor_x, cursor_y = self._previous_cursor_position + terminal_sequence = Control.move( + -cursor_x, -cursor_y + ).segment.text cursor_x, cursor_y = self.cursor_position - terminal_sequence = renderable.render_segments(console) - terminal_sequence += Control.move_to( + terminal_sequence += renderable.render_segments(console) + terminal_sequence += Control.move( cursor_x, cursor_y ).segment.text + self._previous_cursor_position = self.cursor_position else: segments = console.render(renderable) terminal_sequence = console._render_buffer(segments) @@ -3387,7 +3445,7 @@ def notify( message: The message for the notification. title: The title for the notification. severity: The severity of the notification. - timeout: The timeout for the notification. + timeout: The timeout (in seconds) for the notification. The `notify` method is used to create an application-wide notification, shown in a [`Toast`][textual.widgets._toast.Toast], diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 52c13cb067..27c662dde9 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -68,6 +68,7 @@ "focus-within", "focus", "hover", + "inline", "light", } VALID_OVERLAY: Final = {"none", "screen"} diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 8bc14d49c9..6566d61847 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -1152,7 +1152,7 @@ def animate( attribute: Name of the attribute to animate. value: The value to animate to. final_value: The final value of the animation. Defaults to `value` if not set. - duration: The duration of the animate. + duration: The duration (in seconds) of the animation. speed: The speed of the animation. delay: A delay (in seconds) before the animation starts. easing: An easing method. diff --git a/src/textual/demo.tcss b/src/textual/demo.tcss index 2e9d54e25b..fc5f45b23f 100644 --- a/src/textual/demo.tcss +++ b/src/textual/demo.tcss @@ -5,6 +5,9 @@ Screen { layers: base overlay notes notifications; overflow: hidden; + &:inline { + height: 50vh; + } } diff --git a/src/textual/document/_languages.py b/src/textual/document/_languages.py index a33f7544e8..fb313cf7ca 100644 --- a/src/textual/document/_languages.py +++ b/src/textual/document/_languages.py @@ -1,13 +1,19 @@ BUILTIN_LANGUAGES = sorted( [ - "markdown", - "yaml", - "sql", + "bash", "css", + "go", "html", + "java", + "javascript", "json", + "kotlin", + "markdown", "python", + "rust", "regex", + "sql", "toml", + "yaml", ] ) diff --git a/src/textual/driver.py b/src/textual/driver.py index c70edf9588..f0ad89c98b 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -20,6 +20,7 @@ def __init__( app: App, *, debug: bool = False, + mouse: bool = True, size: tuple[int, int] | None = None, ) -> None: """Initialize a driver. @@ -27,22 +28,30 @@ def __init__( Args: app: The App instance. debug: Enable debug mode. + mouse: Enable mouse support, size: Initial size of the terminal or `None` to detect. """ self._app = app self._debug = debug + self._mouse = mouse self._size = size self._loop = asyncio.get_running_loop() self._down_buttons: list[int] = [] self._last_move_event: events.MouseMove | None = None self._auto_restart = True """Should the application auto-restart (where appropriate)?""" + self.cursor_origin: tuple[int, int] | None = None @property def is_headless(self) -> bool: """Is the driver 'headless' (no output)?""" return False + @property + def is_inline(self) -> bool: + """Is the driver 'inline' (not full-screen)?""" + return False + @property def can_suspend(self) -> bool: """Can this driver be suspended?""" @@ -67,6 +76,17 @@ def process_event(self, event: events.Event) -> None: # NOTE: This runs in a thread. # Avoid calling methods on the app. event.set_sender(self._app) + if self.cursor_origin is None: + offset_x = 0 + offset_y = 0 + else: + offset_x, offset_y = self.cursor_origin + if isinstance(event, events.MouseEvent): + event.x -= offset_x + event.y -= offset_y + event.screen_x -= offset_x + event.screen_y -= offset_y + if isinstance(event, events.MouseDown): if event.button: self._down_buttons.append(event.button) @@ -79,6 +99,7 @@ def process_event(self, event: events.Event) -> None: and not event.button and self._last_move_event is not None ): + # Deduplicate self._down_buttons while preserving order. buttons = list(dict.fromkeys(self._down_buttons).keys()) self._down_buttons.clear() diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 3e7958837c..8b38190dc7 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -32,6 +32,7 @@ def __init__( app: App, *, debug: bool = False, + mouse: bool = True, size: tuple[int, int] | None = None, ) -> None: """Initialize Linux driver. @@ -39,9 +40,10 @@ def __init__( Args: app: The App instance. debug: Enable debug mode. + mouse: Enable mouse support. size: Initial size of the terminal or `None` to detect. """ - super().__init__(app, debug=debug, size=size) + super().__init__(app, debug=debug, mouse=mouse, size=size) self._file = sys.__stderr__ self.fileno = sys.__stdin__.fileno() self.attrs_before: list[Any] | None = None @@ -111,6 +113,8 @@ def _get_terminal_size(self) -> tuple[int, int]: def _enable_mouse_support(self) -> None: """Enable reporting of mouse events.""" + if not self._mouse: + return write = self.write write("\x1b[?1000h") # SET_VT200_MOUSE write("\x1b[?1003h") # SET_ANY_EVENT_MOUSE @@ -133,6 +137,8 @@ def _disable_bracketed_paste(self) -> None: def _disable_mouse_support(self) -> None: """Disable reporting of mouse events.""" + if not self._mouse: + return write = self.write write("\x1b[?1000l") # write("\x1b[?1003l") # diff --git a/src/textual/drivers/linux_inline_driver.py b/src/textual/drivers/linux_inline_driver.py new file mode 100644 index 0000000000..c43ff33e54 --- /dev/null +++ b/src/textual/drivers/linux_inline_driver.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +import asyncio +import os +import selectors +import signal +import sys +import termios +import tty +from codecs import getincrementaldecoder +from threading import Event, Thread +from typing import TYPE_CHECKING, Any + +import rich.repr + +from .. import events +from .._xterm_parser import XTermParser +from ..driver import Driver +from ..geometry import Size + +if TYPE_CHECKING: + from ..app import App + + +@rich.repr.auto(angular=True) +class LinuxInlineDriver(Driver): + + def __init__( + self, + app: App, + *, + debug: bool = False, + mouse: bool = True, + size: tuple[int, int] | None = None, + ): + super().__init__(app, debug=debug, mouse=mouse, size=size) + self._file = sys.__stderr__ + self.fileno = sys.__stdin__.fileno() + self.attrs_before: list[Any] | None = None + self.exit_event = Event() + + def __rich_repr__(self) -> rich.repr.Result: + yield self._app + + @property + def is_inline(self) -> bool: + return True + + def _enable_bracketed_paste(self) -> None: + """Enable bracketed paste mode.""" + self.write("\x1b[?2004h") + + def _disable_bracketed_paste(self) -> None: + """Disable bracketed paste mode.""" + self.write("\x1b[?2004l") + + def _get_terminal_size(self) -> tuple[int, int]: + """Detect the terminal size. + + Returns: + The size of the terminal as a tuple of (WIDTH, HEIGHT). + """ + width: int | None = 80 + height: int | None = 25 + import shutil + + try: + width, height = shutil.get_terminal_size() + except (AttributeError, ValueError, OSError): + try: + width, height = shutil.get_terminal_size() + except (AttributeError, ValueError, OSError): + pass + width = width or 80 + height = height or 25 + return width, height + + def _enable_mouse_support(self) -> None: + """Enable reporting of mouse events.""" + if not self._mouse: + return + write = self.write + write("\x1b[?1000h") # SET_VT200_MOUSE + write("\x1b[?1003h") # SET_ANY_EVENT_MOUSE + write("\x1b[?1015h") # SET_VT200_HIGHLIGHT_MOUSE + write("\x1b[?1006h") # SET_SGR_EXT_MODE_MOUSE + + # write("\x1b[?1007h") + self.flush() + + def _disable_mouse_support(self) -> None: + """Disable reporting of mouse events.""" + if not self._mouse: + return + write = self.write + write("\x1b[?1000l") # + write("\x1b[?1003l") # + write("\x1b[?1015l") + write("\x1b[?1006l") + self.flush() + + def write(self, data: str) -> None: + self._file.write(data) + + def _run_input_thread(self) -> None: + """ + Key thread target that wraps run_input_thread() to die gracefully if it raises + an exception + """ + try: + self.run_input_thread() + except BaseException as error: + import rich.traceback + + self._app.call_later( + self._app.panic, + rich.traceback.Traceback(), + ) + + def run_input_thread(self) -> None: + """Wait for input and dispatch events.""" + selector = selectors.SelectSelector() + selector.register(self.fileno, selectors.EVENT_READ) + + fileno = self.fileno + EVENT_READ = selectors.EVENT_READ + + def more_data() -> bool: + """Check if there is more data to parse.""" + + for _key, events in selector.select(0.01): + if events & EVENT_READ: + return True + return False + + parser = XTermParser(more_data, self._debug) + feed = parser.feed + + utf8_decoder = getincrementaldecoder("utf-8")().decode + decode = utf8_decoder + read = os.read + + try: + while not self.exit_event.is_set(): + selector_events = selector.select(0.1) + for _selector_key, mask in selector_events: + if mask & EVENT_READ: + unicode_data = decode( + read(fileno, 1024), final=self.exit_event.is_set() + ) + for event in feed(unicode_data): + if isinstance(event, events.CursorPosition): + self.cursor_origin = (event.x, event.y) + else: + self.process_event(event) + finally: + selector.close() + + def start_application_mode(self) -> None: + + loop = asyncio.get_running_loop() + + def send_size_event() -> None: + terminal_size = self._get_terminal_size() + width, height = terminal_size + textual_size = Size(width, height) + event = events.Resize(textual_size, textual_size) + asyncio.run_coroutine_threadsafe( + self._app._post_message(event), + loop=loop, + ) + + def on_terminal_resize(signum, stack) -> None: + self.write("\x1b[2J") + self.flush() + send_size_event() + + signal.signal(signal.SIGWINCH, on_terminal_resize) + + self.write("\x1b[?25l") # Hide cursor + self.write("\033[?1004h\n") # Enable FocusIn/FocusOut. + + self._enable_mouse_support() + try: + self.attrs_before = termios.tcgetattr(self.fileno) + except termios.error: + # Ignore attribute errors. + self.attrs_before = None + + try: + newattr = termios.tcgetattr(self.fileno) + except termios.error: + pass + else: + newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG]) + newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG]) + + # VMIN defines the number of characters read at a time in + # non-canonical mode. It seems to default to 1 on Linux, but on + # Solaris and derived operating systems it defaults to 4. (This is + # because the VMIN slot is the same as the VEOF slot, which + # defaults to ASCII EOT = Ctrl-D = 4.) + newattr[tty.CC][termios.VMIN] = 1 + + termios.tcsetattr(self.fileno, termios.TCSANOW, newattr) + + self._key_thread = Thread(target=self._run_input_thread) + send_size_event() + self._key_thread.start() + + @classmethod + def _patch_lflag(cls, attrs: int) -> int: + """Patch termios lflag. + + Args: + attributes: New set attributes. + + Returns: + New lflag. + + """ + # if TEXTUAL_ALLOW_SIGNALS env var is set, then allow Ctrl+C to send signals + ISIG = 0 if os.environ.get("TEXTUAL_ALLOW_SIGNALS") else termios.ISIG + + return attrs & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | ISIG) + + @classmethod + def _patch_iflag(cls, attrs: int) -> int: + return attrs & ~( + # Disable XON/XOFF flow control on output and input. + # (Don't capture Ctrl-S and Ctrl-Q.) + # Like executing: "stty -ixon." + termios.IXON + | termios.IXOFF + | + # Don't translate carriage return into newline on input. + termios.ICRNL + | termios.INLCR + | termios.IGNCR + ) + + def disable_input(self) -> None: + """Disable further input.""" + try: + if not self.exit_event.is_set(): + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + self._disable_mouse_support() + self.exit_event.set() + if self._key_thread is not None: + self._key_thread.join() + self.exit_event.clear() + termios.tcflush(self.fileno, termios.TCIFLUSH) + + except Exception as error: + # TODO: log this + pass + + def stop_application_mode(self) -> None: + """Stop application mode, restore state.""" + self._disable_bracketed_paste() + self.disable_input() + + self.write("\x1b[A\x1b[J") + + if self.attrs_before is not None: + try: + termios.tcsetattr(self.fileno, termios.TCSANOW, self.attrs_before) + except termios.error: + pass + + self.write("\x1b[?25h") # Show cursor + self.write("\033[?1004l\n") # Disable FocusIn/FocusOut. + + self.flush() diff --git a/src/textual/drivers/web_driver.py b/src/textual/drivers/web_driver.py index 535e3109c0..e1fc7106a6 100644 --- a/src/textual/drivers/web_driver.py +++ b/src/textual/drivers/web_driver.py @@ -40,7 +40,12 @@ class WebDriver(Driver): """A headless driver that may be run remotely.""" def __init__( - self, app: App, *, debug: bool = False, size: tuple[int, int] | None = None + self, + app: App, + *, + debug: bool = False, + mouse: bool = True, + size: tuple[int, int] | None = None, ): if size is None: try: @@ -50,7 +55,7 @@ def __init__( pass else: size = width, height - super().__init__(app, debug=debug, size=size) + super().__init__(app, debug=debug, mouse=mouse, size=size) self.stdout = sys.__stdout__ self.fileno = sys.__stdout__.fileno() self._write = partial(os.write, self.fileno) diff --git a/src/textual/drivers/windows_driver.py b/src/textual/drivers/windows_driver.py index 1df31728ac..9c53d4f6da 100644 --- a/src/textual/drivers/windows_driver.py +++ b/src/textual/drivers/windows_driver.py @@ -21,6 +21,7 @@ def __init__( app: App, *, debug: bool = False, + mouse: bool = True, size: tuple[int, int] | None = None, ) -> None: """Initialize Windows driver. @@ -28,9 +29,10 @@ def __init__( Args: app: The App instance. debug: Enable debug mode. + mouse: Enable mouse support. size: Initial size of the terminal or `None` to detect. """ - super().__init__(app, debug=debug, size=size) + super().__init__(app, debug=debug, mouse=mouse, size=size) self._file = sys.__stdout__ self.exit_event = Event() self._event_thread: Thread | None = None @@ -53,6 +55,8 @@ def write(self, data: str) -> None: def _enable_mouse_support(self) -> None: """Enable reporting of mouse events.""" + if not self._mouse: + return write = self.write write("\x1b[?1000h") # SET_VT200_MOUSE write("\x1b[?1003h") # SET_ANY_EVENT_MOUSE @@ -62,6 +66,8 @@ def _enable_mouse_support(self) -> None: def _disable_mouse_support(self) -> None: """Disable reporting of mouse events.""" + if not self._mouse: + return write = self.write write("\x1b[?1000l") write("\x1b[?1003l") diff --git a/src/textual/events.py b/src/textual/events.py index 68c3ae8f3d..ce5ecac4e3 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -55,6 +55,14 @@ class Shutdown(Event): pass +@dataclass +class CursorPosition(Event, bubble=False): + """Internal event used to retrieve the terminal's cursor position.""" + + x: int + y: int + + class Load(Event, bubble=False): """ Sent when the App is running but *before* the terminal is in application mode. diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7c9ef51b0e..6e5a784c6a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -350,7 +350,7 @@ def set_timer( """Make a function call after a delay. Args: - delay: Time to wait before invoking callback. + delay: Time (in seconds) to wait before invoking callback. callback: Callback to call after time has expired. name: Name of the timer (for debug). pause: Start timer paused. @@ -382,7 +382,7 @@ def set_interval( """Call a function at periodic intervals. Args: - interval: Time between calls. + interval: Time (in seconds) between calls. callback: Function to call. name: Name of the timer object. repeat: Number of times to repeat the call or 0 for continuous. diff --git a/src/textual/notifications.py b/src/textual/notifications.py index 27f95ccd6d..2b2489bfbf 100644 --- a/src/textual/notifications.py +++ b/src/textual/notifications.py @@ -37,7 +37,7 @@ class Notification: """The severity level for the notification.""" timeout: float = 5 - """The timeout for the notification.""" + """The timeout (in seconds) for the notification.""" raised_at: float = field(default_factory=time) """The time when the notification was raised (in Unix time).""" diff --git a/src/textual/screen.py b/src/textual/screen.py index b9713e7256..aa6410e31e 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -137,6 +137,13 @@ class Screen(Generic[ScreenResultType], Widget): layout: vertical; overflow-y: auto; background: $surface; + + &:inline { + height: auto; + min-height: 1; + border-top: tall $background; + border-bottom: tall $background; + } } """ @@ -664,7 +671,20 @@ async def _on_idle(self, event: events.Idle) -> None: def _compositor_refresh(self) -> None: """Perform a compositor refresh.""" - if self is self.app.screen: + + if self.app.is_inline: + size = self.app.size + self.app._display( + self, + self._compositor.render_inline( + Size(size.width, self._get_inline_height(size)), + screen_stack=self.app._background_screens, + ), + ) + self._dirty_widgets.clear() + self._compositor._dirty_regions.clear() + + elif self is self.app.screen: # Top screen update = self._compositor.render_update( screen_stack=self.app._background_screens @@ -754,6 +774,8 @@ def _pop_result_callback(self) -> None: def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> None: """Refresh the layout (can change size and positions of widgets).""" size = self.outer_size if size is None else size + if self.app.is_inline: + size = Size(size.width, self._get_inline_height(self.app.size)) if not size: return self._compositor.update_widgets(self._dirty_widgets) @@ -846,6 +868,30 @@ async def _on_update_scroll(self, message: messages.UpdateScroll) -> None: self._scroll_required = True self.check_idle() + def _get_inline_height(self, size: Size) -> int: + """Get the inline height (number of lines to display when running inline mode). + + Args: + size: Size of the terminal + + Returns: + Height for inline mode. + """ + height_scalar = self.styles.height + if height_scalar is None or height_scalar.is_auto: + inline_height = self.get_content_height(size, size, size.width) + else: + inline_height = int(height_scalar.resolve(size, size)) + inline_height += self.styles.gutter.height + min_height = self.styles.min_height + max_height = self.styles.max_height + if min_height is not None: + inline_height = max(inline_height, int(min_height.resolve(size, size))) + if max_height is not None: + inline_height = min(inline_height, int(max_height.resolve(size, size))) + inline_height = min(self.app.size.height - 1, inline_height) + return inline_height + def _screen_resized(self, size: Size): """Called by App when the screen is resized.""" self._refresh_layout(size) diff --git a/src/textual/tree-sitter/highlights/bash.scm b/src/textual/tree-sitter/highlights/bash.scm index 23bf03e697..f33a7c2d3a 100644 --- a/src/textual/tree-sitter/highlights/bash.scm +++ b/src/textual/tree-sitter/highlights/bash.scm @@ -1,145 +1,56 @@ -(simple_expansion) @none -(expansion - "${" @punctuation.special - "}" @punctuation.special) @none [ - "(" - ")" - "((" - "))" - "{" - "}" - "[" - "]" - "[[" - "]]" - ] @punctuation.bracket - -[ - ";" - ";;" - (heredoc_start) - ] @punctuation.delimiter - -[ - "$" -] @punctuation.special - -[ - ">" - ">>" - "<" - "<<" - "&" - "&&" - "|" - "||" - "=" - "=~" - "==" - "!=" - ] @operator - -[ - (string) - (raw_string) - (ansi_c_string) - (heredoc_body) -] @string @spell - -(variable_assignment (word) @string) - -[ - "if" - "then" - "else" - "elif" - "fi" - "case" - "in" - "esac" - ] @conditional - -[ - "for" - "do" - "done" - "select" - "until" - "while" - ] @repeat - -[ - "declare" - "export" - "local" - "readonly" - "unset" - ] @keyword - -"function" @keyword.function - -(special_variable_name) @constant - -; trap -l -((word) @constant.builtin - (#match? @constant.builtin "^SIG(HUP|INT|QUIT|ILL|TRAP|ABRT|BUS|FPE|KILL|USR[12]|SEGV|PIPE|ALRM|TERM|STKFLT|CHLD|CONT|STOP|TSTP|TT(IN|OU)|URG|XCPU|XFSZ|VTALRM|PROF|WINCH|IO|PWR|SYS|RTMIN([+]([1-9]|1[0-5]))?|RTMAX(-([1-9]|1[0-4]))?)$")) - -((word) @boolean - (#any-of? @boolean "true" "false")) - -(comment) @comment @spell -(test_operator) @string - -(command_substitution - [ "$(" ")" ] @punctuation.bracket) - -(process_substitution - [ "<(" ")" ] @punctuation.bracket) - - -(function_definition - name: (word) @function) - -(command_name (word) @function.call) - -((command_name (word) @function.builtin) - (#any-of? @function.builtin - "alias" "bg" "bind" "break" "builtin" "caller" "cd" - "command" "compgen" "complete" "compopt" "continue" - "coproc" "dirs" "disown" "echo" "enable" "eval" - "exec" "exit" "fc" "fg" "getopts" "hash" "help" - "history" "jobs" "kill" "let" "logout" "mapfile" - "popd" "printf" "pushd" "pwd" "read" "readarray" - "return" "set" "shift" "shopt" "source" "suspend" - "test" "time" "times" "trap" "type" "typeset" - "ulimit" "umask" "unalias" "wait")) - -(command - argument: [ - (word) @parameter - (concatenation (word) @parameter) - ]) - -((word) @number - (#lua-match? @number "^[0-9]+$")) - -(file_redirect - descriptor: (file_descriptor) @operator - destination: (word) @parameter) - -(expansion - [ "${" "}" ] @punctuation.bracket) - -(variable_name) @variable - -((variable_name) @constant - (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) - -(case_item - value: (word) @parameter) - -(regex) @string.regex - -((program . (comment) @preproc) - (#lua-match? @preproc "^#!/")) + (string) + (raw_string) + (heredoc_body) + (heredoc_start) +] @string + +(command_name) @function + +(variable_name) @property + +[ + "case" + "do" + "done" + "elif" + "else" + "esac" + "export" + "fi" + "for" + "function" + "if" + "in" + "select" + "then" + "unset" + "until" + "while" +] @keyword + +(comment) @comment + +(function_definition name: (word) @function) + +(file_descriptor) @number + +[ + (command_substitution) + (process_substitution) + (expansion) +]@embedded + +[ + "$" + "&&" + ">" + ">>" + "<" + "|" +] @operator + +( + (command (_) @constant) + (#match? @constant "^-") +) diff --git a/src/textual/tree-sitter/highlights/go.scm b/src/textual/tree-sitter/highlights/go.scm new file mode 100644 index 0000000000..7e1d625272 --- /dev/null +++ b/src/textual/tree-sitter/highlights/go.scm @@ -0,0 +1,123 @@ +; Function calls + +(call_expression + function: (identifier) @function.builtin + (.match? @function.builtin "^(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)$")) + +(call_expression + function: (identifier) @function) + +(call_expression + function: (selector_expression + field: (field_identifier) @function.method)) + +; Function definitions + +(function_declaration + name: (identifier) @function) + +(method_declaration + name: (field_identifier) @function.method) + +; Identifiers + +(type_identifier) @type +(field_identifier) @property +(identifier) @variable + +; Operators + +[ + "--" + "-" + "-=" + ":=" + "!" + "!=" + "..." + "*" + "*" + "*=" + "/" + "/=" + "&" + "&&" + "&=" + "%" + "%=" + "^" + "^=" + "+" + "++" + "+=" + "<-" + "<" + "<<" + "<<=" + "<=" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "|" + "|=" + "||" + "~" +] @operator + +; Keywords + +[ + "break" + "case" + "chan" + "const" + "continue" + "default" + "defer" + "else" + "fallthrough" + "for" + "func" + "go" + "goto" + "if" + "import" + "interface" + "map" + "package" + "range" + "return" + "select" + "struct" + "switch" + "type" + "var" +] @keyword + +; Literals + +[ + (interpreted_string_literal) + (raw_string_literal) + (rune_literal) +] @string + +(escape_sequence) @escape + +[ + (int_literal) + (float_literal) + (imaginary_literal) +] @number + +[ + (true) + (false) + (nil) + (iota) +] @constant.builtin + +(comment) @comment diff --git a/src/textual/tree-sitter/highlights/java.scm b/src/textual/tree-sitter/highlights/java.scm new file mode 100644 index 0000000000..301245b09a --- /dev/null +++ b/src/textual/tree-sitter/highlights/java.scm @@ -0,0 +1,142 @@ +; Methods + +(method_declaration + name: (identifier) @function.method) +(method_invocation + name: (identifier) @function.method) +(super) @function.builtin + +; Annotations + +(annotation + name: (identifier) @attribute) +(marker_annotation + name: (identifier) @attribute) + +"@" @operator + +; Types + +(type_identifier) @type + +(interface_declaration + name: (identifier) @type) +(class_declaration + name: (identifier) @type) +(enum_declaration + name: (identifier) @type) + +((field_access + object: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_identifier + scope: (identifier) @type) + (#match? @type "^[A-Z]")) +((method_invocation + object: (identifier) @type) + (#match? @type "^[A-Z]")) +((method_reference + . (identifier) @type) + (#match? @type "^[A-Z]")) + +(constructor_declaration + name: (identifier) @type) + +[ + (boolean_type) + (integral_type) + (floating_point_type) + (floating_point_type) + (void_type) +] @type.builtin + +; Variables + +((identifier) @constant + (#match? @constant "^_*[A-Z][A-Z\\d_]+$")) + +(identifier) @variable + +(this) @variable.builtin + +; Literals + +[ + (hex_integer_literal) + (decimal_integer_literal) + (octal_integer_literal) + (decimal_floating_point_literal) + (hex_floating_point_literal) +] @number + +[ + (character_literal) + (string_literal) +] @string +(escape_sequence) @string.escape + +[ + (true) + (false) + (null_literal) +] @constant.builtin + +[ + (line_comment) + (block_comment) +] @comment + +; Keywords + +[ + "abstract" + "assert" + "break" + "case" + "catch" + "class" + "continue" + "default" + "do" + "else" + "enum" + "exports" + "extends" + "final" + "finally" + "for" + "if" + "implements" + "import" + "instanceof" + "interface" + "module" + "native" + "new" + "non-sealed" + "open" + "opens" + "package" + "private" + "protected" + "provides" + "public" + "requires" + "record" + "return" + "sealed" + "static" + "strictfp" + "switch" + "synchronized" + "throw" + "throws" + "to" + "transient" + "transitive" + "try" + "uses" + "volatile" + "while" + "with" +] @keyword diff --git a/src/textual/tree-sitter/highlights/javascript.scm b/src/textual/tree-sitter/highlights/javascript.scm new file mode 100644 index 0000000000..613a49a86f --- /dev/null +++ b/src/textual/tree-sitter/highlights/javascript.scm @@ -0,0 +1,205 @@ +; Special identifiers +;-------------------- + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^[A-Z_][A-Z\\d_]+$")) + + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +((identifier) @variable.builtin + (#match? @variable.builtin "^(arguments|module|console|window|document)$") + (#is-not? local)) + +((identifier) @function.builtin + (#eq? @function.builtin "require") + (#is-not? local)) + +; Function and method definitions +;-------------------------------- + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Function and method calls +;-------------------------- + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Variables +;---------- + +(identifier) @variable + +; Properties +;----------- + +(property_identifier) @property + +; Literals +;--------- + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens +;------- + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + ";" + (optional_chain) + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword diff --git a/src/textual/tree-sitter/highlights/kotlin.scm b/src/textual/tree-sitter/highlights/kotlin.scm new file mode 100644 index 0000000000..d2e15a68ee --- /dev/null +++ b/src/textual/tree-sitter/highlights/kotlin.scm @@ -0,0 +1,380 @@ +;; Based on the nvim-treesitter highlighting, which is under the Apache license. +;; See https://github.com/nvim-treesitter/nvim-treesitter/blob/f8ab59861eed4a1c168505e3433462ed800f2bae/queries/kotlin/highlights.scm +;; +;; The only difference in this file is that queries using #lua-match? +;; have been removed. + +;;; Identifiers + +(simple_identifier) @variable + +; `it` keyword inside lambdas +; FIXME: This will highlight the keyword outside of lambdas since tree-sitter +; does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "it")) + +; `field` keyword inside property getter/setter +; FIXME: This will highlight the keyword outside of getters and setters +; since tree-sitter does not allow us to check for arbitrary nestation +((simple_identifier) @variable.builtin +(#eq? @variable.builtin "field")) + +; `this` this keyword inside classes +(this_expression) @variable.builtin + +; `super` keyword inside classes +(super_expression) @variable.builtin + +(class_parameter + (simple_identifier) @property) + +(class_body + (property_declaration + (variable_declaration + (simple_identifier) @property))) + +; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties +(_ + (navigation_suffix + (simple_identifier) @property)) + +(enum_entry + (simple_identifier) @constant) + +(type_identifier) @type + +((type_identifier) @type.builtin + (#any-of? @type.builtin + "Byte" + "Short" + "Int" + "Long" + "UByte" + "UShort" + "UInt" + "ULong" + "Float" + "Double" + "Boolean" + "Char" + "String" + "Array" + "ByteArray" + "ShortArray" + "IntArray" + "LongArray" + "UByteArray" + "UShortArray" + "UIntArray" + "ULongArray" + "FloatArray" + "DoubleArray" + "BooleanArray" + "CharArray" + "Map" + "Set" + "List" + "EmptyMap" + "EmptySet" + "EmptyList" + "MutableMap" + "MutableSet" + "MutableList" +)) + +(package_header + . (identifier)) @namespace + +(import_header + "import" @include) + + +; TODO: Seperate labeled returns/breaks/continue/super/this +; Must be implemented in the parser first +(label) @label + +;;; Function definitions + +(function_declaration + . (simple_identifier) @function) + +(getter + ("get") @function.builtin) +(setter + ("set") @function.builtin) + +(primary_constructor) @constructor +(secondary_constructor + ("constructor") @constructor) + +(constructor_invocation + (user_type + (type_identifier) @constructor)) + +(anonymous_initializer + ("init") @constructor) + +(parameter + (simple_identifier) @parameter) + +(parameter_with_optional_type + (simple_identifier) @parameter) + +; lambda parameters +(lambda_literal + (lambda_parameters + (variable_declaration + (simple_identifier) @parameter))) + +;;; Function calls + +; function() +(call_expression + . (simple_identifier) @function) + +; object.function() or object.property.function() +(call_expression + (navigation_expression + (navigation_suffix + (simple_identifier) @function) . )) + +(call_expression + . (simple_identifier) @function.builtin + (#any-of? @function.builtin + "arrayOf" + "arrayOfNulls" + "byteArrayOf" + "shortArrayOf" + "intArrayOf" + "longArrayOf" + "ubyteArrayOf" + "ushortArrayOf" + "uintArrayOf" + "ulongArrayOf" + "floatArrayOf" + "doubleArrayOf" + "booleanArrayOf" + "charArrayOf" + "emptyArray" + "mapOf" + "setOf" + "listOf" + "emptyMap" + "emptySet" + "emptyList" + "mutableMapOf" + "mutableSetOf" + "mutableListOf" + "print" + "println" + "error" + "TODO" + "run" + "runCatching" + "repeat" + "lazy" + "lazyOf" + "enumValues" + "enumValueOf" + "assert" + "check" + "checkNotNull" + "require" + "requireNotNull" + "with" + "suspend" + "synchronized" +)) + +;;; Literals + +[ + (line_comment) + (multiline_comment) + (shebang_line) +] @comment + +(real_literal) @float +[ + (integer_literal) + (long_literal) + (hex_literal) + (bin_literal) + (unsigned_literal) +] @number + +[ + "null" ; should be highlighted the same as booleans + (boolean_literal) +] @boolean + +(character_literal) @character + +(string_literal) @string + +(character_escape_seq) @string.escape + +; There are 3 ways to define a regex +; - "[abc]?".toRegex() +(call_expression + (navigation_expression + ((string_literal) @string.regex) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "toRegex"))))) + +; - Regex("[abc]?") +(call_expression + ((simple_identifier) @_function + (#eq? @_function "Regex")) + (call_suffix + (value_arguments + (value_argument + (string_literal) @string.regex)))) + +; - Regex.fromLiteral("[abc]?") +(call_expression + (navigation_expression + ((simple_identifier) @_class + (#eq? @_class "Regex")) + (navigation_suffix + ((simple_identifier) @_function + (#eq? @_function "fromLiteral")))) + (call_suffix + (value_arguments + (value_argument + (string_literal) @string.regex)))) + +;;; Keywords + +(type_alias "typealias" @keyword) +[ + (class_modifier) + (member_modifier) + (function_modifier) + (property_modifier) + (platform_modifier) + (variance_modifier) + (parameter_modifier) + (visibility_modifier) + (reification_modifier) + (inheritance_modifier) +]@keyword + +[ + "val" + "var" + "enum" + "class" + "object" + "interface" +; "typeof" ; NOTE: It is reserved for future use +] @keyword + +("fun") @keyword.function + +(jump_expression) @keyword.return + +[ + "if" + "else" + "when" +] @conditional + +[ + "for" + "do" + "while" +] @repeat + +[ + "try" + "catch" + "throw" + "finally" +] @exception + + +(annotation + "@" @attribute (use_site_target)? @attribute) +(annotation + (user_type + (type_identifier) @attribute)) +(annotation + (constructor_invocation + (user_type + (type_identifier) @attribute))) + +(file_annotation + "@" @attribute "file" @attribute ":" @attribute) +(file_annotation + (user_type + (type_identifier) @attribute)) +(file_annotation + (constructor_invocation + (user_type + (type_identifier) @attribute))) + +;;; Operators & Punctuation + +[ + "!" + "!=" + "!==" + "=" + "==" + "===" + ">" + ">=" + "<" + "<=" + "||" + "&&" + "+" + "++" + "+=" + "-" + "--" + "-=" + "*" + "*=" + "/" + "/=" + "%" + "%=" + "?." + "?:" + "!!" + "is" + "!is" + "in" + "!in" + "as" + "as?" + ".." + "->" +] @operator + +[ + "(" ")" + "[" "]" + "{" "}" +] @punctuation.bracket + +[ + "." + "," + ";" + ":" + "::" +] @punctuation.delimiter + +; NOTE: `interpolated_identifier`s can be highlighted in any way +(string_literal + "$" @punctuation.special + (interpolated_identifier) @none) +(string_literal + "${" @punctuation.special + (interpolated_expression) @none + "}" @punctuation.special) diff --git a/src/textual/tree-sitter/highlights/python.scm b/src/textual/tree-sitter/highlights/python.scm index 37aceef1fd..6f844b6135 100644 --- a/src/textual/tree-sitter/highlights/python.scm +++ b/src/textual/tree-sitter/highlights/python.scm @@ -15,20 +15,6 @@ ((identifier) @constant (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) -((identifier) @constant.builtin - (#lua-match? @constant.builtin "^__[a-zA-Z0-9_]*__$")) - -((identifier) @constant.builtin - (#any-of? @constant.builtin - ;; https://docs.python.org/3/library/constants.html - "NotImplemented" - "Ellipsis" - "quit" - "exit" - "copyright" - "credits" - "license")) - ((attribute attribute: (identifier) @field) (#match? @field "^([A-Z])@!.*$")) diff --git a/src/textual/tree-sitter/highlights/regex.scm b/src/textual/tree-sitter/highlights/regex.scm index 7c671c2c04..8b653465b4 100644 --- a/src/textual/tree-sitter/highlights/regex.scm +++ b/src/textual/tree-sitter/highlights/regex.scm @@ -1,34 +1,50 @@ -;; Forked from tree-sitter-regex -;; The MIT License (MIT) Copyright (c) 2014 Max Brunsfeld [ - "(" - ")" - "(?" - "(?:" - "(?<" - ">" - "[" - "]" - "{" - "}" -] @regex.punctuation.bracket + "(" + ")" + "(?" + "(?:" + "(?<" + ">" + "[" + "]" + "{" + "}" +] @punctuation.bracket (group_name) @property -;; These are escaped special characters that lost their special meaning -;; -> no special highlighting -(identity_escape) @string.regex - -(class_character) @constant +[ + (identity_escape) + (control_letter_escape) + (character_class_escape) + (control_escape) + (start_assertion) + (end_assertion) + (boundary_assertion) + (non_boundary_assertion) +] @escape [ - (control_letter_escape) - (character_class_escape) - (control_escape) - (start_assertion) - (end_assertion) - (boundary_assertion) - (non_boundary_assertion) -] @string.escape + "*" + "+" + "?" + "|" + "=" + "!" +] @operator + +(count_quantifier + [ + (decimal_digits) @number + "," @punctuation.delimiter + ]) + +(character_class + [ + "^" @operator + (class_range "-" @operator) + ]) + +(class_character) @constant.character -[ "*" "+" "?" "|" "=" "!" ] @regex.operator +(pattern_character) @string diff --git a/src/textual/tree-sitter/highlights/rust.scm b/src/textual/tree-sitter/highlights/rust.scm new file mode 100644 index 0000000000..c1556847b3 --- /dev/null +++ b/src/textual/tree-sitter/highlights/rust.scm @@ -0,0 +1,155 @@ +; Identifier conventions + +; Assume all-caps names are constants +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]+$'")) + +; Assume that uppercase names in paths are types +((scoped_identifier + path: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_identifier + path: (scoped_identifier + name: (identifier) @type)) + (#match? @type "^[A-Z]")) +((scoped_type_identifier + path: (identifier) @type) + (#match? @type "^[A-Z]")) +((scoped_type_identifier + path: (scoped_identifier + name: (identifier) @type)) + (#match? @type "^[A-Z]")) + +; Assume other uppercase names are enum constructors +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +; Assume all qualified names in struct patterns are enum constructors. (They're +; either that, or struct names; highlighting both as constructors seems to be +; the less glaring choice of error, visually.) +(struct_pattern + type: (scoped_type_identifier + name: (type_identifier) @constructor)) + +; Function calls + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function.method)) +(call_expression + function: (scoped_identifier + "::" + name: (identifier) @function)) + +(generic_function + function: (identifier) @function) +(generic_function + function: (scoped_identifier + name: (identifier) @function)) +(generic_function + function: (field_expression + field: (field_identifier) @function.method)) + +(macro_invocation + macro: (identifier) @function.macro + "!" @function.macro) + +; Function definitions + +(function_item (identifier) @function) +(function_signature_item (identifier) @function) + +; Other identifiers + +(type_identifier) @type +(primitive_type) @type.builtin +(field_identifier) @property + +(line_comment) @comment +(block_comment) @comment + +"(" @punctuation.bracket +")" @punctuation.bracket +"[" @punctuation.bracket +"]" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) +(type_parameters + "<" @punctuation.bracket + ">" @punctuation.bracket) + +"::" @punctuation.delimiter +":" @punctuation.delimiter +"." @punctuation.delimiter +"," @punctuation.delimiter +";" @punctuation.delimiter + +(parameter (identifier) @variable.parameter) + +(lifetime (identifier) @label) + +"as" @keyword +"async" @keyword +"await" @keyword +"break" @keyword +"const" @keyword +"continue" @keyword +"default" @keyword +"dyn" @keyword +"else" @keyword +"enum" @keyword +"extern" @keyword +"fn" @keyword +"for" @keyword +"if" @keyword +"impl" @keyword +"in" @keyword +"let" @keyword +"loop" @keyword +"macro_rules!" @keyword +"match" @keyword +"mod" @keyword +"move" @keyword +"pub" @keyword +"ref" @keyword +"return" @keyword +"static" @keyword +"struct" @keyword +"trait" @keyword +"type" @keyword +"union" @keyword +"unsafe" @keyword +"use" @keyword +"where" @keyword +"while" @keyword +(crate) @keyword +(mutable_specifier) @keyword +(use_list (self) @keyword) +(scoped_use_list (self) @keyword) +(scoped_identifier (self) @keyword) +(super) @keyword + +(self) @variable.builtin + +(char_literal) @string +(string_literal) @string +(raw_string_literal) @string + +(boolean_literal) @constant.builtin +(integer_literal) @constant.builtin +(float_literal) @constant.builtin + +(escape_sequence) @escape + +(attribute_item) @attribute +(inner_attribute_item) @attribute + +"*" @operator +"&" @operator +"'" @operator diff --git a/src/textual/tree-sitter/highlights/toml.scm b/src/textual/tree-sitter/highlights/toml.scm index 9228d28072..26e3982fef 100644 --- a/src/textual/tree-sitter/highlights/toml.scm +++ b/src/textual/tree-sitter/highlights/toml.scm @@ -9,7 +9,7 @@ ;--------- (boolean) @boolean -(comment) @comment @spell +(comment) @comment (string) @string (integer) @number (float) @float diff --git a/src/textual/widget.py b/src/textual/widget.py index 4bfaabdee0..e334c18ecf 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1792,7 +1792,7 @@ def animate( attribute: Name of the attribute to animate. value: The value to animate to. final_value: The final value of the animation. Defaults to `value` if not set. - duration: The duration of the animate. + duration: The duration (in seconds) of the animation. speed: The speed of the animation. delay: A delay (in seconds) before the animation starts. easing: An easing method. @@ -3064,6 +3064,8 @@ def get_pseudo_classes(self) -> Iterable[str]: yield "focus-within" break node = node._parent + if self.app.is_inline: + yield "inline" def get_pseudo_class_state(self) -> PseudoClasses: """Get an object describing whether each pseudo class is present on this object or not. @@ -3791,7 +3793,7 @@ def notify( message: The message for the notification. title: The title for the notification. severity: The severity of the notification. - timeout: The timeout for the notification. + timeout: The timeout (in seconds) for the notification. See [`App.notify`][textual.app.App.notify] for the full documentation for this method. diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 110646e109..532076c9f8 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, cast import rich.repr +from rich.cells import cell_len from rich.console import ConsoleRenderable, RenderableType from rich.text import Text, TextType from typing_extensions import Literal, Self @@ -15,6 +16,7 @@ from ..binding import Binding from ..css._error_tools import friendly_list +from ..geometry import Size from ..message import Message from ..pad import HorizontalPad from ..reactive import reactive @@ -203,6 +205,13 @@ def __init__( self.active_effect_duration = 0.3 """Amount of time in seconds the button 'press' animation lasts.""" + def get_content_width(self, container: Size, viewport: Size) -> int: + try: + return max([cell_len(line) for line in self.label.plain.splitlines()]) + 2 + except ValueError: + # Empty string label + return 2 + def __rich_repr__(self) -> rich.repr.Result: yield from super().__rich_repr__() yield "variant", self.variant, "default" @@ -227,7 +236,7 @@ def validate_label(self, label: TextType) -> Text: def render(self) -> RenderResult: assert isinstance(self.label, Text) label = self.label.copy() - label.stylize(self.rich_style) + label.stylize_before(self.rich_style) return HorizontalPad( label, 1, diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py index 4ad2023aad..37231b21aa 100644 --- a/src/textual/widgets/_text_area.py +++ b/src/textual/widgets/_text_area.py @@ -1292,27 +1292,27 @@ def _undo_batch(self, edits: Sequence[Edit]) -> None: return old_gutter_width = self.gutter_width - minimum_from = edits[-1].top - maximum_old_end = (0, 0) - maximum_new_end = (0, 0) + minimum_top = edits[-1].top + maximum_old_bottom = (0, 0) + maximum_new_bottom = (0, 0) for edit in reversed(edits): edit.undo(self) end_location = ( edit._edit_result.end_location if edit._edit_result else (0, 0) ) - if edit.from_location < minimum_from: - minimum_from = edit.from_location - if end_location > maximum_old_end: - maximum_old_end = end_location - if edit.to_location > maximum_new_end: - maximum_new_end = edit.bottom + if edit.top < minimum_top: + minimum_top = edit.top + if end_location > maximum_old_bottom: + maximum_old_bottom = end_location + if edit.bottom > maximum_new_bottom: + maximum_new_bottom = edit.bottom new_gutter_width = self.gutter_width if old_gutter_width != new_gutter_width: self.wrapped_document.wrap(self.wrap_width, self.indent_width) else: self.wrapped_document.wrap_range( - minimum_from, maximum_old_end, maximum_new_end + minimum_top, maximum_old_bottom, maximum_new_bottom ) self._refresh_size() @@ -1338,29 +1338,29 @@ def _redo_batch(self, edits: Sequence[Edit]) -> None: return old_gutter_width = self.gutter_width - minimum_from = edits[0].from_location - maximum_old_end = (0, 0) - maximum_new_end = (0, 0) + minimum_top = edits[0].top + maximum_old_bottom = (0, 0) + maximum_new_bottom = (0, 0) for edit in edits: edit.do(self, record_selection=False) end_location = ( edit._edit_result.end_location if edit._edit_result else (0, 0) ) - if edit.from_location < minimum_from: - minimum_from = edit.from_location - if end_location > maximum_new_end: - maximum_new_end = end_location - if edit.to_location > maximum_old_end: - maximum_old_end = edit.to_location + if edit.top < minimum_top: + minimum_top = edit.top + if end_location > maximum_new_bottom: + maximum_new_bottom = end_location + if edit.bottom > maximum_old_bottom: + maximum_old_bottom = edit.bottom new_gutter_width = self.gutter_width if old_gutter_width != new_gutter_width: self.wrapped_document.wrap(self.wrap_width, self.indent_width) else: self.wrapped_document.wrap_range( - minimum_from, - maximum_old_end, - maximum_new_end, + minimum_top, + maximum_old_bottom, + maximum_new_bottom, ) self._refresh_size() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index c375135d8b..4051913429 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -2321,6 +2321,171 @@ ''' # --- +# name: test_button_with_console_markup + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ButtonsWithMarkupApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Focused Button + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Blurred Button + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Disabled Button + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_buttons_render ''' @@ -36564,6 +36729,486 @@ ''' # --- +# name: test_text_area_language_rendering[bash] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +   1  #!/bin/bash +   2   +   3  # Variables +   4  name="John" +   5  age=30  +   6  is_student=true  +   7   +   8  # Printing variables +   9  echo"Hello, $name! You are $age years old." +  10   +  11  # Conditional statements +  12  if [[ $age -ge 18 &&$is_student == true ]]; then +  13  echo"You are an adult student." +  14  elif [[ $age -ge 18 ]]; then +  15  echo"You are an adult." +  16  else +  17  echo"You are a minor." +  18  fi +  19   +  20  # Arrays +  21  numbers=(1 2 3 4 5)  +  22  echo"Numbers: ${numbers[@]}" +  23   +  24  # Loops +  25  for num in"${numbers[@]}"do +  26  echo"Number: $num" +  27  done +  28   +  29  # Functions +  30  greet() {  +  31    local name=$ +  32  echo"Hello, $name!" +  33   +  34  greet"Alice" +  35   +  36  # Command substitution +  37  current_date=$(date +%Y-%m-%d)  +  38  echo"Current date: $current_date" +  39   +  40  # File operations +  41  touch file.txt  +  42  echo"Some content"> file.txt  +  43  cat file.txt  +  44   +  45  # Conditionals with file checks +  46  if [[ -f file.txt ]]; then +  47  echo"file.txt exists." +  48  else +  49  echo"file.txt does not exist." +  50  fi +  51   +  52  # Case statement +  53  case$age in +  54    18)  +  55  echo"You are 18 years old." +  56      ;;  +  57    30)  +  58  echo"You are 30 years old." +  59      ;;  +  60    *)  +  61  echo"You are neither 18 nor 30 years old." +  62      ;;  +  63  esac +  64   +  65  # While loop +  66  counter=0  +  67  while [[ $counter -lt 5 ]]; do +  68  echo"Counter: $counter" +  69    ((counter++))  +  70  done +  71   +  72  # Until loop +  73  until [[ $counter -eq 0 ]]; do +  74  echo"Counter: $counter" +  75    ((counter--))  +  76  done +  77   +  78  # Heredoc +  79  cat << EOF +  80  This is a heredoc.  +  81  It allows you to write multiple lines of text.  +  82  EOF  +  83   +  84  # Redirection +  85  ls> file_list.txt  +  86  grep"file" file_list.txt > filtered_list.txt  +  87   +  88  # Pipes +  89  cat file_list.txt |wc -l  +  90   +  91  # Arithmetic operations +  92  result=$((10 + 5))  +  93  echo"Result: $result" +  94   +  95  # Exporting variables +  96  export DB_PASSWORD="secret" +  97   +  98  # Sourcing external files +  99  source config.sh  + 100   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- # name: test_text_area_language_rendering[css] ''' @@ -36917,6 +37562,363 @@ ''' # --- +# name: test_text_area_language_rendering[go] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  package main                                                             +  2   +  3  import (  +  4  "fmt" +  5  "math" +  6  "strings" +  7   +  8   +  9  const PI =3.14159 + 10   + 11  type Shape interface {  + 12      Area() float64  + 13   + 14   + 15  type Circle struct {  + 16      Radius float64  + 17   + 18   + 19  func (c Circle) Area() float64 {  + 20  return PI * c.Radius * c.Radius  + 21   + 22   + 23  funcmain() {  + 24  var name string ="John" + 25      age :=30 + 26      isStudent :=true + 27   + 28      fmt.Printf("Hello, %s! You are %d years old.", name, age)  + 29   + 30  if age >=18&& isStudent {  + 31          fmt.Println("You are an adult student." + 32      } elseif age >=18 {  + 33          fmt.Println("You are an adult." + 34      } else {  + 35          fmt.Println("You are a minor." + 36      }  + 37   + 38      numbers := []int{12345 + 39      sum :=0 + 40  for _, num :=range numbers {  + 41          sum += num  + 42      }  + 43      fmt.Printf("The sum is: %d", sum)  + 44   + 45      message :="Hello, World!" + 46      uppercaseMessage := strings.ToUpper(message)  + 47      fmt.Println(uppercaseMessage)  + 48   + 49      circle := Circle{Radius: 5 + 50      fmt.Printf("Circle area: %.2f", circle.Area())  + 51   + 52      result :=factorial(5 + 53      fmt.Printf("Factorial of 5: %d", result)  + 54   + 55  defer fmt.Println("Program finished." + 56   + 57      sqrt :=func(x float64) float64 {  + 58  return math.Sqrt(x)  + 59      }  + 60      fmt.Printf("Square root of 16: %.2f"sqrt(16))  + 61   + 62   + 63  funcfactorial(n int) int {  + 64  if n ==0 {  + 65  return1 + 66      }  + 67  return n *factorial(n-1 + 68   + 69   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- # name: test_text_area_language_rendering[html] ''' @@ -37225,6 +38227,898 @@ ''' # --- +# name: test_text_area_language_rendering[java] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +   1  import java.util.ArrayList;                                             +   2  import java.util.HashMap;  +   3  import java.util.List;  +   4  import java.util.Map;  +   5   +   6  // Classes and interfaces +   7  interface Shape {  +   8      double getArea();  +   9   +  10   +  11  class Rectangle implements Shape {  +  12  private double width;  +  13  private double height;  +  14   +  15  public Rectangle(double width, double height) {  +  16          this.width = width;  +  17          this.height = height;  +  18      }  +  19   +  20  @Override  +  21  public double getArea() {  +  22  return width * height;  +  23      }  +  24   +  25   +  26  // Enums +  27  enum DaysOfWeek {  +  28      MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY  +  29   +  30   +  31  publicclass Main {  +  32  // Constants +  33  privatestaticfinal double PI = 3.14159 +  34   +  35  // Methods +  36  publicstatic int sum(int a, int b) {  +  37  return a + b;  +  38      }  +  39   +  40  publicstatic void main(String[] args) {  +  41  // Variables +  42          String name = "John" +  43          int age = 30 +  44          boolean isStudent = true +  45   +  46  // Printing variables +  47          System.out.println("Hello, " + name + "! You are " + age + " ye +  48   +  49  // Conditional statements +  50  if (age >= 18 && isStudent) {  +  51              System.out.println("You are an adult student.");  +  52          } elseif (age >= 18) {  +  53              System.out.println("You are an adult.");  +  54          } else {  +  55              System.out.println("You are a minor.");  +  56          }  +  57   +  58  // Arrays +  59          int[] numbers = {12345};  +  60          System.out.println("Numbers: " + Arrays.toString(numbers));  +  61   +  62  // Lists +  63          List<String> fruits = new ArrayList<>();  +  64          fruits.add("apple");  +  65          fruits.add("banana");  +  66          fruits.add("orange");  +  67          System.out.println("Fruits: " + fruits);  +  68   +  69  // Loops +  70  for (int num : numbers) {  +  71              System.out.println("Number: " + num);  +  72          }  +  73   +  74  // Hash maps +  75          Map<String, Integer> scores = new HashMap<>();  +  76          scores.put("Alice"100);  +  77          scores.put("Bob"80);  +  78          System.out.println("Alice's score: " + scores.get("Alice"));  +  79   +  80  // Exception handling +  81  try {  +  82              int result = 10 / 0 +  83          } catch (ArithmeticException e) {  +  84              System.out.println("Error: " + e.getMessage());  +  85          }  +  86   +  87  // Instantiating objects +  88          Rectangle rect = new Rectangle(1020);  +  89          System.out.println("Rectangle area: " + rect.getArea());  +  90   +  91  // Enums +  92          DaysOfWeek today = DaysOfWeek.MONDAY;  +  93          System.out.println("Today is " + today);  +  94   +  95  // Calling methods +  96          int sum = sum(510);  +  97          System.out.println("Sum: " + sum);  +  98   +  99  // Ternary operator + 100          String message = age >= 18 ? "You are an adult." : "You are a m + 101          System.out.println(message);  + 102      }  + 103   + 104   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- +# name: test_text_area_language_rendering[javascript] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  // Variable declarations +  2  const name ="John" +  3  let age =30 +  4  var isStudent =true +  5   +  6  // Template literals +  7  console.log(`Hello, ${name}! You are ${age} years old.`);  +  8   +  9  // Conditional statements + 10  if (age >=18&& isStudent) {  + 11    console.log("You are an adult student.");  + 12  elseif (age >=18) {  + 13    console.log("You are an adult.");  + 14  else {  + 15    console.log("You are a minor.");  + 16   + 17   + 18  // Arrays and array methods + 19  const numbers = [12345];  + 20  const doubledNumbers = numbers.map((num) => num *2);  + 21  console.log("Doubled numbers:", doubledNumbers);  + 22   + 23  // Objects + 24  const person = {  + 25    firstName: "John" + 26    lastName: "Doe" + 27    getFullName() {  + 28  return`${this.firstName} ${this.lastName}` + 29    },  + 30  };  + 31  console.log("Full name:", person.getFullName());  + 32   + 33  // Classes + 34  class Rectangle {  + 35    constructor(width, height) {  + 36      this.width = width;  + 37      this.height = height;  + 38    }  + 39   + 40    getArea() {  + 41  return this.width * this.height;  + 42    }  + 43   + 44  const rectangle =new Rectangle(53);  + 45  console.log("Rectangle area:", rectangle.getArea());  + 46   + 47  // Async/Await and Promises + 48  asyncfunctionfetchData() {  + 49  try {  + 50  const response =awaitfetch("https://api.example.com/data");  + 51  const data =await response.json();  + 52      console.log("Fetched data:", data);  + 53    } catch (error) {  + 54      console.error("Error:", error);  + 55    }  + 56   + 57  fetchData();  + 58   + 59  // Arrow functions + 60  constgreet= (name) => {  + 61    console.log(`Hello, ${name}!`);  + 62  };  + 63  greet("Alice");  + 64   + 65  // Destructuring assignment + 66  const [a, b, ...rest] = [12345];  + 67  console.log(a, b, rest);  + 68   + 69  // Spread operator + 70  const arr1 = [123];  + 71  const arr2 = [456];  + 72  const combinedArr = [...arr1, ...arr2];  + 73  console.log("Combined array:", combinedArr);  + 74   + 75  // Ternary operator + 76  const message = age >=18 ? "You are an adult." : "You are a minor." + 77  console.log(message);  + 78   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- # name: test_text_area_language_rendering[json] ''' @@ -37430,6 +39324,472 @@ ''' # --- +# name: test_text_area_language_rendering[kotlin] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  // Variables +  2  val name ="John" +  3  var age =30 +  4  var isStudent =true +  5   +  6  // Printing variables +  7  println("Hello, $name! You are $age years old." +  8   +  9  // Conditional statements + 10  when {  + 11      age >=18&& isStudent ->println("You are an adult student." + 12      age >=18->println("You are an adult." + 13  else->println("You are a minor." + 14   + 15   + 16  // Arrays + 17  val numbers =arrayOf(12345 + 18  println("Numbers: ${numbers.contentToString()}" + 19   + 20  // Lists + 21  val fruits =listOf("apple""banana""orange" + 22  println("Fruits: $fruits" + 23   + 24  // Loops + 25  for (num in numbers) {  + 26  println("Number: $num" + 27   + 28   + 29  // Functions + 30  fungreet(name: String) {  + 31  println("Hello, $name!" + 32   + 33  greet("Alice" + 34   + 35  // Lambda functions + 36  val square = { num: Int -> num * num }  + 37  println("Square of 5: ${square(5)}" + 38   + 39  // Extension functions + 40  fun String.reverse(): String {  + 41  return this.reversed() + 42   + 43  val reversed ="Hello".reverse()  + 44  println("Reversed: $reversed" + 45   + 46  // Data classes + 47  dataclass Person(val name: String, val age: Int)  + 48  val person =Person("John"30 + 49  println("Person: $person" + 50   + 51  // Null safety + 52  var nullable: String? =null + 53  println("Length: ${nullable?.length}" + 54   + 55  // Elvis operator + 56  val length = nullable?.length ?:0 + 57  println("Length (Elvis): $length" + 58   + 59  // Smart casts + 60  funprintLength(obj: Any) {  + 61  if (obj is String) {  + 62  println("Length: ${obj.length}" + 63      }  + 64   + 65  printLength("Hello" + 66   + 67  // Object expressions + 68  val comparator =object : Comparator<Int> {  + 69  overridefun compare(a: Int, b: Int): Int {  + 70  return a - b + 71      }  + 72   + 73  val sortedNumbers = numbers.sortedWith(comparator)  + 74  println("Sorted numbers: ${sortedNumbers.contentToString()}" + 75   + 76  // Companion objects + 77  class MyClass {  + 78      companion object {  + 79  funcreate(): MyClass {  + 80  return MyClass() + 81          }  + 82      }  + 83   + 84  val obj = MyClass.create()  + 85   + 86  // Sealed classes + 87  sealedclass Result {  + 88  dataclass Success(val data: String) : Result()  + 89  dataclass Error(val message: String) : Result()  + 90   + 91  val result: Result = Result.Success("Data" + 92  when (result) {  + 93  is Result.Success ->println("Success: ${result.data}" + 94  is Result.Error ->println("Error: ${result.message}" + 95   + 96   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- # name: test_text_area_language_rendering[markdown] ''' @@ -38206,155 +40566,659 @@ font-weight: 700; } - .terminal-250155606-matrix { + .terminal-3279709-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-250155606-title { + .terminal-3279709-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-250155606-r1 { fill: #1e1e1e } - .terminal-250155606-r2 { fill: #0178d4 } - .terminal-250155606-r3 { fill: #c5c8c6 } - .terminal-250155606-r4 { fill: #c2c2bf } - .terminal-250155606-r5 { fill: #272822 } - .terminal-250155606-r6 { fill: #f8f8f2 } - .terminal-250155606-r7 { fill: #90908a } - .terminal-250155606-r8 { fill: #f92672 } - .terminal-250155606-r9 { fill: #23568b } + .terminal-3279709-r1 { fill: #1e1e1e } + .terminal-3279709-r2 { fill: #0178d4 } + .terminal-3279709-r3 { fill: #c5c8c6 } + .terminal-3279709-r4 { fill: #c2c2bf } + .terminal-3279709-r5 { fill: #272822 } + .terminal-3279709-r6 { fill: #e6db74 } + .terminal-3279709-r7 { fill: #f8f8f2 } + .terminal-3279709-r8 { fill: #90908a } + .terminal-3279709-r9 { fill: #f92672 } + .terminal-3279709-r10 { fill: #ae81ff } + .terminal-3279709-r11 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot + TextAreaSnapshot - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  1  ^abc            # Matches any string that starts with "abc"              -  2  abc$            # Matches any string that ends with "abc"  -  3  ^abc$           # Matches the string "abc" and nothing else  -  4  a.b             # Matches any string containing "a", any character, then -  5  a[.]b           # Matches the string "a.b"  -  6  a|b             # Matches either "a" or "b"  -  7  a{2}            # Matches "aa"  -  8  a{2,}           # Matches two or more consecutive "a" characters  -  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters  - 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a") - 11  a*              # Matches zero or more consecutive "a" characters  - 12  a+              # Matches one or more consecutive "a" characters  - 13  \d              # Matches any digit (equivalent to [0-9]) - 14  \D              # Matches any non-digit  - 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_] - 16  \W              # Matches any non-word character  - 17  \s              # Matches any whitespace character (spaces, tabs, line b - 18  \S              # Matches any non-whitespace character  - 19  (?i)abc         # Case-insensitive match for "abc"  - 20  (?:a|b)         # Non-capturing group for either "a" or "b"  - 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by " - 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded  - 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b - 24  a(?!b)          # Negative lookahead: matches "a" that is not followed b - 25   - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  ^abc            # Matches any string that starts with "abc" +  2  abc$            # Matches any string that ends with "abc" +  3  ^abc$           # Matches the string "abc" and nothing else +  4  a.b             # Matches any string containing "a", any character, then +  5  a[.]b           # Matches the string "a.b" +  6  a|b             # Matches either "a" or "b" +  7  a{2}            # Matches "aa" +  8  a{2,}           # Matches two or more consecutive "a" characters +  9  a{2,5}          # Matches between 2 and 5 consecutive "a" characters + 10  a?              # Matches "a" or nothing (0 or 1 occurrence of "a" + 11  a*              # Matches zero or more consecutive "a" characters + 12  a+              # Matches one or more consecutive "a" characters + 13  \d              # Matches any digit (equivalent to [0-9])  + 14  \D              # Matches any non-digit + 15  \w              # Matches any word character (equivalent to [a-zA-Z0-9_] + 16  \W              # Matches any non-word character + 17  \s              # Matches any whitespace character (spaces, tabs, line b + 18  \S              # Matches any non-whitespace character + 19  (?i)abc         # Case-insensitive match for "abc" + 20  (?:a|b)         # Non-capturing group for either "a" or "b" + 21  (?<=a)b         # Positive lookbehind: matches "b" that is preceded by " + 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded  + 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b + 24  a(?!b)          # Negative lookahead: matches "a" that is not followed b + 25   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + ''' +# --- +# name: test_text_area_language_rendering[rust] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +   1  use std::collections::HashMap;                                          +   2   +   3  // Constants +   4  const PI: f64 = 3.14159 +   5   +   6  // Structs +   7  struct Rectangle {  +   8      width: u32,  +   9      height: u32,  +  10   +  11   +  12  impl Rectangle {  +  13  fnarea(&self) -> u32 {  +  14          self.width * self.height  +  15      }  +  16   +  17   +  18  // Enums +  19  enum Result<T, E> {  +  20      Ok(T),  +  21      Err(E),  +  22   +  23   +  24  // Functions +  25  fngreet(name: &str) {  +  26      println!("Hello, {}!", name);  +  27   +  28   +  29  fnmain() {  +  30  // Variables +  31  let name = "John" +  32  letmut age = 30 +  33  let is_student = true +  34   +  35  // Printing variables +  36      println!("Hello, {}! You are {} years old.", name, age);  +  37   +  38  // Conditional statements +  39  if age >= 18 && is_student {  +  40          println!("You are an adult student.");  +  41      } elseif age >= 18 {  +  42          println!("You are an adult.");  +  43      } else {  +  44          println!("You are a minor.");  +  45      }  +  46   +  47  // Arrays +  48  let numbers = [12345];  +  49      println!("Numbers: {:?}", numbers);  +  50   +  51  // Vectors +  52  letmut fruits = vec!["apple""banana""orange"];  +  53      fruits.push("grape");  +  54      println!("Fruits: {:?}", fruits);  +  55   +  56  // Loops +  57  for num in&numbers {  +  58          println!("Number: {}", num);  +  59      }  +  60   +  61  // Pattern matching +  62  let result = Result::Ok(42);  +  63  match result {  +  64          Result::Ok(value) => println!("Value: {}", value),  +  65          Result::Err(error) => println!("Error: {:?}", error),  +  66      }  +  67   +  68  // Ownership and borrowing +  69  let s1 = String::from("hello");  +  70  let s2 = s1.clone();  +  71      println!("s1: {}, s2: {}", s1, s2);  +  72   +  73  // References +  74  let rect = Rectangle {  +  75          width: 10 +  76          height: 20 +  77      };  +  78      println!("Rectangle area: {}", rect.area());  +  79   +  80  // Hash maps +  81  letmut scores = HashMap::new();  +  82      scores.insert("Alice"100);  +  83      scores.insert("Bob"80);  +  84      println!("Alice's score: {}", scores["Alice"]);  +  85   +  86  // Closures +  87  let square = |num: i32| num * num;  +  88      println!("Square of 5: {}", square(5));  +  89   +  90  // Traits +  91  trait Printable {  +  92  fnprint(&self);  +  93      }  +  94   +  95  impl Printable for Rectangle {  +  96  fnprint(&self) {  +  97              println!("Rectangle: width={}, height={}", self.width, self +  98          }  +  99      }  + 100      rect.print();  + 101   + 102  // Modules + 103  greet("Alice");  + 104   + 105   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -38384,234 +41248,235 @@ font-weight: 700; } - .terminal-1168310614-matrix { + .terminal-276599044-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1168310614-title { + .terminal-276599044-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1168310614-r1 { fill: #1e1e1e } - .terminal-1168310614-r2 { fill: #0178d4 } - .terminal-1168310614-r3 { fill: #c5c8c6 } - .terminal-1168310614-r4 { fill: #c2c2bf } - .terminal-1168310614-r5 { fill: #272822 } - .terminal-1168310614-r6 { fill: #75715e } - .terminal-1168310614-r7 { fill: #f8f8f2 } - .terminal-1168310614-r8 { fill: #90908a } - .terminal-1168310614-r9 { fill: #f92672 } - .terminal-1168310614-r10 { fill: #ae81ff } - .terminal-1168310614-r11 { fill: #e6db74 } - .terminal-1168310614-r12 { fill: #23568b } + .terminal-276599044-r1 { fill: #1e1e1e } + .terminal-276599044-r2 { fill: #0178d4 } + .terminal-276599044-r3 { fill: #c5c8c6 } + .terminal-276599044-r4 { fill: #c2c2bf } + .terminal-276599044-r5 { fill: #272822 } + .terminal-276599044-r6 { fill: #75715e } + .terminal-276599044-r7 { fill: #f8f8f2 } + .terminal-276599044-r8 { fill: #90908a } + .terminal-276599044-r9 { fill: #f92672 } + .terminal-276599044-r10 { fill: #ae81ff } + .terminal-276599044-r11 { fill: #66d9ef;font-style: italic; } + .terminal-276599044-r12 { fill: #e6db74 } + .terminal-276599044-r13 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAreaSnapshot - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  1  -- This is a comment in SQL -  2   -  3  -- Create tables -  4  CREATETABLE Authors (  -  5      AuthorID INT PRIMARY KEY -  6      Name VARCHAR(255NOT NULL -  7      Country VARCHAR(50 -  8  );  -  9   - 10  CREATETABLE Books (  - 11      BookID INT PRIMARY KEY - 12      Title VARCHAR(255NOT NULL - 13      AuthorID INT,  - 14      PublishedDate DATE,  - 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)  - 16  );  - 17   - 18  -- Insert data - 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell' - 20   - 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1 - 22   - 23  -- Update data - 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK' - 25   - 26  -- Select data with JOIN - 27  SELECT Books.Title, Authors.Name   - 28  FROM Books   - 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;  - 30   - 31  -- Delete data (commented to preserve data for other examples) - 32  -- DELETE FROM Books WHERE BookID = 1; - 33   - 34  -- Alter table structure - 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;  - 36   - 37  -- Create index - 38  CREATEINDEX idx_author_name ON Authors(Name);  - 39   - 40  -- Drop index (commented to avoid actually dropping it) - 41  -- DROP INDEX idx_author_name ON Authors; - 42   - 43  -- End of script - 44   - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + TextAreaSnapshot + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  1  -- This is a comment in SQL +  2   +  3  -- Create tables +  4  CREATETABLE Authors (  +  5      AuthorID INT PRIMARY KEY +  6      Name VARCHAR(255NOT NULL +  7      Country VARCHAR(50 +  8  );  +  9   + 10  CREATETABLE Books (  + 11      BookID INT PRIMARY KEY + 12      Title VARCHAR(255NOT NULL + 13      AuthorID INT,  + 14      PublishedDate DATE,  + 15      FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID)  + 16  );  + 17   + 18  -- Insert data + 19  INSERTINTO Authors (AuthorID, Name, Country) VALUES (1'George Orwell' + 20   + 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1 + 22   + 23  -- Update data + 24  UPDATE Authors SET Country ='United Kingdom'WHERE Country ='UK' + 25   + 26  -- Select data with JOIN + 27  SELECT Books.Title, Authors.Name   + 28  FROM Books   + 29  JOIN Authors ON Books.AuthorID = Authors.AuthorID;  + 30   + 31  -- Delete data (commented to preserve data for other examples) + 32  -- DELETE FROM Books WHERE BookID = 1; + 33   + 34  -- Alter table structure + 35  ALTER TABLE Authors ADD COLUMN BirthDate DATE;  + 36   + 37  -- Create index + 38  CREATEINDEX idx_author_name ON Authors(Name);  + 39   + 40  -- Drop index (commented to avoid actually dropping it) + 41  -- DROP INDEX idx_author_name ON Authors; + 42   + 43  -- End of script + 44   + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -42788,6 +45653,163 @@ ''' # --- +# name: test_width_100 + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Width100PCentApp + + + + + + + + + + ─────────────────────────────────────────────────────────── + ───────────────────────────────────────────────────────── + I want to be 100% of my parent + ───────────────────────────────────────────────────────── + ───────────────────────────────────────────────────────── + I want my parent to be wide enough to wrap me and no more + ───────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + + ─────────────────────────────────────────────────────────── + + + + + ''' +# --- # name: test_zero_scrollbar_size ''' diff --git a/tests/snapshot_tests/language_snippets.py b/tests/snapshot_tests/language_snippets.py index 6b55775159..f0c6c77469 100644 --- a/tests/snapshot_tests/language_snippets.py +++ b/tests/snapshot_tests/language_snippets.py @@ -452,6 +452,570 @@ def say_hello(): a(?!b) # Negative lookahead: matches "a" that is not followed by "b" """ +GO = """\ +package main + +import ( + "fmt" + "math" + "strings" +) + +const PI = 3.14159 + +type Shape interface { + Area() float64 +} + +type Circle struct { + Radius float64 +} + +func (c Circle) Area() float64 { + return PI * c.Radius * c.Radius +} + +func main() { + var name string = "John" + age := 30 + isStudent := true + + fmt.Printf("Hello, %s! You are %d years old.", name, age) + + if age >= 18 && isStudent { + fmt.Println("You are an adult student.") + } else if age >= 18 { + fmt.Println("You are an adult.") + } else { + fmt.Println("You are a minor.") + } + + numbers := []int{1, 2, 3, 4, 5} + sum := 0 + for _, num := range numbers { + sum += num + } + fmt.Printf("The sum is: %d", sum) + + message := "Hello, World!" + uppercaseMessage := strings.ToUpper(message) + fmt.Println(uppercaseMessage) + + circle := Circle{Radius: 5} + fmt.Printf("Circle area: %.2f", circle.Area()) + + result := factorial(5) + fmt.Printf("Factorial of 5: %d", result) + + defer fmt.Println("Program finished.") + + sqrt := func(x float64) float64 { + return math.Sqrt(x) + } + fmt.Printf("Square root of 16: %.2f", sqrt(16)) +} + +func factorial(n int) int { + if n == 0 { + return 1 + } + return n * factorial(n-1) +} +""" + +JAVASCRIPT = """\ +// Variable declarations +const name = "John"; +let age = 30; +var isStudent = true; + +// Template literals +console.log(`Hello, ${name}! You are ${age} years old.`); + +// Conditional statements +if (age >= 18 && isStudent) { + console.log("You are an adult student."); +} else if (age >= 18) { + console.log("You are an adult."); +} else { + console.log("You are a minor."); +} + +// Arrays and array methods +const numbers = [1, 2, 3, 4, 5]; +const doubledNumbers = numbers.map((num) => num * 2); +console.log("Doubled numbers:", doubledNumbers); + +// Objects +const person = { + firstName: "John", + lastName: "Doe", + getFullName() { + return `${this.firstName} ${this.lastName}`; + }, +}; +console.log("Full name:", person.getFullName()); + +// Classes +class Rectangle { + constructor(width, height) { + this.width = width; + this.height = height; + } + + getArea() { + return this.width * this.height; + } +} +const rectangle = new Rectangle(5, 3); +console.log("Rectangle area:", rectangle.getArea()); + +// Async/Await and Promises +async function fetchData() { + try { + const response = await fetch("https://api.example.com/data"); + const data = await response.json(); + console.log("Fetched data:", data); + } catch (error) { + console.error("Error:", error); + } +} +fetchData(); + +// Arrow functions +const greet = (name) => { + console.log(`Hello, ${name}!`); +}; +greet("Alice"); + +// Destructuring assignment +const [a, b, ...rest] = [1, 2, 3, 4, 5]; +console.log(a, b, rest); + +// Spread operator +const arr1 = [1, 2, 3]; +const arr2 = [4, 5, 6]; +const combinedArr = [...arr1, ...arr2]; +console.log("Combined array:", combinedArr); + +// Ternary operator +const message = age >= 18 ? "You are an adult." : "You are a minor."; +console.log(message); +""" + +BASH = """\ +#!/bin/bash + +# Variables +name="John" +age=30 +is_student=true + +# Printing variables +echo "Hello, $name! You are $age years old." + +# Conditional statements +if [[ $age -ge 18 && $is_student == true ]]; then + echo "You are an adult student." +elif [[ $age -ge 18 ]]; then + echo "You are an adult." +else + echo "You are a minor." +fi + +# Arrays +numbers=(1 2 3 4 5) +echo "Numbers: ${numbers[@]}" + +# Loops +for num in "${numbers[@]}"; do + echo "Number: $num" +done + +# Functions +greet() { + local name=$1 + echo "Hello, $name!" +} +greet "Alice" + +# Command substitution +current_date=$(date +%Y-%m-%d) +echo "Current date: $current_date" + +# File operations +touch file.txt +echo "Some content" > file.txt +cat file.txt + +# Conditionals with file checks +if [[ -f file.txt ]]; then + echo "file.txt exists." +else + echo "file.txt does not exist." +fi + +# Case statement +case $age in + 18) + echo "You are 18 years old." + ;; + 30) + echo "You are 30 years old." + ;; + *) + echo "You are neither 18 nor 30 years old." + ;; +esac + +# While loop +counter=0 +while [[ $counter -lt 5 ]]; do + echo "Counter: $counter" + ((counter++)) +done + +# Until loop +until [[ $counter -eq 0 ]]; do + echo "Counter: $counter" + ((counter--)) +done + +# Heredoc +cat << EOF +This is a heredoc. +It allows you to write multiple lines of text. +EOF + +# Redirection +ls > file_list.txt +grep "file" file_list.txt > filtered_list.txt + +# Pipes +cat file_list.txt | wc -l + +# Arithmetic operations +result=$((10 + 5)) +echo "Result: $result" + +# Exporting variables +export DB_PASSWORD="secret" + +# Sourcing external files +source config.sh +""" + +KOTLIN = """\ +// Variables +val name = "John" +var age = 30 +var isStudent = true + +// Printing variables +println("Hello, $name! You are $age years old.") + +// Conditional statements +when { + age >= 18 && isStudent -> println("You are an adult student.") + age >= 18 -> println("You are an adult.") + else -> println("You are a minor.") +} + +// Arrays +val numbers = arrayOf(1, 2, 3, 4, 5) +println("Numbers: ${numbers.contentToString()}") + +// Lists +val fruits = listOf("apple", "banana", "orange") +println("Fruits: $fruits") + +// Loops +for (num in numbers) { + println("Number: $num") +} + +// Functions +fun greet(name: String) { + println("Hello, $name!") +} +greet("Alice") + +// Lambda functions +val square = { num: Int -> num * num } +println("Square of 5: ${square(5)}") + +// Extension functions +fun String.reverse(): String { + return this.reversed() +} +val reversed = "Hello".reverse() +println("Reversed: $reversed") + +// Data classes +data class Person(val name: String, val age: Int) +val person = Person("John", 30) +println("Person: $person") + +// Null safety +var nullable: String? = null +println("Length: ${nullable?.length}") + +// Elvis operator +val length = nullable?.length ?: 0 +println("Length (Elvis): $length") + +// Smart casts +fun printLength(obj: Any) { + if (obj is String) { + println("Length: ${obj.length}") + } +} +printLength("Hello") + +// Object expressions +val comparator = object : Comparator { + override fun compare(a: Int, b: Int): Int { + return a - b + } +} +val sortedNumbers = numbers.sortedWith(comparator) +println("Sorted numbers: ${sortedNumbers.contentToString()}") + +// Companion objects +class MyClass { + companion object { + fun create(): MyClass { + return MyClass() + } + } +} +val obj = MyClass.create() + +// Sealed classes +sealed class Result { + data class Success(val data: String) : Result() + data class Error(val message: String) : Result() +} +val result: Result = Result.Success("Data") +when (result) { + is Result.Success -> println("Success: ${result.data}") + is Result.Error -> println("Error: ${result.message}") +} +""" + +RUST = """\ +use std::collections::HashMap; + +// Constants +const PI: f64 = 3.14159; + +// Structs +struct Rectangle { + width: u32, + height: u32, +} + +impl Rectangle { + fn area(&self) -> u32 { + self.width * self.height + } +} + +// Enums +enum Result { + Ok(T), + Err(E), +} + +// Functions +fn greet(name: &str) { + println!("Hello, {}!", name); +} + +fn main() { + // Variables + let name = "John"; + let mut age = 30; + let is_student = true; + + // Printing variables + println!("Hello, {}! You are {} years old.", name, age); + + // Conditional statements + if age >= 18 && is_student { + println!("You are an adult student."); + } else if age >= 18 { + println!("You are an adult."); + } else { + println!("You are a minor."); + } + + // Arrays + let numbers = [1, 2, 3, 4, 5]; + println!("Numbers: {:?}", numbers); + + // Vectors + let mut fruits = vec!["apple", "banana", "orange"]; + fruits.push("grape"); + println!("Fruits: {:?}", fruits); + + // Loops + for num in &numbers { + println!("Number: {}", num); + } + + // Pattern matching + let result = Result::Ok(42); + match result { + Result::Ok(value) => println!("Value: {}", value), + Result::Err(error) => println!("Error: {:?}", error), + } + + // Ownership and borrowing + let s1 = String::from("hello"); + let s2 = s1.clone(); + println!("s1: {}, s2: {}", s1, s2); + + // References + let rect = Rectangle { + width: 10, + height: 20, + }; + println!("Rectangle area: {}", rect.area()); + + // Hash maps + let mut scores = HashMap::new(); + scores.insert("Alice", 100); + scores.insert("Bob", 80); + println!("Alice's score: {}", scores["Alice"]); + + // Closures + let square = |num: i32| num * num; + println!("Square of 5: {}", square(5)); + + // Traits + trait Printable { + fn print(&self); + } + + impl Printable for Rectangle { + fn print(&self) { + println!("Rectangle: width={}, height={}", self.width, self.height); + } + } + rect.print(); + + // Modules + greet("Alice"); +} +""" + +JAVA = """\ +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// Classes and interfaces +interface Shape { + double getArea(); +} + +class Rectangle implements Shape { + private double width; + private double height; + + public Rectangle(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public double getArea() { + return width * height; + } +} + +// Enums +enum DaysOfWeek { + MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY +} + +public class Main { + // Constants + private static final double PI = 3.14159; + + // Methods + public static int sum(int a, int b) { + return a + b; + } + + public static void main(String[] args) { + // Variables + String name = "John"; + int age = 30; + boolean isStudent = true; + + // Printing variables + System.out.println("Hello, " + name + "! You are " + age + " years old."); + + // Conditional statements + if (age >= 18 && isStudent) { + System.out.println("You are an adult student."); + } else if (age >= 18) { + System.out.println("You are an adult."); + } else { + System.out.println("You are a minor."); + } + + // Arrays + int[] numbers = {1, 2, 3, 4, 5}; + System.out.println("Numbers: " + Arrays.toString(numbers)); + + // Lists + List fruits = new ArrayList<>(); + fruits.add("apple"); + fruits.add("banana"); + fruits.add("orange"); + System.out.println("Fruits: " + fruits); + + // Loops + for (int num : numbers) { + System.out.println("Number: " + num); + } + + // Hash maps + Map scores = new HashMap<>(); + scores.put("Alice", 100); + scores.put("Bob", 80); + System.out.println("Alice's score: " + scores.get("Alice")); + + // Exception handling + try { + int result = 10 / 0; + } catch (ArithmeticException e) { + System.out.println("Error: " + e.getMessage()); + } + + // Instantiating objects + Rectangle rect = new Rectangle(10, 20); + System.out.println("Rectangle area: " + rect.getArea()); + + // Enums + DaysOfWeek today = DaysOfWeek.MONDAY; + System.out.println("Today is " + today); + + // Calling methods + int sum = sum(5, 10); + System.out.println("Sum: " + sum); + + // Ternary operator + String message = age >= 18 ? "You are an adult." : "You are a minor."; + System.out.println(message); + } +} +""" + SNIPPETS = { "python": PYTHON, "markdown": MARKDOWN, @@ -462,4 +1026,10 @@ def say_hello(): "html": HTML, "json": JSON, "regex": REGEX, + "go": GO, + "javascript": JAVASCRIPT, + "bash": BASH, + "kotlin": KOTLIN, + "rust": RUST, + "java": JAVA, } diff --git a/tests/snapshot_tests/snapshot_apps/button_markup.py b/tests/snapshot_tests/snapshot_apps/button_markup.py new file mode 100644 index 0000000000..2cd68a4719 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/button_markup.py @@ -0,0 +1,14 @@ +from textual.app import App, ComposeResult +from textual.widgets import Button + + +class ButtonsWithMarkupApp(App): + def compose(self) -> ComposeResult: + yield Button("[italic red]Focused[/] Button") + yield Button("[italic red]Blurred[/] Button") + yield Button("[italic red]Disabled[/] Button", disabled=True) + + +if __name__ == "__main__": + app = ButtonsWithMarkupApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/width_100.py b/tests/snapshot_tests/snapshot_apps/width_100.py new file mode 100644 index 0000000000..9e9eefa587 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/width_100.py @@ -0,0 +1,36 @@ +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import Label + + +class Width100PCentApp(App[None]): + + CSS = """ + Vertical { + border: solid red; + width: auto; + + Label { + border: solid green; + } + + #first { + width: 100%; + } + + #second { + width: auto; + } + } + """ + + def compose(self) -> ComposeResult: + with Vertical(): + yield Label("I want to be 100% of my parent", id="first") + yield Label( + "I want my parent to be wide enough to wrap me and no more", id="second" + ) + + +if __name__ == "__main__": + Width100PCentApp().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index cf6ccb2fa3..600f48af06 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -686,6 +686,7 @@ async def run_before(pilot): run_before=run_before, ) + def test_markdown_space_squashing(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "markdown_whitespace.py") @@ -1170,3 +1171,12 @@ def test_button_widths(snap_compare): def test_welcome(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "welcome_widget.py") + +def test_button_with_console_markup(snap_compare): + """Regression test for https://github.com/Textualize/textual/issues/4328""" + assert snap_compare(SNAPSHOT_APPS_DIR / "button_markup.py") + + +def test_width_100(snap_compare): + """Regression test for https://github.com/Textualize/textual/issues/4360""" + assert snap_compare(SNAPSHOT_APPS_DIR / "width_100.py") diff --git a/tests/text_area/test_history.py b/tests/text_area/test_history.py index b6a30e4132..8d50a63f83 100644 --- a/tests/text_area/test_history.py +++ b/tests/text_area/test_history.py @@ -341,3 +341,16 @@ async def test_redo_stack(pilot: Pilot, text_area: TextArea): text_area.redo() assert len(text_area.history.undo_stack) == 2 assert len(text_area.history.redo_stack) == 0 + + +async def test_backward_selection_undo_redo(pilot: Pilot, text_area: TextArea): + # Failed prior to https://github.com/Textualize/textual/pull/4352 + text_area.text = SIMPLE_TEXT + text_area.selection = Selection((3, 2), (0, 0)) + + await pilot.press("a") + + text_area.undo() + await pilot.press("down", "down", "down", "down") + + assert text_area.text == SIMPLE_TEXT